teachyou.ai academy
← All posts
Claude CodeCI/CDGitHub ActionsDevOpsautomation

Integrating Claude Code into CI Pipelines

Pramod Dutta · Jul 7, 2026 · 13 min read

Running Claude Code CI jobs turns a terminal coding assistant into a pipeline worker that reviews diffs, triages failing tests, and drafts release notes without a human sitting at the keyboard. The trick is getting Claude Code to behave in a non-interactive shell: no prompts waiting for approval, no tool calls that need a human to click "allow," and a token budget that does not blow up your CI bill. This guide walks through the actual flags, YAML, and guardrails that make that work, starting from a bare npm install -g @anthropic-ai/claude-code and ending with a PR review bot and a nightly test-triage job.

Why Run Claude Code in CI

Claude Code was built as an interactive CLI, but the same binary ships a "print mode" (-p / --print) that runs one prompt to completion and exits, which is exactly the shape a CI step needs. Once you have that, a few patterns become cheap to automate:

  • Automated first-pass code review on every pull request, commenting directly on the diff.
  • Explaining a failing test suite in plain language and attaching the explanation to the build log.
  • Drafting a changelog entry or release note from the commits between two tags.
  • Running a lightweight security or dependency check before merge.
  • Triaging flaky test failures and tagging them instead of paging a human at 2am.

None of this replaces your existing linters, unit tests, or static analysis. Claude Code CI jobs sit alongside those tools as a reasoning layer: they read code the way a senior engineer skims a diff, catching things pattern-based linters miss, like a race condition in async code or a migration that silently drops a column.

Installing and Authenticating Claude Code in CI

Every CI runner is ephemeral, so authentication has to be handled with an API key rather than an interactive OAuth login. Set ANTHROPIC_API_KEY as a masked secret in your CI provider's settings, never in the workflow file itself.

npm install -g @anthropic-ai/claude-code
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
claude --version

If your organization uses Claude through a cloud provider's model marketplace instead of a direct API key, Claude Code also reads provider-specific environment variables for that route. Check your provider's console for the exact variable names before wiring the workflow, since they differ from the direct API key flow.

Confirm the binary resolves and prints a version before you build anything on top of it. A silent install failure inside a workflow step is one of the most common causes of a mysterious "command not found" three steps later.

Running Claude Code in Non-Interactive (Print) Mode

The core flag for claude code ci usage is -p, which takes a prompt, runs it to completion, and exits instead of dropping into the REPL.

claude -p "Summarize the changes in this diff and flag any obvious bugs" \
  --output-format json

Key flags for CI:

  • -p "<prompt>" or --print: run once and exit, no interactive session.
  • --output-format json: get structured output you can parse in a later step instead of scraping stdout text.
  • --output-format stream-json: stream events as they happen, useful for long-running jobs where you want to log progress in real time.
  • --max-turns <n>: cap how many tool-use turns Claude Code can take before it must produce a final answer, which bounds both latency and cost.
  • --allowedTools and --disallowedTools: explicitly scope which tools Claude Code can call in this run.

A minimal test-triage script that reads a pytest failure log and writes a summary file:

claude -p "Read test-failures.log and write a short root-cause summary for each failing test to triage.md. Do not modify any source files." \
  --allowedTools "Read,Write" \
  --max-turns 6 \
  --output-format json > claude-run.json

cat triage.md

Piping the prompt from a file instead of an inline string keeps your workflow YAML readable once prompts grow past a couple of sentences:

claude -p "$(cat .claude-ci/review-prompt.txt)" --output-format json

GitHub Actions Workflow: PR Review Bot

This workflow runs on every pull request, checks out the diff, and asks Claude Code to review it, then posts the review as a PR comment using the GitHub CLI.

name: claude-code-review

on:
  pull_request:
    types: [opened, synchronize, reopened]

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: "20"

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Generate diff
        run: git diff origin/${{ github.base_ref }}...HEAD > pr.diff

      - name: Run Claude Code review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p "Review pr.diff for bugs, security issues, and missing tests. Output markdown with a short summary and a bulleted list of findings, each tagged [blocking] or [suggestion]." \
            --allowedTools "Read" \
            --max-turns 8 \
            --output-format text > review.md

      - name: Post review comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh pr comment ${{ github.event.pull_request.number }} --body-file review.md

A few details matter here. fetch-depth: 0 on checkout ensures the full git history is available so the diff against the base branch actually resolves. --allowedTools "Read" keeps this run read-only: Claude Code can inspect files but cannot edit anything or run arbitrary shell commands, which is the right default for a job that only needs to produce a comment. The GITHUB_TOKEN used for gh pr comment is the default token GitHub Actions injects, scoped to pull-requests: write in the permissions block above.

GitLab CI Example: Release Notes Generator

The same print-mode pattern works in GitLab CI. This job runs on tag pushes and drafts release notes from the commit range since the last tag.

generate-release-notes:
  stage: release
  image: node:20
  rules:
    - if: '$CI_COMMIT_TAG'
  script:
    - npm install -g @anthropic-ai/claude-code
    - git fetch --tags
    - PREV_TAG=$(git describe --tags --abbrev=0 HEAD^)
    - git log ${PREV_TAG}..HEAD --oneline > commits.txt
    - >
      claude -p "Read commits.txt and write release notes grouped under Features, Fixes, and Chores. Skip merge commits. Output to RELEASE_NOTES.md." 
      --allowedTools "Read,Write"
      --max-turns 5
      --output-format text
  artifacts:
    paths:
      - RELEASE_NOTES.md
  variables:
    ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY

Store ANTHROPIC_API_KEY as a masked, protected CI/CD variable in GitLab project settings rather than in the YAML. The rules block restricts this job to tag pipelines so it does not run on every commit, which keeps token spend proportional to actual releases.

Scoping Permissions with a settings.json

Passing --allowedTools on every command line works for simple jobs, but once you have several CI steps calling Claude Code it is cleaner to check in a project-level settings file that CI picks up automatically. A .claude/settings.json committed to the repo (or a CI-only variant referenced with --settings) lets you define the exact permission set once:

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Grep(**)",
      "Glob(**)"
    ],
    "deny": [
      "Bash(rm *)",
      "Bash(curl *)",
      "Write(.env*)",
      "Write(**/secrets/**)"
    ]
  }
}

Point a CI-specific config at this file explicitly rather than relying on whatever exists in the checkout, so a malicious or accidental change to .claude/settings.json in a fork's PR cannot loosen permissions for the review job that runs against it:

claude -p "Review this diff" --settings .claude/ci-settings.json

Avoid --dangerously-skip-permissions in CI unless the job runs inside a fully disposable, network-isolated sandbox with no access to secrets or write access to anything you care about. The whole point of running Claude Code non-interactively is that nobody is watching the terminal to catch a bad tool call before it executes, so the permission list is doing the job a human would normally do at the approval prompt.

Controlling Cost and Token Budget

CI runs multiply fast: every pull request, every push, every nightly job. A few habits keep claude code ci spend predictable:

  • Use --max-turns on every job. A review that should take three or four tool calls has no business being allowed twenty.
  • Prefer --allowedTools "Read" for read-only analysis jobs. Restricting tools also reduces how many turns Claude Code needs, since it cannot go exploring with Bash when it only has Read and Grep.
  • Trigger expensive jobs (full-repo audits, release note generation) on specific events like tag pushes or a manual workflow_dispatch, not on every commit.
  • Pass a trimmed diff or log file instead of the whole repository. git diff output is far cheaper to process than asking Claude Code to open every changed file itself.
  • Set a job-level timeout in your CI YAML (timeout-minutes in GitHub Actions, timeout in GitLab) as a hard backstop in case a run gets stuck retrying a tool call.
jobs:
  review:
    timeout-minutes: 10

Parsing Structured Output for Downstream Steps

--output-format json returns a structured payload with the final result plus metadata about turns used and tool calls made, which is far more reliable to parse than scraping free-text output. A downstream step can pull just the result field:

claude -p "Classify this failing test as flaky, regression, or environment issue. Respond with one word." \
  --output-format json \
  --max-turns 3 > result.json

CLASSIFICATION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('result.json','utf8')).result.trim())")
echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT"

That output variable can then gate a later step, for example only re-running a test three times if it was classified as flaky, or opening an issue automatically if it was classified as a regression.

Caching and Speeding Up Claude Code CI Steps

Installing Claude Code fresh on every job wastes time that adds up across a busy repository. Cache the global npm install the same way you would cache node_modules, keyed on the Node version and lockfile so a version bump invalidates the cache automatically.

- name: Cache Claude Code install
  uses: actions/cache@v4
  with:
    path: ~/.npm-global
    key: claude-code-${{ runner.os }}-${{ hashFiles('.claude-code-version') }}

- name: Install Claude Code
  run: |
    npm config set prefix ~/.npm-global
    npm install -g @anthropic-ai/claude-code
    echo "~/.npm-global/bin" >> "$GITHUB_PATH"

Pinning a version file like .claude-code-version and installing that exact version (npm install -g @anthropic-ai/claude-code@<version>) also keeps CI behavior stable between runs. An unpinned @latest install means a review job can behave differently on Tuesday than it did on Monday for no reason related to your code, which makes flaky CI failures harder to reason about. Bump the pin deliberately, on its own pull request, so a Claude Code version change is visible in the diff rather than silently rolling out.

For monorepos running several Claude Code jobs in the same pipeline (a review job, a test-triage job, a docs-check job), run them as parallel jobs rather than sequential steps in one job. Each gets its own timeout and its own token budget, and a stuck triage job does not block the review job from posting its comment.

Handling Secrets and Network Access

CI runners frequently have broader network and filesystem access than a laptop, which raises the stakes on tool scoping. Two habits matter beyond the permission settings already covered:

  • Never let a Claude Code CI job read .env files, credential directories, or CI secret files as part of its context. The deny list in your CI settings file should block Read on those paths just as strictly as it blocks Write, since a review job that accidentally echoes a secret into a PR comment is a leak regardless of intent.
  • If a job only needs to read the diff and write a markdown file, do not give it Bash access at all. --allowedTools "Read,Write" with Bash omitted removes an entire class of exfiltration risk, since the model literally cannot run curl or git push even if a crafted prompt in a malicious PR description tried to talk it into doing so.

Debugging Failed Claude Code CI Runs

When a Claude Code CI step fails or produces useless output, work through these checks in order:

  1. Confirm the API key secret is actually populated in that specific job. A common mistake is scoping a secret to one environment or branch protection rule and forgetting a new workflow needs it added explicitly.
  2. Re-run the exact same prompt locally with the same flags to see if it is a prompt problem or an environment problem. If it works locally and fails in CI, the difference is almost always file access, missing checkout depth, or a permission denial.
  3. Check --output-format json for a turns-used count near your --max-turns ceiling. If it is hitting the ceiling every time, the task needs either a higher limit or a narrower prompt.
  4. Look for permission denials in the run log. A denied tool call does not always fail the whole job loudly, it can just make Claude Code produce a degraded answer because it could not read a file it needed.
  5. Verify the prompt file, if you are piping one from disk, actually exists at the path the CI step expects. Relative paths behave differently depending on the working directory a given CI step starts in.

FAQ

Does Claude Code need an interactive terminal to run in CI? No. Print mode (-p or --print) runs a single prompt to completion and exits, which is the mode every CI integration should use. The interactive REPL is only for local development sessions.

How do I stop Claude Code from making unwanted changes during a CI review job? Scope the run with --allowedTools "Read" (and Grep/Glob if needed) so it cannot call Write, Edit, or Bash. Pair that with a .claude/settings.json deny list for anything you never want executed, like rm or curl, even in jobs that do have broader tool access.

Can I run Claude Code CI jobs on pull requests from forks safely? Treat fork PRs as untrusted input. Do not expose secrets like ANTHROPIC_API_KEY or a GITHUB_TOKEN with write scope to workflows triggered by pull_request from forks without review, since GitHub already restricts secret access on fork-triggered runs by default; keep that restriction in place rather than working around it, and use a read-only Claude Code review job that comments through a separate, more trusted workflow trigger if you need write access to post comments.

What is the difference between `--output-format text` and `--output-format json`? Text mode returns the final answer as plain text, good for writing directly to a markdown file like a review comment or release note. JSON mode wraps that same result with metadata (turns used, tool calls made, stop reason), which is what you want when a later CI step needs to parse the outcome programmatically instead of just displaying it.

How do I keep token costs from growing unbounded as the team adds more Claude Code CI jobs? Cap every job with --max-turns, restrict tools to the minimum a job actually needs, trigger heavy jobs on specific events instead of every push, and feed Claude Code a trimmed diff or log file rather than letting it explore the whole repository. Reviewing the turns-used field in JSON output over time will show which jobs are creeping toward their ceiling and need a narrower prompt.

Should CI jobs use the same Claude Code settings file as local development? Not necessarily. Local development settings are often more permissive since a human is present to approve risky actions. CI should point at a dedicated, more restrictive settings file passed explicitly with --settings, so a change to the default project settings cannot silently widen what an unattended pipeline job is allowed to do.