Headless mode, scripts and CI automation

Run the agent non-interactively, parse its JSON output, gate a pipeline on exit codes, and keep CI permissions tighter than your laptop's.

Non-interactive runs

# one prompt, one answer, no session UI
claude -p "summarise what changed in this commit" 

# machine-readable output
claude -p "list every TODO added in this branch" --output-format json

# streaming events, one JSON object per line
claude -p "review the diff" --output-format stream-json --verbose

# bound the run
claude -p "fix the lint errors" \
  --max-turns 10 \
  --allowedTools "Read,Edit,Bash(npm run lint:*)" \
  --permission-mode acceptEdits
FlagPurpose
-p, --printRun once and print the result, then exit
--output-format jsonSingle JSON object with the result and metadata
--output-format stream-jsonNewline-delimited events as the run progresses
--max-turnsHard ceiling on agent turns; the main cost control
--allowedToolsPre-approve a specific list, avoiding interactive prompts
--permission-modeSet the default posture for the run
--resume, -cContinue an earlier session id
⚠️
In CI, never use --dangerously-skip-permissions. On a runner with a deploy token and network access, an unconstrained agent is a remote code execution primitive with your credentials attached. Constrain tools, run on a read-only checkout where possible, and let a separate job apply changes.

Parsing output and gating on exit codes

#!/usr/bin/env bash
set -euo pipefail

out=$(claude -p "Review the staged diff. Reply with exactly PASS or FAIL then one sentence." \
  --output-format json \
  --max-turns 3 \
  --allowedTools "Read,Grep,Bash(git diff:*)")

# json output carries the text, cost and turn count
echo "$out" | jq '{result, num_turns, total_cost_usd, is_error}'

if [ "$(echo "$out" | jq -r '.is_error')" = "true" ]; then
  echo "agent run failed" >&2
  exit 1
fi

case "$(echo "$out" | jq -r '.result')" in
  PASS*) echo "review passed" ;;
  *)     echo "review requested changes" >&2; exit 1 ;;
esac
  • The JSON result includes the text, the number of turns, an error flag and a cost figure - log all four.
  • Ask for a machine-checkable first token (PASS or FAIL) instead of parsing prose.
  • Exit non-zero from your wrapper when the verdict is bad, so the pipeline and the reviewer both see it.
  • Save the stream-json transcript as a build artefact; it is the only record of what the agent actually did.

A review job in a pipeline

name: agent-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
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p "Review the diff against the base branch. Report only defects that would block a merge, with file:line." \
            --output-format json \
            --max-turns 8 \
            --allowedTools "Read,Grep,Bash(git diff:*),Bash(git log:*)" \
            > review.json || true

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: agent-review
          path: review.json
  • Give the job contents: read; a reviewing agent needs no write scope.
  • Use a dedicated API key with a spend limit, and set a per-run turn ceiling so a loop cannot burn the budget.
  • Run on pull requests rather than pushes to main - it keeps the cost proportional to review volume.
  • Set an explicit job timeout as well; a hung run should fail fast rather than hold a runner.

FAQ

What is the difference between exit codes and the JSON error flag?
The process exit code tells the shell that the invocation itself failed; is_error in the JSON tells you the agent run reported a problem. CI should check both, because a run can finish cleanly at the process level while reporting an in-band failure.
How do I keep CI cost predictable?
Cap --max-turns, restrict --allowedTools, choose a smaller model for review-style tasks, and only run on the events that need it. Log total_cost_usd per run so a regression is visible in the build history rather than on the invoice.

Hooks and deterministic automation Cost, model choice and troubleshooting

Last refreshed 2026-09-18.