Claude Code in a CI/CD Pipeline: A Working Example

Claude Code in a CI/CD Pipeline: A Working Example

Running Claude Code in a CI/CD pipeline means a build server, not a person, starts Claude, gives it a task and reads the answer. That sounds fancy, but the core is one command and one exit code. In this guide you will build a small pull request review gate, run it for real, watch it block a risky change, and then watch it pass after the fixes.

This is Part 43 of 45 in our Claude Code Deep Dives, part of our Claude Code tutorials. In Part 42 we built an agent with the TypeScript SDK, and here we put Claude Code inside the automation that checks your code.

Tested with Claude Code v2.1.322 on November 13, 2026, on macOS. We ran the review step as a script in a throwaway Git repository. We did not run the GitHub workflow on GitHub itself, because that needs a hosted repository and a secret key, so that part follows Anthropic’s official docs.

QuestionShort answer
What is it?Claude Code running unattended as one step of a CI job
Two ways to do itThe Claude Code GitHub Action, or claude -p inside any CI system
How the job passes or failsClaude returns JSON, and a shell test turns the verdict into an exit code
Safety flags we used--permission-mode dontAsk, --allowedTools "Read", --max-turns 6, --max-budget-usd 0.50
What we ran for realA failing review (exit 1) and a passing review (exit 0)
Cost in our testAbout $0.08 to $0.18 per review of a 25-line diff

What Claude Code in CI/CD Means

Continuous integration, or CI, is the robot that runs checks every time someone opens or updates a pull request. Usually those checks are tests and linters. With Claude Code, you can add a check that reads the change and gives a judgment, like a quick first reviewer.

Anthropic’s docs describe two main routes. The first is the Claude Code GitHub Action, which responds to @claude mentions and can run your own prompts on GitHub events. The second is headless mode, where you call claude -p from any script, so it works in GitHub Actions, GitLab CI or a plain shell.

We cover the basics of headless mode in our headless guide, and the sign-in options in headless auth guide. This post puts those pieces into a pipeline.

Diagram of a CI pipeline where a pull request starts a job, Claude Code reviews read-only, and a JSON verdict becomes an exit code
A pull request starts the job, Claude reviews, and the exit code decides

Two Ways to Run Claude Code in CI

Before you write anything, choose the route that fits your team. The table compares them.

OptionBest forHow results appearWhere it runs
Claude Code GitHub ActionComments on pull requests and issuesA comment, or the workflow log in automation modeGitHub Actions
claude -p in a scriptA pass or fail gateJSON you parse, plus an exit codeAny CI system
Agent SDK in a scriptCustom tools and your own logicMessages your code handlesAny CI system

The action has two modes, according to the GitHub Actions documentation. In interactive mode, when the workflow gives no prompt, Claude waits for the trigger phrase @claude. In automation mode, when the workflow provides a prompt input, Claude runs without waiting for a mention.

Decision ladder choosing between the GitHub Action, claude -p with a JSON schema, and the Agent SDK
Start at the top and stop at the first option that fits your need

Step 1: Set Up a Safe Demo Repository

We made a tiny Python project with two branches. The main branch holds a working total_price function and a test. A feature/discounts branch adds a discount, an average function and a report function, and it hides several problems on purpose.

def total_price(items, tax_rate, discount=0):
    subtotal = sum(i["price"] * i["qty"] for i in items)
    total = subtotal * (1 + tax_rate) - discount
    return round(total, 2)

def average_price(items):
    return sum(i["price"] for i in items) / len(items)

def run_report(name):
    os.system("echo Report for " + name)

The branch also contains a fake payment key written straight into the source, PAYMENT_KEY = "sk_live_...". It is not a real key. We planted it so that the reviewer has a real security problem to find.

Terminal showing git log and a diff stat of one changed file, app.py, with 16 insertions and 2 deletions
The feature branch changes one file, and that diff is what the reviewer reads

Step 2: Write the Review Script

The heart of the pipeline is a small shell script named ci-review.sh. It saves the branch diff to a file, asks Claude to review it, and turns the answer into an exit code. Here is the full file.

#!/usr/bin/env bash
set -euo pipefail
git diff main...HEAD > pr.diff

SCHEMA='{"type":"object","properties":{"verdict":{"type":"string","enum":["pass","fail"]},"issues":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string"},"file":{"type":"string"},"problem":{"type":"string"}},"required":["severity","file","problem"]}}},"required":["verdict","issues"]}'

claude -p "Review the changes in pr.diff. Report real bugs and security problems only. Use verdict fail if any high severity issue exists." \
  --output-format json \
  --json-schema "$SCHEMA" \
  --permission-mode dontAsk \
  --allowedTools "Read" \
  --max-turns 6 \
  --max-budget-usd 0.50 < /dev/null > review.json

jq -r '.structured_output.issues[] | "\(.severity)\t\(.file)\t\(.problem)"' review.json
VERDICT=$(jq -r '.structured_output.verdict' review.json)
echo "verdict: $VERDICT"
[ "$VERDICT" = "pass" ]

The last line is the trick. A shell test returns exit code 0 when it is true and 1 when it is not, and CI systems treat any non-zero exit as a failed job. So the verdict is the gate.

Terminal showing the ci-review.sh script with the claude -p call and its flags highlighted
One headless call, a few safety flags, and a test that decides the exit code

What each flag does

  • -p runs one prompt and exits, which is what a CI job needs.
  • --output-format json returns structured JSON with the result, session ID and metadata, as the headless docs describe.
  • --json-schema asks for a fixed shape. Our run put the answer in a structured_output field.
  • --permission-mode dontAsk denies every call that would otherwise prompt, which the docs call useful for locked-down CI runs.
  • --allowedTools "Read" pre-approves reading files. Nothing else can run.
  • --max-turns 6 and --max-budget-usd 0.50 cap how long and how much one review can run.
  • < /dev/null tells Claude there is no piped input. Without it, our first test printed a warning that it waited three seconds for stdin.

If you want to understand permission modes better, read permission modes guide. In CI you almost never want a mode that can wait for a human, because nobody is there to answer.

Step 3: Run the Pipeline and Watch It Fail

Now run the script on the risky branch. We piped the output through cut only so each line fits in the screenshot.

./ci-review.sh; echo "exit code: $?"

Claude read the diff and returned four issues. Two were high severity, a hardcoded live payment key and a command injection in run_report. Two were medium, a discount that can push the total below zero and an average that crashes on an empty list.

The verdict was fail, and the script exited with code 1.

Terminal output listing two high and two medium issues, then verdict fail and exit code 1
Two high issues and two medium issues, then a failed job with exit code 1

In a real pipeline, that exit code 1 would turn the check red and block the merge until someone fixes the problems. The review ran in about ten seconds in our first test.

Step 4: Fix the Findings and Watch It Pass

We rewrote app.py by hand to address the findings. The key moved to an environment variable, the shell call became subprocess.run with a list of arguments, and the discount and average functions got guards. Then we committed and ran the same script again.

def run_report(name):
    subprocess.run(["echo", "Report for", name], check=True)

def average_price(items):
    count = sum(i["qty"] for i in items)
    if count == 0:
        return 0.0
    return sum(i["price"] * i["qty"] for i in items) / count

This time Claude returned only two low-severity notes, the verdict was pass, and the exit code was 0. The job would turn green.

Terminal output showing two low severity notes, verdict pass and exit code 0
After the fixes, only low notes remain and the job passes with exit code 0

Notice that the model’s wording is not identical between runs. Our passing run even listed a low note about how the discount interacts with tax. A review is a judgment, not a unit test, so keep your human reviewers in the loop.

Check the cost and turns

The JSON result carries useful numbers. We printed a few fields with jq, and the passing run used 3 turns and cost about $0.08. Our earlier failing test run used 3 turns and cost about $0.18.

Terminal showing jq output with subtype success, num_turns 3, total_cost_usd 0.0809901 and verdict pass
The result JSON includes turns, cost and the verdict

Those figures come from a 25-line diff on one Mac. A bigger pull request will use more tokens, so keep your caps in place. Our Claude Code costs explains how to track usage over time.

Step 5: Move It Into GitHub Actions

The script above is portable. To run it on GitHub, you install Claude Code on the runner and provide a key as a secret.

Anthropic also offers an action that does the setup for you, which is the simpler path on GitHub. This is the workflow we wrote from the docs, and its YAML parses cleanly, but we did not run it on GitHub.

name: Claude PR Review
on:
  pull_request:
    types: [opened, synchronize]
jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
      pull-requests: write
      id-token: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: "Review this pull request for bugs and security problems. Report only real issues."
          claude_args: |
            --max-turns 6
            --allowedTools "Read,Grep"
Terminal showing a GitHub Actions workflow file with the permissions block and the prompt line highlighted
The workflow gives the action a prompt, a turn limit and a short list of allowed tools

The lines that matter

  • on: pull_request starts the job when a pull request opens or gets new commits.
  • permissions gives the job only what it needs. The docs list id-token: write as required for the action’s default GitHub App authentication.
  • timeout-minutes: 10 is a workflow-level timeout. The docs recommend one, so a stuck job cannot run forever.
  • anthropic_api_key reads the key from a repository secret. Never paste a key into the file.
  • prompt switches the action into automation mode.
  • claude_args passes command line flags, such as --max-turns and --allowedTools.

The fastest way to install the app and secret is to run /install-github-app inside Claude Code, which the docs describe as the quick setup. If you want a longer walkthrough of that route, see our GitHub Actions setup.

Safety Rules for Unattended Runs

A CI job runs without you watching, so treat it like a new teammate with a narrow job. The official docs and our run suggest these rules.

  • Store keys as GitHub Secrets. The docs warn never to commit API keys or OAuth tokens to your repository.
  • Give the job the least permissions it needs, and review any changes Claude makes before merging.
  • Allow only the tools the task needs. A reviewer needs Read, not Bash.
  • Cap turns, budget and time. The docs suggest --max-turns, workflow timeouts and GitHub concurrency controls.
  • Know who can trigger runs. On issue and pull request events the triggering user needs write access, and the action rejects bot actors unless you list them in allowed_bots.
  • Keep your CLAUDE.md short, because Claude reads it on every run.

The docs also note that runs use GitHub Actions minutes and API tokens, so both bills grow with usage. On public repositories, GitHub withholds secrets from runs triggered by fork pull requests, so the review works only for branches in the same repository.

Real Examples You Can Copy

Fail the build only on high severity

Our script fails whenever the verdict is fail. If you would rather count issues yourself, use jq to check severity directly.

jq -e '[.structured_output.issues[] | select(.severity == "high")] | length == 0' review.json

The -e flag makes jq exit non-zero when the result is false, so the same gate works in one line.

Run on a schedule

The docs show a scheduled workflow that writes a daily report to the run log at 09:00 UTC. GitHub runs scheduled workflows only from the default branch, and in public repositories it disables the schedule after 60 days without repository activity. If you want a recurring task that does not need GitHub, compare options in routines versus loop.

Run a skill in the pipeline

The prompt input accepts a skill invocation as well as plain text. For a skill in your repository’s .claude/skills/ folder, run actions/checkout first, then pass /skill-name as the prompt. Our code review skill shows how to build one.

Try It Yourself

Make a throwaway Git repository, add a small file with one obvious bug on a feature branch, and run the review script. Then ask Claude to write the script for you with this prompt.

Write a shell script named ci-review.sh that saves git diff main...HEAD to pr.diff, runs claude -p to review it with a JSON schema that returns a verdict and a list of issues, allows only the Read tool, caps turns and budget, and exits 1 when the verdict is fail.

Treat the review as a gate with limits, and let people keep the final say.

Common Mistakes

  • Letting Claude use shell tools in CI when it only needs to read files.
  • Forgetting --max-turns and a workflow timeout, which lets a stuck run burn minutes.
  • Trusting the verdict without human review. Our two runs gave different low-level notes on similar code.
  • Passing github_token: ${{ secrets.GITHUB_TOKEN }} to the action. The docs say GitHub does not trigger workflows on commits made with the default token, so remove it unless you need it.
  • Writing @claude-bot or /claude in a comment. The trigger must be the whole word @claude.
  • Putting keys in the workflow file instead of Secrets.
  • Skipping the local test. Run your script on your own machine first, as we did, before you wire it into CI.

Pro Tips

  • Print total_cost_usd and num_turns into the job log, so you notice a review that suddenly costs more.
  • Use --json-schema so you parse fields, not free text.
  • Add your review rules to CLAUDE.md, such as “flag any hardcoded secret”.
  • Use issue templates, since the docs say specific requests need fewer turns.
  • Pair the review with your normal tests. Claude reads code, and your test suite runs it.
  • For a local version before you push, try the /code-review command, and add hooks for checks on your own machine.

Cheat Sheet

Cheat sheet card with headless flags, safety flags, GitHub Action inputs and habits for running Claude Code in CI
The flags and habits behind a safe pipeline step on one card

Frequently Asked Questions

Can Claude Code run in any CI system?

Yes, if it can run the command line. The headless docs say you can run Claude Code programmatically from the CLI, Python or TypeScript, and they link a separate guide for GitLab CI/CD. The GitHub Action is only for GitHub.

How does a CI job authenticate?

The action docs list ANTHROPIC_API_KEY for a Console key, or CLAUDE_CODE_OAUTH_TOKEN for a Claude subscription token made with claude setup-token. Our local demo used an existing sign-in on one Mac, and we did not create a token.

Will Claude change my code during the review?

Not in our script. We allowed only the Read tool and used dontAsk, so any write would be denied. If you want Claude to push fixes, grant tools on purpose and review the result.

How much does a review cost?

Our two test reviews of a 25-line diff cost about $0.08 and $0.18. Your cost depends on diff size, repository size and prompt length, so set --max-budget-usd and watch the logs.

What happens when no one is there to approve a permission?

With dontAsk, Claude Code denies every call that would otherwise prompt. Tools you list in --allowedTools still run, so the job never hangs waiting for an answer.

Conclusion

Claude Code in a CI/CD pipeline comes down to a headless call, a JSON verdict and an exit code. In our test, a read-only review blocked a branch with a leaked key and a command injection, then passed after the fixes, at a cost of cents per run. Start with the script on your own machine, add limits for tools, turns and budget, store keys as secrets, and move to the GitHub Action when you want comments on pull requests. You can find more guides in our Claude Code tutorials and read about the model behind it on our Claude AI page.

Previous in the series: Part 42: Claude Agent SDK in TypeScript: Build a Small Agent

Next in the series: Part 44: Claude Code Worktrees: A Daily Workflow That Works.

Newsletter

New AI tools in your inbox

The best new tools and the AI news that matters, in one short email. Unsubscribe any time.

Free · No spam · Unsubscribe any time

✓ Looks like you’re subscribed

Loading Google’s sign-up form…

Looks like you’re already subscribed

Thanks for reading! New AI tools and the AI news that matters will land in your inbox.

Not subscribed yet? Refresh the page to load the sign-up form.

The sign-up form couldn’t load. If you use an ad or content blocker, allow news.google.com and reload the page.

Free instant check before you pay. Takes a few seconds.

Scroll to Top