Run in CI pipelines
Syntic Code runs in any CI system that can run a shell command. A job calls syntic -p "<task>", the agent does the work on the runner, and the job uses the result: a review comment on a pull request, a fix pushed to a branch, a report attached to a scheduled run.
There is no special CI product to install. Everything on this page is the normal CLI in one-shot mode, plus a few settings that make unattended runs safe and predictable.
What you can automate
| Job | Trigger |
|---|---|
| Review every pull request or merge request and post findings | A new or updated PR / MR |
| Answer a request in a comment (“@syntic add tests for this”) | A comment on a PR, MR or issue |
| Fix a failing build on a branch and push the fix | A failed check |
| Summarize the day’s merges, find flaky tests, update dependencies | A schedule |
Set up
Make the binary available on the runner
Put the syntic binary on the runner’s PATH: bake it into your runner image, or cache it between jobs. The examples below assume syntic is on the PATH.
Store a credential as a secret
CI runs sign in without a browser. The usual choice is a refresh token, stored as a masked secret and passed in SYNTIC_OAUTH_REFRESH_TOKEN (Sign in without a browser). Syntic Code strips it from the environment of every command, hook and MCP server it starts, so tools it runs cannot read it.
Trust the checkout
In a folder nobody has trusted, a -p run starts in restricted mode: the project’s settings, hooks and MCP servers are not loaded. A CI checkout is code you chose to build, so trust it before the run:
mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_foldersSkip this step if you do not want the repository’s own .syntic/settings.json, hooks or .mcp.json to apply in CI (for example, for pull requests from forks).
Decide what the run may do
Nobody can answer a permission prompt in CI. Anything that would ask is refused, and the agent is told why. Set the limits up front:
| Job | Suggested setup |
|---|---|
| Review, report, summarize | --permission-mode plan (reads only). |
| Fix and commit | --permission-mode acceptEdits plus --allowed-tools for the exact commands it needs. |
| Anything | --max-turns and --max-budget-usd to bound the run. |
GitHub Actions
Review every pull request
# .github/workflows/syntic-review.yml
name: Syntic review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Trust the checkout
run: mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_folders
- name: Review
env:
SYNTIC_OAUTH_REFRESH_TOKEN: ${{ secrets.SYNTIC_TOKEN }}
run: |
syntic -p "Review the changes between origin/${{ github.base_ref }} and HEAD for bugs, \
security problems and missing tests. Reply in Markdown: a one-line verdict, then a \
numbered list of findings with file and line. Reply LGTM if there are none." \
--permission-mode plan --max-turns 30 --max-budget-usd 2 \
--output-format json > review.json
- name: Post the review
env:
GH_TOKEN: ${{ github.token }}
run: |
jq -r .result review.json > review.md
gh pr comment ${{ github.event.pull_request.number }} --body-file review.mdRespond to a comment
Run when someone writes @syntic in a pull request comment, and let the agent push a commit:
on:
issue_comment:
types: [created]
jobs:
respond:
if: github.event.issue.pull_request && contains(github.event.comment.body, '@syntic')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Check out the PR branch
env:
GH_TOKEN: ${{ github.token }}
run: gh pr checkout ${{ github.event.issue.number }}
- name: Trust the checkout
run: mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_folders
- name: Do the task
env:
SYNTIC_OAUTH_REFRESH_TOKEN: ${{ secrets.SYNTIC_TOKEN }}
REQUEST: ${{ github.event.comment.body }}
run: |
syntic -p "A reviewer asked on this pull request: $REQUEST
Make the change, run the tests, and commit with a clear message. Do not push." \
--permission-mode acceptEdits \
--allowed-tools "Bash(npm test:*),Bash(git add:*),Bash(git commit:*)" \
--max-turns 40 --max-budget-usd 3
git pushOnly run comment-triggered jobs for people you trust: anyone who can comment can write the prompt. Check github.event.comment.author_association (for example, require MEMBER or OWNER) before the job runs.
Run on a schedule
on:
schedule:
- cron: "0 7 * * 1-5"
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- run: mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_folders
- env:
SYNTIC_OAUTH_REFRESH_TOKEN: ${{ secrets.SYNTIC_TOKEN }}
run: |
syntic -p "Summarize the commits merged to main in the last 24 hours for the team: \
what changed, anything risky, anything that needs a follow-up." \
--permission-mode plan > summary.md
- uses: actions/upload-artifact@v4
with:
name: daily-summary
path: summary.mdGitLab CI/CD
Review every merge request
# .gitlab-ci.yml
syntic-review:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
SYNTIC_OAUTH_REFRESH_TOKEN: $SYNTIC_TOKEN
GIT_DEPTH: "0"
script:
- mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_folders
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- >
syntic -p "Review the changes between origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME and HEAD
for bugs and security problems. Reply in Markdown with a verdict and a numbered list of findings."
--permission-mode plan --max-turns 30 --max-budget-usd 2 > review.md
- glab mr note "$CI_MERGE_REQUEST_IID" --message "$(cat review.md)"Store SYNTIC_TOKEN as a masked, protected CI/CD variable. Posting the note needs glab on the runner and a token with API access in GITLAB_TOKEN.
Turn an issue into a merge request
syntic-implement:
rules:
- if: $CI_PIPELINE_SOURCE == "trigger" && $ISSUE_IID
variables:
SYNTIC_OAUTH_REFRESH_TOKEN: $SYNTIC_TOKEN
script:
- mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_folders
- git checkout -b "syntic/issue-$ISSUE_IID"
- glab issue view "$ISSUE_IID" > issue.txt
- >
syntic -p "Implement the issue described in issue.txt. Add tests, run them, and commit."
--permission-mode acceptEdits
--allowed-tools "Bash(make test:*),Bash(git add:*),Bash(git commit:*)"
--max-turns 60 --max-budget-usd 5
- git push -u origin "syntic/issue-$ISSUE_IID"
- glab mr create --fill --source-branch "syntic/issue-$ISSUE_IID"Other CI systems
The pattern is the same everywhere:
export SYNTIC_OAUTH_REFRESH_TOKEN="$YOUR_SECRET"
mkdir -p ~/.syntic && pwd -P >> ~/.syntic/trusted_folders
syntic -p "<task>" --permission-mode plan --max-turns 30 --output-format json > result.json
jq -r .result result.jsonFail the job on the result if you need to. A run that fails exits with 1 and, with --output-format json, sets is_error to true. For a pass/fail answer you can rely on, ask for structured output:
syntic -p "Do the database migrations in this branch lock large tables?" \
--json-schema '{"type":"object","properties":{"safe":{"type":"boolean"},"why":{"type":"string"}},"required":["safe","why"]}' \
--permission-mode plan > verdict.json
jq -e '.safe' verdict.jsonGood practice
Write the rules down
Put your project’s conventions, test commands and review standards in SYNTIC.md. CI runs read it like any session, so reviews and fixes follow the same rules as local work (Memory).
Protect your credentials
- Keep the token in your CI system’s secret store, never in the repository or the workflow file.
- Give the CI token its own account or a scope you can revoke on its own.
- Do not run jobs that use secrets on pull requests from forks unless you have reviewed the change: the prompt and the code are both controlled by the author.
Keep runs bounded and cheap
| Control | Effect |
|---|---|
--max-turns <n> | Stops after n agent turns. |
--max-budget-usd <amount> | Stops when the estimated cost passes the amount. |
--effort low | Less reasoning for simple jobs. |
--permission-mode plan | No edits at all for review and report jobs. |
--exclude-dynamic-system-prompt-sections | Leaves machine-specific details out of the system prompt so runs on different runners share the prompt cache. |
MCP_CONNECTION_NONBLOCKING=true | Does not wait for slow MCP servers. |
When CI is set, background helpers and update checks stay quiet, and commands left running in the background are stopped when the run ends.
Troubleshooting
| Problem | What to do |
|---|---|
The run exits with 1 and “not signed in” | Check the secret reaches the job as SYNTIC_OAUTH_REFRESH_TOKEN, and has not expired or been revoked. |
| Project settings, hooks or MCP servers are ignored | The checkout is not trusted. Add it to ~/.syntic/trusted_folders as shown above. |
| The agent says a tool call was refused | The run needed a permission it did not have. Add the command to --allowed-tools, or change --permission-mode. |
| The job runs for a long time | Add --max-turns and --max-budget-usd. Ask for a narrower task. |
| The review comment is empty | Read review.json: .is_error and .result explain what went wrong. |
| Commits from the agent do not trigger CI | Pushes made with the CI system’s own token often do not start new pipelines. Push with a separate token if you need that. |