Skip to content

Claude Code runs in CI/CD through the anthropics/claude-code-action@v1 GitHub Action and through headless mode (claude -p) in any other pipeline. The action answers @claude mentions or runs a fixed prompt on any GitHub event, such as a label or a failed build. Every run is bounded by tool allowlists, turn limits and the same required checks as human code.

Your team opens 30 pull requests a week, each waits half a day for a first human pass, and the build breaks twice a week on a forgotten lint rule. You want an agent in the pipeline that cannot push a deleted test to green or leak a secret to a stranger’s pull request. This page is for the developer who wires the workflows and the tech lead who decides what they are allowed to do.

What a working Claude Code CI setup gives you

Section titled “What a working Claude Code CI setup gives you”
  • An @claude workflow that answers requests in issue and pull request comments
  • An automatic review pass on every pull request that posts inline findings, plus a custom security-focused review prompt
  • A labelled-issue-to-pull-request job with a spec gate in front of it
  • A CI auto-fix workflow that cannot delete or skip a test to go green and never runs the branch’s code next to your API key, because a separate job with no secrets re-runs the gate before anything is pushed
  • A headless claude -p recipe for GitLab, Jenkins or any runner, with structured JSON output a script can gate on
  • Bedrock, Google Cloud and Foundry configurations, cost caps, and the failure modes to expect in the first week

Which Claude Code CI entry point should you use?

Section titled “Which Claude Code CI entry point should you use?”
You wantUseResult lands in
Ad hoc requests from people on issues and PRsThe action with no prompt input (interactive mode)A comment Claude updates as it works
A fixed task on an event (PR opened, label, schedule)The action with a prompt input (automation mode)The run log, or a comment if the prompt asks
Review on every PR without maintaining a workflowManaged Code Review (Team and Enterprise, research preview)Inline PR comments
Claude in GitLab, Jenkins, Buildkite or a cron boxHeadless claude -p in a scriptstdout, JSON you parse
A recurring task that needs no repository eventRoutinesA cloud session

With no prompt the action waits for the trigger phrase (@claude by default); with one it runs immediately. For the pipeline YAML itself (caching, matrices, path filters) see pipeline configuration using Claude Code. This page covers the agent running inside that pipeline.

Install the Claude GitHub App and the @claude workflow

Section titled “Install the Claude GitHub App and the @claude workflow”
  1. Run the installer from a local session. Install the GitHub CLI and run gh auth login. Open claude in the repository and enter /install-github-app. You need admin access to the repository, and the command works only for github.com remotes. It installs the Claude GitHub App and stores a repository secret: ANTHROPIC_API_KEY for an API key, or CLAUDE_CODE_OAUTH_TOKEN for a subscription token (generate one with claude setup-token).

  2. Merge the workflow pull request it opens. The installer pushes a branch with claude.yml (and claude-code-review.yml if you select the review workflow) and opens GitHub on a ready pull request. Merging it activates @claude.

  3. Or add the workflow by hand. Use manual setup for Bedrock, Google Cloud or Foundry, or when you want to control the file. Install the Claude GitHub App, add the secret, and commit this file:

    .github/workflows/claude.yml
    name: Claude Code
    on:
    issue_comment:
    types: [created]
    pull_request_review_comment:
    types: [created]
    jobs:
    claude:
    if: contains(github.event.comment.body, '@claude')
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
    contents: write
    pull-requests: write
    issues: write
    id-token: write # the action's GitHub App authentication
    actions: read # lets Claude read CI results on the PR
    steps:
    - uses: actions/checkout@v7
    with:
    fetch-depth: 1
    persist-credentials: false
    - uses: anthropics/claude-code-action@v1
    with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    claude_args: '--max-turns 30'
  4. Test it. Comment @claude fix the TypeError in the dashboard component on an issue. Claude replies in a comment it updates as it works and commits to a new branch. Its comment links to GitHub’s pull request page, so a person decides what gets proposed.

On a pull request, the action restores Claude’s own configuration paths from the base branch before Claude starts: .claude/, .mcp.json, .claude.json, CLAUDE.md, CLAUDE.local.md, .husky/ and a few others. A pull request therefore cannot bring its own hooks or MCP servers. Everything else stays at the pull request’s head, including package.json, so a base-branch hook that calls npm run format runs a script the pull request wrote. Call tools directly, with a pinned version (the action’s docs/security.md, read 2026-09-26).

Which action inputs exist, and which are silently ignored?

Section titled “Which action inputs exist, and which are silently ignored?”

The common inputs are prompt, claude_args, anthropic_api_key or claude_code_oauth_token, github_token, settings, plugins, plugin_marketplaces, trigger_phrase, track_progress, allowed_bots, and use_bedrock, use_vertex or use_foundry (full list in action.yml). The beta-era model, max_turns, timeout_minutes, direct_prompt and custom_instructions do not exist in v1 (action.yml on main, 2026-09-26): GitHub prints an “Unexpected input” warning and the run continues with defaults. Move CLI options into claude_args:

  • --model claude-sonnet-5 pins the model. Without it the action uses the Claude Code default model, so the per-run cost moves when the default moves. See the models hub before choosing.
  • --max-turns 10 ends the run with an error when Claude reaches the limit.
  • --allowedTools "..." grants tools. In automation mode Claude has no shell and no GitHub API access beyond what you grant.
  • --append-system-prompt "..." replaces the old custom_instructions.

The job clock is GitHub’s own timeout-minutes: on the job, not an action input.

To store no long-lived Claude credential, the action can exchange the workflow’s OIDC token for API access through a Claude Console service account. Set anthropic_federation_rule_id and anthropic_organization_id (plus anthropic_service_account_id or anthropic_workspace_id when the rule needs them) and keep id-token: write. If you share one secret across an organization, make it a Console API key: a CLAUDE_CODE_OAUTH_TOKEN is tied to the subscription of whoever ran claude setup-token.

Review every pull request with Claude Code

Section titled “Review every pull request with Claude Code”

The quick setup’s review workflow runs the code-review plugin from Anthropic’s claude-code marketplace. This is the same workflow written out, so you control triggers, model and permissions:

.github/workflows/claude-review.yml
name: Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
jobs:
review:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment"'

Two lines decide whether anyone sees the review. --comment makes Claude post inline comments, or one summary comment when it finds nothing; without it the findings stay in the run log. The action starts the inline-comment MCP server only when --allowedTools grants it. On public repositories GitHub withholds secrets from fork pull requests, so this job reviews same-repository branches only.

To make the review follow your house rules, write them in CLAUDE.md or in a skill under .claude/skills/ and invoke it as the prompt (/security-pass). The checkout step must run first so the skill files exist on the runner. Custom slash commands are skills now; see custom commands.

Use it as the prompt: of the workflow above in place of the plugin call, and extend --allowedTools with Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr comment:*), so Claude can read the diff and post the summary comment. The prompt’s confirmed: true line matters too: the action’s inline-comment tool holds back unconfirmed comments and classifies them after the session, so a custom prompt without it can see comments delayed or filtered out. The severity labels matter: a review that flags everything equally is ignored by week two, and “nothing HIGH, here is what I checked” is evidence a reviewer can use.

Treat this review as one sensor, not as the approval. What a person reads instead of the whole diff is covered in reviewing an agent’s pull request without reading every line. For local review before a push, see review automation.

The label is the spec gate: nothing runs until a person decides the issue has acceptance criteria an agent can meet.

.github/workflows/issue-to-pr.yml
name: Issue to PR
on:
issues:
types: [labeled]
permissions: {}
jobs:
implement:
# Holds the API key and a read-only token. Claude edits and tests; it cannot push.
if: >-
github.event.label.name == 'claude-implement' &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
issues: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }} # scoped to this job's read-only permissions
prompt: |
Implement issue #${{ github.event.issue.number }} in ${{ github.repository }}.
Read it with `gh issue view ${{ github.event.issue.number }}` and ignore its comments.
Follow the instructions in .github/claude/implement.md.
Write the pull request body to ${{ runner.temp }}/pr-body.md.
claude_args: >-
--max-turns 40
--add-dir ${{ runner.temp }}
--allowedTools "Read,Edit,Write,Glob,Grep,Bash(gh issue view:*),Bash(npm ci),Bash(npm test:*),Bash(npm run lint:*),Bash(npm run typecheck:*)"
- name: Export Claude's change as a patch
run: |
git add -A && git diff --cached --binary > "$RUNNER_TEMP/change.patch"
[ -s "$RUNNER_TEMP/pr-body.md" ] || echo "Claude wrote no PR body." > "$RUNNER_TEMP/pr-body.md"
- uses: actions/upload-artifact@v7
with:
name: claude-implement
path: |
${{ runner.temp }}/change.patch
${{ runner.temp }}/pr-body.md
open-pr:
# Holds the write token, never sees the API key, runs nothing Claude wrote.
needs: implement
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: claude-implement
path: ${{ runner.temp }}
- name: Push claude/issue-N and open a PR, or post Claude's questions
env:
GH_TOKEN: ${{ github.token }}
ISSUE: ${{ github.event.issue.number }}
run: |
if [ ! -s "$RUNNER_TEMP/change.patch" ]; then
gh issue comment "$ISSUE" --body-file "$RUNNER_TEMP/pr-body.md"; exit 0; fi
BR="claude/issue-$ISSUE"
git apply --index "$RUNNER_TEMP/change.patch"
if git diff --cached --name-only | grep '^\.github/'; then
echo "Refusing: the change touches .github/"; exit 1; fi
git config user.name "claude[bot]"
git config user.email "claude[bot]@users.noreply.github.com"
git switch -c "$BR"
git commit -m "Implement #$ISSUE"
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$BR"
gh pr create --head "$BR" --title "Implement #$ISSUE" --body-file "$RUNNER_TEMP/pr-body.md"

The prompt passes the issue number, not ${{ github.event.issue.body }}. Interpolating the body makes the issue author’s text part of your instructions; passing the number keeps your instructions yours and leaves the issue as data Claude reads. That lowers prompt-injection risk but does not remove it, so the workflow adds four controls around it:

  • Who wrote the text, and who labelled it. Triage users can apply the label too, but claude-code-action refuses to start for any actor without write or admin access unless you set allowed_non_write_users, so only a write-access labeller starts a run. That gate lives in the action (checkWritePermissions, checked on main at 97c5347, 2026-10-01): replace the action with a plain claude -p step and you lose it. The author_association condition skips issues not written by an owner, organisation member or collaborator, and COLLABORATOR includes read- and triage-level collaborators. Claude reads the issue without its comments, because anyone can comment after the label goes on.
  • No git for Claude. An allowlist entry such as Bash(git:*) is arbitrary shell: git -c alias.x='!sh -c …' x runs any command, and the action ships its own scripts/git-push.sh wrapper because git push --receive-pack=… does the same. Here Claude has no git and a token scoped to contents: read.
  • The write token sits in another job. open-pr never sees the API key and runs nothing Claude wrote: it applies the patch, refuses any change under .github/, then pushes claude/issue-<number> and opens the pull request, or posts Claude’s questions on the issue when there is no change.
  • Every checkout uses persist-credentials: false, so no step leaves a token in .git/config for a later step to find.

npm test is still code execution beside the API key, because Claude writes the tests it runs. That is acceptable here only because both the code (the default branch) and the instructions (a trusted author’s issue) are trusted. For issues from outside contributors, use the shape of the CI-fix workflow below: file tools only for Claude and a job with no secrets for the gate. A pull request opened with the workflow’s GITHUB_TOKEN does not trigger other workflows, so use a GitHub App token in open-pr if CI must run on it.

The risk is not theoretical. In the chain security researcher Adnan Khan named “Clinejection”, as Simon Willison summarised it on 2026-03-06 (a secondary account), a prompt injected through an issue title into an AI triage workflow built on claude-code-action with Bash, Write and Edit tools led to Actions cache poisoning and stolen publish tokens.

A pull request opened this way carries its own evidence: criteria mapped to tests, commands run, open questions. That is the shape the evidence bundle defines, and it is what lets a reviewer approve on evidence rather than on a line-by-line read.

Fix a failing CI run without weakening the tests

Section titled “Fix a failing CI run without weakening the tests”

The dangerous version of this job checks out the failing branch, gives Claude the API key and a shell, and pushes what it produces. Sooner or later it deletes the flaky test that was failing. It also runs the branch’s package.json scripts, test files and build config in the same job as your secret, so anyone who can push a branch can read the key.

The version below splits the work into three jobs with different privileges:

JobHoldsExecutes the branch’s code?Does
patchAPI key, read-only tokenNoClaude reads the logs and edits a copy of the commit
verifyNothing: no secrets, read-only tokenYesApplies the patch, rejects weakened tests, re-runs the gate
open-prWrite tokenNoCommits the verified patch to a new branch and opens a PR
.github/workflows/claude-fix-ci.yml
name: Claude fixes CI
on:
workflow_run:
workflows: ['CI']
types: [completed]
permissions: {}
jobs:
patch:
# Holds the API key. Reads and edits the failing commit, never executes it.
if: >-
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.pull_requests[0] &&
!startsWith(github.event.workflow_run.head_branch, 'claude-fix/')
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
actions: read
id-token: write
steps:
- uses: actions/checkout@v7 # trusted default branch at the workspace root
with:
persist-credentials: false
- uses: actions/checkout@v7 # the failing commit, in a subdirectory
with:
ref: ${{ github.event.workflow_run.head_sha }}
path: pr-head
persist-credentials: false
- name: Save the failing jobs' logs
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --log-failed | tail -c 60000 > "$RUNNER_TEMP/ci-failure.log"
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
CI failed on the commit checked out in pr-head/. The failing jobs' logs are in
${{ runner.temp }}/ci-failure.log. Edit files under pr-head/ only.
Follow the CI-fix rules in .github/claude/fix-ci.md.
claude_args: >-
--max-turns 25
--add-dir pr-head ${{ runner.temp }}
--allowedTools "Read,Edit,Write,Glob,Grep"
- name: Export Claude's change as a patch
run: git -C pr-head add -A && git -C pr-head diff --cached --binary > fix.patch
- uses: actions/upload-artifact@v7
with:
name: claude-fix
path: fix.patch
verify:
# Executes the branch's code, so it gets no secrets and a read-only token.
needs: patch
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: claude-fix
path: ${{ runner.temp }}
- name: Apply the patch and re-run the gate outside Claude
run: |
[ -s "$RUNNER_TEMP/fix.patch" ] || { echo "Claude changed nothing"; exit 1; }
git apply --index "$RUNNER_TEMP/fix.patch"
if git diff --cached --name-only --diff-filter=D | grep -E '\.(test|spec)\.'; then
echo "Refusing: a test file was deleted"; exit 1; fi
if git diff --cached -U0 | grep -E '^\+.*\.(skip|only)\(|^\+.*\bxit\('; then
echo "Refusing: a test was skipped or focused"; exit 1; fi
if git diff --cached --name-only | grep -E '^\.github/|(^|/)package(-lock)?\.json$|(jest|vitest|playwright|karma)\.config\.'; then
echo "Refusing: the patch edits CI, package scripts or test config"; exit 1; fi
npm ci && npm run lint && npm run typecheck && npm test
open-pr:
# Can write to the repository, but runs no code from the branch.
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: claude-fix
path: ${{ runner.temp }}
- name: Push to a fix branch and open a PR
env:
GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
RUN_ID: ${{ github.run_id }}
run: |
SAFE=$(printf '%s' "$HEAD_BRANCH" | tr -cd 'a-zA-Z0-9/_.-')
BR="claude-fix/$SAFE-$RUN_ID"
git apply --index "$RUNNER_TEMP/fix.patch"
git config user.name "claude[bot]"
git config user.email "claude[bot]@users.noreply.github.com"
git switch -c "$BR"
git commit -m "fix: repair CI failure from run $RUN_ID"
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$BR"
gh pr create --head "$BR" --base "$HEAD_BRANCH" --fill

Four choices carry the safety. Claude gets file tools and no shell, so the branch’s code never runs beside the key. That code runs only in verify, which holds no secret and refuses a patch that touches .github/, package.json or a test config, so Claude cannot rewrite the gate that checks it. The claude-fix/ prefix check stops the workflow triggering itself. And the pull request targets the developer’s branch, so the author accepts or rejects the fix.

verify is not a sandbox, though. A workflow_run job runs in the default branch’s context and shares its Actions cache scope, so untrusted code there can poison the cache without any secret, which is the Clinejection vector cited above. Keep actions/cache and setup-node caching out of verify, and never restore caches in release or publish workflows.

The price is that Claude cannot run the tests, so a failed verify means no pull request, not a retry; the claude-fix artifact keeps the patch and diagnosis. A pull request opened with the workflow’s GITHUB_TOKEN does not trigger other workflows; verify already ran the gate, and a GitHub App token (actions/create-github-app-token) in open-pr makes CI run on it as well.

The prompt file lives on the default branch, so a pull request cannot rewrite the rules Claude follows. CLAUDE_FIX_NOTES.md rides along in the commit for the reviewer; delete it before merging or move it into the PR body.

Run Claude Code headless in GitLab, Jenkins or any runner

Section titled “Run Claude Code headless in GitLab, Jenkins or any runner”

Outside GitHub Actions, call the CLI directly. In CI, load nothing from the checkout. --bare skips hooks, plugin sync, auto memory, LSP and CLAUDE.md auto-discovery, and since v2.1.286 (both release channels on 2026-10-12) it connects only the MCP servers named on the command line, sends no system reminders and starts no background tasks. --setting-sources "" skips the user, project and local settings files. --strict-mcp-config ignores every MCP server you did not pass with --mcp-config. Bare mode authenticates only with ANTHROPIC_API_KEY, an apiKeyHelper passed with --settings, or cloud provider credentials, never with a subscription login or the keychain (claude --help, 2.1.283; the v2.1.286 change: Claude Code changelog, checked 2026-10-12).

Terminal window
# In the CI job; ANTHROPIC_API_KEY comes from the CI secret store.
CI_TMP=$(mktemp -d)
git diff "origin/$TARGET_BRANCH...HEAD" > "$CI_TMP/change.diff"
claude --bare -p "Classify the risk of the change in $CI_TMP/change.diff for release. Read the files it touches. Answer in the schema." \
--setting-sources "" --strict-mcp-config \
--add-dir "$CI_TMP" \
--allowedTools "Read,Grep,Glob" \
--permission-mode dontAsk \
--max-turns 15 --max-budget-usd 3 \
--output-format json \
--json-schema '{"type":"object","properties":{"risk":{"enum":["low","medium","high"]},"reasons":{"type":"array","items":{"type":"string"}}},"required":["risk","reasons"]}' \
> risk.json
jq -e '.is_error == false and (.structured_output.risk == "low" or .structured_output.risk == "medium")' risk.json || { jq '.structured_output.reasons // .result' risk.json; exit 1; }

--permission-mode dontAsk denies anything that would prompt instead of waiting for an answer that never comes (reads in the working directories and read-only commands still run). --max-budget-usd stops the run at a dollar cap and works only with -p. --json-schema puts a validated object in the structured_output field, so the pipeline branches on a value, not on free text. A run that errors, hits the budget or hits the turn cap has no structured_output, so the gate must treat a missing verdict as a failure; that is why the jq line checks is_error and allows only the two known-safe values. --max-turns does not appear in claude --help output but is documented in the CLI reference and works with -p (checked on 2.1.283). For a job that must only read, --restricted also removes Bash and the other code-running tools and confines the file tools to the working directories and --add-dir.

For GitLab there is also a GitLab-maintained Claude Code GitLab CI/CD integration, in beta. For programmatic control beyond a shell call, use the Claude Agent SDK. Longer multi-step scripts belong in automation with Claude Code.

Run the action on Bedrock, Google Cloud or Foundry

Section titled “Run the action on Bedrock, Google Cloud or Foundry”

With a cloud provider, the action authenticates through OIDC federation, so the repository stores no static cloud credentials. Set one input (use_bedrock, use_vertex or use_foundry), grant id-token: write, and add the provider’s login step before the action.

permissions:
contents: write
pull-requests: write
issues: write
id-token: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-west-2 # exported as AWS_REGION for the rest of the job
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: 'true'
claude_args: '--model us.anthropic.claude-opus-5-5 --max-turns 10'

Use the inference-profile ID your Bedrock console shows for the region group where you granted model access, for example us. plus the Bedrock model ID anthropic.claude-opus-5-5. The IAM role needs Anthropic’s scoped invocation policy, which includes bedrock:InvokeModel.

On a public repository, any comment with the trigger phrase starts these jobs, and the cloud login runs before the action checks the commenter’s write access. Add a first step that checks the commenter’s permission with gh api and exits early. The full provider setup, including a custom GitHub App with only Contents, Issues and Pull requests, is in Anthropic’s cloud providers guide and our enterprise integration page.

Each run spends GitHub Actions minutes and model tokens; a CLAUDE_CODE_OAUTH_TOKEN bills the subscription instead of the API. Five controls do most of the work:

  • Do not run at all on drafts, docs-only changes or bot pull requests: if: github.event.pull_request.draft == false plus a paths: filter on the trigger.
  • Pin the model in claude_args, so a change of default does not change your bill. Prices live on the models hub.
  • Cap the work: --max-turns in the action, --max-turns plus --max-budget-usd in headless runs.
  • Cap the clock with the job’s timeout-minutes:.
  • Cancel superseded runs with a concurrency: group per pull request, as in the review workflow above.

Managed Code Review is billed separately: Anthropic’s documentation says each review “averages $15-25” and scales with PR size (Code Review docs, checked 2026-09-26). Track CI spend next to interactive usage with cost control for Claude Code.

How do you know the automated output is right?

Section titled “How do you know the automated output is right?”

The proof comes from gates Claude cannot edit:

  1. Required status checks. Branch protection requires the same tests, typecheck and lint jobs on Claude’s pull requests as on anyone else’s. The auto-fix workflow re-runs them in a job Claude never touches.
  2. Protected tests. A CODEOWNERS rule on test directories and CI config means a change to the oracle needs a named human’s approval. The auto-fix workflow refuses deleted, skipped or focused tests before pushing.
  3. Evidence in the PR body. Acceptance criteria mapped to tests, commands run, and open questions, following the evidence bundle.
  4. Structured verdicts. Headless jobs return --json-schema output, and the pipeline fails on a value rather than on a human reading prose.
  5. A human approver per risk class. Claude’s review comments are input. The merge decision stays with the person CODEOWNERS names, and for high-risk paths that person reads the sampled hotspots described in agent PR review.

For a tech lead, the adoption order is: the review workflow first (read-only, low risk), then @claude, then issue-to-PR behind a label, then auto-fix once the verify job and CODEOWNERS on tests are in place.

Claude does not answer @claude. Check the Claude GitHub App installation, that Actions are enabled, the secret name, that @claude is a whole word, and the commenter’s write access. The run log names the check that rejected the event.

CI does not run on Claude’s commits. GitHub does not trigger workflows on pushes made with the default GITHUB_TOKEN. Remove github_token: ${{ secrets.GITHUB_TOKEN }} from the action so it authenticates as the Claude GitHub App, or pass a custom GitHub App token.

The review runs but posts nothing. The prompt lacks --comment, or claude_args does not allow mcp__github_inline_comment__create_inline_comment. The code-review plugin also skips drafts, closed pull requests, and pull requests Claude already commented on.

The workflow ignores your model or turn limit. You set model: or max_turns: as action inputs. Look for the “Unexpected input” warning annotation on the run, and move the values into claude_args.

A scheduled job fails the actor check. GitHub attributes scheduled runs to the user who last changed the cron line. If that is a bot, add it to allowed_bots. In public repositories GitHub also disables schedules after 60 days without activity.

The job ends with a max-turns error. The task is too large for one run. Split it (one issue per run, one failing job per fix) before raising the limit.

The auto-fix workflow never opens a pull request. verify failed, or Claude stopped by rule and wrote only a diagnosis. Read CLAUDE_FIX_NOTES.md in the claude-fix artifact and finish by hand. If verify fails on most runs, keep a person on these failures rather than giving the key-holding job a shell.

open-pr fails with “GitHub Actions is not permitted to create or approve pull requests”. The fix branch is pushed, but the repository or organization blocks the workflow’s GITHUB_TOKEN from opening pull requests. Enable “Allow GitHub Actions to create and approve pull requests” under Settings → Actions → General, or use a GitHub App token in open-pr.

The auto-fix workflow opens a pull request that weakens a test. The patterns in verify missed it, for example a lowered coverage threshold. Close the PR, add that pattern to verify, and put the config file behind CODEOWNERS.

A release or publish job runs code you never merged. A job that executed branch code, such as verify, wrote to the default branch’s Actions cache, and a later trusted workflow restored it. Remove caching from every job that runs untrusted code, stop restoring caches in release and publish workflows, and clear the repository’s caches under Actions → Caches.

Bedrock or Google Cloud returns 401 or 403. The OIDC trust policy does not match this repository or branch, or the role lacks model-invocation permission, or model access is missing in one region of the inference profile.