ClaudeFolio

Claude Code in CI: headless mode, the Agent SDK, and GitHub Actions

Everything you do in the interactive terminal, Claude Code can also do unattended. Three ways to run it in a pipeline: the headless CLI (claude -p), the Agent SDK, and the official GitHub Action. Same engine, three entry points, one set of cost and permission rules to get right.

·8 min read
Claude Code in CI: headless mode, the Agent SDK, and GitHub Actions

The interactive terminal is where most people meet Claude Code, but it is not the only way to run it. The same engine that drives the chat loop also runs unattended: in a shell script, inside your own program, or as a step in a CI pipeline. There are three entry points worth knowing, and they sit on top of the same core. The headless CLI (claude -p) is the fastest way to script the tool. The Agent SDK embeds that same agent loop inside a TypeScript or Python program. The official GitHub Action wires it into pull requests and issues. Pick the one that matches where your automation lives, and the mental model you already have from interactive use carries over.

The thing to internalize before any of this is that running an agent unattended changes the stakes around two things: permissions and cost. Interactively, you are the safety check, where every risky tool call waits for you to approve it and you can watch the token meter. In CI nobody is watching, so the guardrails have to be configured in advance. The good news is that the same permission and cost controls exist across all three entry points, because they share one engine. Get them right once and the knowledge transfers.

Headless mode: claude -p

Headless mode is the interactive tool with the prompt loop replaced by a single shot. You pass a prompt, the agent works the task to completion using its tools, and it prints a result. The flag is -p (or --print):

claude -p "Summarize what changed in the last commit"

You can pipe input in, which is how most CI usage works because the interesting context is usually the output of another command:

git diff main...HEAD | claude -p "Review this diff and list any bugs as file:line followed by the issue"

For anything programmatic you want structured output rather than prose, which is what --output-format is for. The three values are text (the default, just the response), json (a single object with the result plus metadata), and stream-json (newline-delimited events as they happen, for live progress). The JSON object is the one worth knowing, because it carries the fields you need to act on a run: the result text, a session_id you can resume from, and the cost of the run in total_cost_usd. A typical scripted call pulls the result out with jq:

result=$(claude -p "Generate release notes from git log since the last tag" \
  --output-format json)
echo "$result" | jq -r '.result'
echo "$result" | jq -r '.total_cost_usd'

Sessions persist on disk per project, so a multi-step pipeline can keep context across calls. --continue resumes the most recent session in the working directory, and --resume <session-id> resumes a specific one by the id you captured from the JSON output. This lets you split a long job into stages without the second stage losing what the first one learned:

sid=$(claude -p "Read the failing test and explain the root cause" \
  --output-format json | jq -r '.session_id')
claude -p "Now write the fix" --resume "$sid"

Permissions in a pipeline

This is the part that bites people. In interactive use the tool asks before it runs a risky command. In -p mode there is nobody to ask, so an unconfigured run will refuse the actions it would normally prompt for, and the task stalls. You fix this by deciding in advance what the agent may do. The cleanest lever is --allowedTools, which takes the same permission-rule syntax as your settings file, so you can allow whole tools or scope a tool down to specific commands:

claude -p "Run the test suite and fix any failures" \
  --allowedTools "Read,Edit,Bash(npm test),Bash(npm run *)" \
  --max-turns 6

--disallowedTools is the inverse for carving exceptions out of a broad allow. --permission-mode sets the overall posture: acceptEdits auto-approves file edits while still gating other actions, and bypassPermissions approves everything. There is also --dangerously-skip-permissions, which is the blunt "approve everything" switch. The name is a warning, not a joke. It belongs only in a throwaway sandbox where the agent cannot touch anything you care about, never against a checkout with real credentials in the environment. The right default for CI is the narrowest --allowedTools list that lets the task finish, because least privilege is the whole game once no human is in the loop.

Auth and the other useful flags

Headless runs authenticate with the ANTHROPIC_API_KEY environment variable. If you route through a cloud provider instead, set CLAUDE_CODE_USE_BEDROCK=1 for Amazon Bedrock or CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI and supply that provider's normal credentials. A handful of other flags earn their place in CI: --max-turns caps the agentic loop so a confused run cannot spin forever and burn tokens; --model pins the model (for example claude-sonnet-4-6 for cheaper bulk work or claude-opus-4-8 for hard reasoning); --append-system-prompt adds a house instruction without replacing the default system prompt; --add-dir grants read access to a directory outside the working tree; and --verbose turns on the debug logging you will want the first time a workflow misbehaves.

The Agent SDK

When the automation is a program rather than a shell script, reach for the Agent SDK. It embeds the exact agent loop the CLI uses, so you get the same tools, the same permission model, and the same session handling, but driven from your own code with results you can branch on. One naming note that trips people up: this used to be called the "Claude Code SDK" and was renamed to the Claude Agent SDK. The packages are @anthropic-ai/claude-agent-sdk on npm and claude-agent-sdk on PyPI. If you find a tutorial referencing the old name, the concepts still apply; the package moved.

The core of both SDKs is a single query() function. You give it a prompt and an options object, and it returns an async stream of messages: the assistant's turns, the tool calls it makes, and a final result message carrying the cost. You iterate that stream the way the CLI prints turns. The TypeScript shape:

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Find and fix the failing test in this repo",
  options: {
    allowedTools: ["Read", "Edit", "Bash"],
    permissionMode: "acceptEdits",
    maxTurns: 8,
    model: "claude-sonnet-4-6",
  },
})) {
  if (message.type === "result") {
    console.log("done, cost was", message.total_cost_usd);
  }
}

The same thing in Python reads almost identically, which is deliberate:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    async for message in query(
        prompt="Find and fix the failing test in this repo",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Edit", "Bash"],
            permission_mode="acceptEdits",
            max_turns=8,
            model="claude-sonnet-4-6",
        ),
    ):
        print(message)

asyncio.run(main())

The options map onto the CLI flags you already know: allowedTools, permissionMode, maxTurns, model, a cwd for the working directory, mcpServers to connect MCP servers, and resume to pick up a prior session. Auth is the same ANTHROPIC_API_KEY environment variable. The decision between the SDK and the headless CLI comes down to where the logic lives: if your automation is mostly shell glue around one agent call, claude -p with jq is less ceremony; if you are fanning out over many inputs, branching on results, or embedding the agent inside a larger service, the SDK gives you real control flow instead of string parsing.

GitHub Actions

The most common place teams want Claude Code unattended is on their pull requests and issues, and that is what the official anthropics/claude-code-action covers. There are two ways it runs. In its default mode it watches for an @claude mention in an issue, a PR, or a comment, and responds to whatever you asked: implement the issue, fix the failing test, review the diff. In automation mode you give it a fixed prompt and a trigger of your choosing, which is how you build an unattended reviewer or a scheduled report.

The fastest setup path is to run /install-github-app from inside an interactive Claude Code session. It installs the GitHub App on your repo, adds the ANTHROPIC_API_KEY repository secret, and drops a starter workflow into .github/workflows/. If you would rather wire it by hand, install the app, add the secret under Settings then Secrets and variables then Actions, and add a workflow. A mention-triggered workflow looks like this:

name: Claude
on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]

jobs:
  claude:
    if: contains(github.event.comment.body, '@claude')
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          claude_args: "--max-turns 10 --model claude-sonnet-4-6"

Notice that the CLI flags you learned for headless mode pass straight through the claude_args input, because the action is running the same binary underneath. Automation mode swaps the mention trigger for a fixed prompt. A workflow that reviews every pull request as it opens:

name: Claude review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: "Review this pull request for correctness bugs and security issues. Comment inline on specific lines."
          claude_args: "--max-turns 8"

If your account does not call the Claude API directly, the action also runs against Amazon Bedrock or Google Vertex AI, and it supports OIDC-based authentication so you can avoid storing a long-lived key as a secret at all. Those setups are documented in the action's repo and the platform docs linked at the end. Whichever auth you pick, two costs are in play on every run: the GitHub Actions minutes the job consumes on the runner, and the Claude tokens the agent spends, which bill to whichever account owns the key or cloud credentials.

Cost and safety in CI

The discipline that keeps unattended runs from surprising you is the same three habits in every entry point. Scope the tools as tightly as the task allows, because an agent that can only read and run your test command cannot do much damage even if it goes off the rails. Cap the loop with --max-turns so a confused run ends instead of grinding, since each turn is real money. And capture total_cost_usd from the JSON output (or the result message in the SDK) and log it, so the cost of your automation is a number you watch rather than a line item you discover on the invoice. Treat --dangerously-skip-permissions and bypassPermissions as sandbox-only, reserved for environments where the agent has nothing valuable within reach. The interactive tool lets you be loose because you are the backstop. In CI the configuration is the backstop, so spend a few minutes making it the narrowest thing that still gets the job done.

Sources

Steps

  1. Run your first headless command

    Set ANTHROPIC_API_KEY in the environment, then run claude -p "your prompt". Add --output-format json and pipe to jq to pull out .result and .total_cost_usd. Pipe other commands in (git diff | claude -p ...) to feed context.

  2. Scope permissions before running unattended

    In -p mode there is no human to approve tool calls, so pass --allowedTools with the narrowest list that finishes the task, e.g. --allowedTools "Read,Edit,Bash(npm test)". Avoid --dangerously-skip-permissions outside a throwaway sandbox.

  3. Cap the loop and track cost

    Add --max-turns to stop a confused run from spinning, and read total_cost_usd from the JSON output to log spend per run. Both controls exist in the CLI, the SDK, and the GitHub Action.

  4. Embed the agent with the SDK when you need control flow

    Install @anthropic-ai/claude-agent-sdk (npm) or claude-agent-sdk (PyPI), then call query({ prompt, options }) and iterate the message stream. Options map to the CLI flags: allowedTools, permissionMode, maxTurns, model, cwd, mcpServers.

  5. Wire up GitHub Actions

    Run /install-github-app from an interactive session to install the app, add the ANTHROPIC_API_KEY secret, and scaffold a workflow. Use anthropics/claude-code-action@v1, pass CLI flags through claude_args, and trigger on @claude mentions or a fixed prompt for automation.

Frequently asked questions

What is the difference between the headless CLI, the Agent SDK, and the GitHub Action?
All three run the same Claude Code engine. The headless CLI (claude -p) is for shell scripts and pipelines. The Agent SDK embeds that engine in your own TypeScript or Python program when you need real control flow and to branch on results. The GitHub Action (anthropics/claude-code-action) wires it into issues and pull requests, triggered by @claude mentions or a fixed prompt. Permission and cost controls are the same across all three.
Why does Claude Code refuse to do anything in CI?
In interactive use the tool prompts you before risky tool calls. In headless mode there is no one to prompt, so unconfigured runs decline those actions and stall. Pass --allowedTools with the tools (and scoped Bash commands) the task needs, or set --permission-mode acceptEdits. Use the narrowest allowlist that lets the job finish.
How do I see what a headless run cost?
Run with --output-format json and read the total_cost_usd field from the output object (the same object also carries result and session_id). In the Agent SDK, read total_cost_usd off the final result message. Log it per run so automation spend is something you watch rather than discover on the invoice.
Was the SDK renamed?
Yes. The Claude Code SDK is now the Claude Agent SDK. The packages are @anthropic-ai/claude-agent-sdk on npm and claude-agent-sdk on PyPI. Older tutorials referencing the Claude Code SDK still apply conceptually; only the package name changed.
Do I need an Anthropic API key, or can I use Bedrock or Vertex?
All three entry points authenticate with ANTHROPIC_API_KEY by default. To route through a cloud provider, set CLAUDE_CODE_USE_BEDROCK=1 for Amazon Bedrock or CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI and supply that provider's credentials. The GitHub Action supports those plus OIDC-based auth so you can avoid storing a static key as a repo secret.
How do I keep a CI agent from running away?
Three habits: scope --allowedTools tightly so it can only do what the task needs, set --max-turns to bound the loop, and reserve --dangerously-skip-permissions and bypassPermissions for sandboxes where the agent has nothing valuable in reach. The configuration is your backstop in CI because no human is watching.

Built something with Claude Code?

ClaudeFolio is the directory of projects built with Claude Code. If this guide helped you ship something, the community would like to see it.

More guides