Sparround

The agent in CI: headless mode and automation

Besides interactive mode, Claude Code runs non-interactively (headless). That is intended for scripts and CI:

  • -p "<prompt>" — a non-interactive run.
  • --output-format json — structured output (result, session ID, usage stats, a cost estimate).
  • --json-schema '<schema>' — constrain the output to a specific JSON schema.
  • --allowedTools "Read,Edit,Bash" — which tools are permitted.
  • --permission-mode dontAsk — anything not on the list is denied.
  • --bare — skips auto-discovery of hooks, skills, plugins and MCP servers.

The exit code is 0 on success and non-zero on failure, so scripts can branch on it.

`--bare` is the recommended mode for CI. The reason is security: without it a -p session runs the hooks from the project's .claude/settings.json and connects to the servers in .mcp.json — with no approval dialog at all. So an untrusted fork's PR could execute code in your CI. --bare reads none of it.

CI scenarioSuitabilityNote
Automatic review comment on a PRGoodKeep it non-blocking — noise wears the team down
Explaining a build failureGoodPipe the log in — no Bash permission needed
Issue triage and labellingGoodKeep write permissions narrow
Drafting release notesGoodHuman check before publishing
Auto-fixing a failing testRiskyThe agent may change the test to make it pass
Automatic mergeNoThe human gate must not be removed
yaml
# GitHub Actions — a non-blocking review comment on a PR
name: AI review
on: pull_request

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install Claude Code
        run: curl -fsSL https://claude.ai/install.sh | bash

      - name: Review diff
        env:
          CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
        run: |
          git diff origin/${{ github.base_ref }}...HEAD -- '*.kt' > diff.txt
          # Pipe the diff in: the agent needs no Bash permission
          cat diff.txt | claude --bare -p \
            "Review this Android diff. Report ONLY blocking and medium
             severity problems: crash risk, resource leaks, forgotten
             error cases, broken Compose state hoisting.
             Do NOT write style notes. Format: [LEVEL] file:line - problem.
             If there are no problems, say so; do not invent findings." \
            --output-format json > review.json

      - name: Post comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          jq -r '.result' review.json > body.md
          gh pr comment ${{ github.event.pull_request.number }} --body-file body.md

Three things to note: `--bare` (security), piping the diff in (no Bash permission needed), and posting the result as a comment (non-blocking).

Security rules for the agent in CI:

  • Use `--bare` so hooks and MCP configuration from the repo don't run.
  • Least privilege — a narrow --allowedTools list, or --permission-mode dontAsk.
  • Token management — create a long-lived CI token with claude setup-token and store it as the CLAUDE_CODE_OAUTH_TOKEN secret. Note that --bare does not read that variable, so use ANTHROPIC_API_KEY there.
  • Beware fork PRs — content from an external fork is untrusted (prompt injection). Either don't run the agent on those, or run it read-only.
  • Track cost — the --output-format json output carries a per-invocation cost estimate; log it.

📚 Sources and documentation