Running Claude Code in CI/CD with GitHub Actions and headless mode
PLClaude 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
@claudeworkflow 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 -precipe 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 want | Use | Result lands in |
|---|---|---|
| Ad hoc requests from people on issues and PRs | The 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 workflow | Managed Code Review (Team and Enterprise, research preview) | Inline PR comments |
| Claude in GitLab, Jenkins, Buildkite or a cron box | Headless claude -p in a script | stdout, JSON you parse |
| A recurring task that needs no repository event | Routines | A 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”-
Run the installer from a local session. Install the GitHub CLI and run
gh auth login. Openclaudein 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_KEYfor an API key, orCLAUDE_CODE_OAUTH_TOKENfor a subscription token (generate one withclaude setup-token). -
Merge the workflow pull request it opens. The installer pushes a branch with
claude.yml(andclaude-code-review.ymlif you select the review workflow) and opens GitHub on a ready pull request. Merging it activates@claude. -
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 Codeon:issue_comment:types: [created]pull_request_review_comment:types: [created]jobs:claude:if: contains(github.event.comment.body, '@claude')runs-on: ubuntu-latesttimeout-minutes: 30permissions:contents: writepull-requests: writeissues: writeid-token: write # the action's GitHub App authenticationactions: read # lets Claude read CI results on the PRsteps:- uses: actions/checkout@v7with:fetch-depth: 1persist-credentials: false- uses: anthropics/claude-code-action@v1with:anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}claude_args: '--max-turns 30' -
Test it. Comment
@claude fix the TypeError in the dashboard componenton 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-5pins 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 10ends 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 oldcustom_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:
name: Code Reviewon: 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.
Turn a labelled issue into a pull request
Section titled “Turn a labelled issue into a pull request”The label is the spec gate: nothing runs until a person decides the issue has acceptance criteria an agent can meet.
name: Issue to PRon: 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-actionrefuses to start for any actor without write or admin access unless you setallowed_non_write_users, so only a write-access labeller starts a run. That gate lives in the action (checkWritePermissions, checked onmainat 97c5347, 2026-10-01): replace the action with a plainclaude -pstep and you lose it. Theauthor_associationcondition skips issues not written by an owner, organisation member or collaborator, andCOLLABORATORincludes 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 …' xruns any command, and the action ships its ownscripts/git-push.shwrapper becausegit push --receive-pack=…does the same. Here Claude has no git and a token scoped tocontents: read. - The write token sits in another job.
open-prnever sees the API key and runs nothing Claude wrote: it applies the patch, refuses any change under.github/, then pushesclaude/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/configfor 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:
| Job | Holds | Executes the branch’s code? | Does |
|---|---|---|---|
patch | API key, read-only token | No | Claude reads the logs and edits a copy of the commit |
verify | Nothing: no secrets, read-only token | Yes | Applies the patch, rejects weakened tests, re-runs the gate |
open-pr | Write token | No | Commits the verified patch to a new branch and opens a PR |
name: Claude fixes CIon: 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" --fillFour 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).
# 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.
permissions: contents: write pull-requests: write issues: write id-token: write steps: - uses: actions/checkout@v7 with: persist-credentials: false - id: auth uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - uses: anthropics/claude-code-action@v1 with: use_vertex: 'true' claude_args: '--model claude-sonnet-5 --max-turns 10' env: ANTHROPIC_VERTEX_PROJECT_ID: ${{ steps.auth.outputs.project_id }} CLOUD_ML_REGION: us-east5The input is still called use_vertex, and region and project come from the CLOUD_ML_REGION and ANTHROPIC_VERTEX_PROJECT_ID environment variables, not from action inputs.
permissions: contents: write pull-requests: write issues: write id-token: write steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: anthropics/claude-code-action@v1 with: use_foundry: 'true' claude_args: '--model claude-sonnet-5 --max-turns 10' env: ANTHROPIC_FOUNDRY_RESOURCE: your-foundry-resourceUse a model ID that matches a Claude deployment in your Foundry resource.
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.
Keep Claude Code CI spend bounded
Section titled “Keep Claude Code CI spend bounded”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 == falseplus apaths: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-turnsin the action,--max-turnsplus--max-budget-usdin 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:
- 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.
- 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.
- Evidence in the PR body. Acceptance criteria mapped to tests, commands run, and open questions, following the evidence bundle.
- Structured verdicts. Headless jobs return
--json-schemaoutput, and the pipeline fails on a value rather than on a human reading prose. - 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.
When Claude Code in GitHub Actions breaks
Section titled “When Claude Code in GitHub Actions breaks”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.
Where to go next with Claude Code in CI
Section titled “Where to go next with Claude Code in CI”- Pipeline automation with AI: the cross-tool view of agents in the delivery pipeline
- Pipeline configuration using Claude Code: generate and speed up the pipeline these jobs run in
- Reviewing an agent’s pull request without reading every line: what the human reads after Claude’s review
- The evidence bundle: the PR contract the issue-to-PR prompt fills in
- Permissions and sandboxing: tool allowlists and sandboxes for unattended agents
- Hooks and automation: enforce the same rules locally before code reaches CI
- Headless agents in CI:
claude -p,codex execand the Agent SDKs compared as pipeline building blocks - The same jobs in the other tools: Codex in CI/CD and Cursor in CI/CD