Build with Syntic CodeGuidesRun in CI pipelines

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

JobTrigger
Review every pull request or merge request and post findingsA 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 fixA failed check
Summarize the day’s merges, find flaky tests, update dependenciesA 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_folders

Skip 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:

JobSuggested 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.md

Respond 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 push
⚠️

Only 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.md

GitLab 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.json

Fail 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.json

Good 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

ControlEffect
--max-turns <n>Stops after n agent turns.
--max-budget-usd <amount>Stops when the estimated cost passes the amount.
--effort lowLess reasoning for simple jobs.
--permission-mode planNo edits at all for review and report jobs.
--exclude-dynamic-system-prompt-sectionsLeaves machine-specific details out of the system prompt so runs on different runners share the prompt cache.
MCP_CONNECTION_NONBLOCKING=trueDoes 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

ProblemWhat 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 ignoredThe checkout is not trusted. Add it to ~/.syntic/trusted_folders as shown above.
The agent says a tool call was refusedThe run needed a permission it did not have. Add the command to --allowed-tools, or change --permission-mode.
The job runs for a long timeAdd --max-turns and --max-budget-usd. Ask for a narrower task.
The review comment is emptyRead review.json: .is_error and .result explain what went wrong.
Commits from the agent do not trigger CIPushes made with the CI system’s own token often do not start new pipelines. Push with a separate token if you need that.