Headless Claude Code: CI, Scripts, and Automation
Run the agent without the terminal UI: nightly fixes, PR reviews, batch jobs.
In the last episode, we saw how Claude Code remembers your project across sessions. Now we take the agent out of the interactive terminal and into scripts and CI pipelines.
Headless Claude Code runs the same agent that edits your code and runs tests, but without an interactive UI. A prompt, some configuration, and a set of permission rules replace the human. The agent calls tools, applies edits, and emits results as plain text or structured JSON for a script or CI job to consume. It is the same agent loop, the same project context and the same safety checks you use locally, driven by a program.
What changes is who answers the permission questions. On a CI runner nobody is there to click "Yes," so anything that would ask is denied instead, and you decide in advance what the agent may do: with allow rules, with a strict mode such as dontAsk, or with auto mode's classifier reviewing each action. That is what lets you automate PR reviews, nightly fixes and batch code changes without building a separate agent.
How does Claude Code run without a terminal UI?
The claude CLI (command-line interface) has a flag that skips the interactive session. That flag is -p, short for print mode. When you run claude -p "Explain this code", Claude Code reads the prompt, loads your project context, runs the agent to completion, prints the final answer to stdout, and exits with code 0 on success or a non-zero code on failure.
- Terminal UI you type into
- You can approve actions as they come
- Real time feedback
- No terminal UI
- Permissions decided in advance
- Output to stdout or a file
Unless you say otherwise, everything else is the same. A claude -p run discovers and loads your CLAUDE.md, your settings, the project's MCP (Model Context Protocol) servers, hooks, skills and auto memory, just as an interactive session would, according to the headless documentation. The only difference is that input comes from the -p argument and any piped stdin, and output goes to stdout instead of a chat panel.
You can pipe data straight into the agent. For example, cat build-error.txt | claude -p "Explain the root cause of this build error" feeds the log in as part of the prompt, just like a user pasting it (piped input is capped at 10 MB). This makes -p a good fit for scripts that need to analyze a specific file, log or patch.
The --output-format flag controls how the result is delivered. text prints the agent's plain response. json emits one object with the result text in a result field, plus the session ID, token usage and cost. stream-json emits newline-delimited JSON (JavaScript Object Notation) messages as the agent works, so you can follow progress in real time. These options let you plug Claude Code into any pipeline that can read JSON or plain text.
Why should CI runs use bare mode?
Loading everything from the working directory is convenient on your laptop and risky on a CI runner. Without --bare, a -p run executes the hooks in the repository's .claude/settings.json and connects the servers in its .mcp.json, "even in a folder you've never trusted," and it shows no trust dialog or approval prompt, because nobody is there to see one. On a pipeline that runs on pull requests from forks, that means a stranger's settings file could run commands on your runner.
The --bare flag fixes that. It skips the auto-discovery of hooks, skills, plugins, MCP servers, auto memory and CLAUDE.md, so every run starts from the same known state on every machine. The docs call it the recommended mode for scripted calls and say it will become the default for -p in a future release. Because it reads nothing implicitly, you pass what the run needs explicitly: --append-system-prompt-file for instructions, --settings for permission rules, --mcp-config for servers. Bare mode authenticates only with ANTHROPIC_API_KEY (or an apiKeyHelper in --settings), never with a subscription login:
How does the Agent SDK fit in?
The Agent SDK (software development kit) is the same engine packaged as a library. It gives your Python or TypeScript program the tools, agent loop and context management that power Claude Code; the SDKs are separate packages that bundle the Claude Code binary, as the Agent SDK overview explains. Use the CLI with -p for shell scripts and CI steps, and the SDK when the agent is part of a larger program.
Neither simply wraps the Messages API (application programming interface). Both run the full agent loop: building the system prompt, managing tool definitions, dispatching tool calls, enforcing permissions and keeping context across turns. You provide a prompt, a working directory and a few settings, and you get back the final result plus usage numbers.
Here is a simplified view of a headless run:
A denied call does not crash the run. Claude is told the call was denied and carries on, and with json or stream-json output the final result lists every refusal in a permission_denials field, so your pipeline can check what the agent tried and was not allowed to do.
The JSON output also reports what the run cost: token usage and total_cost_usd, with a per-model breakdown. Real agentic runs use more tokens than people expect, since Claude Code's own instructions and tool definitions come first, so read those fields from a real run before you budget, and cap a run with --max-turns or --max-budget-usd if you need a hard stop.
How do permissions work when there is no human to approve?
In interactive mode, the agent asks before it edits files, runs most shell commands, or makes network calls. In headless mode, anything that would ask is denied, so you decide in advance what is allowed. Claude Code enforces these rules in the runtime, not in the model.
Permissions are a mode plus rules. Rules allow, ask or deny specific tools, and they must name Claude Code's own tools exactly: Bash, Read, Edit, Write, Glob, Grep, WebFetch, WebSearch, Agent, and mcp__<server>__<tool> for MCP tools. A specifier narrows a rule: Bash(npm test *) allows any command starting with npm test, and WebFetch(domain:example.com) allows fetches from one domain. The mode sets the baseline. Claude Code has six: default (labelled Manual), acceptEdits, plan, auto, dontAsk and bypassPermissions, and a -p run with no mode set may start in default or auto depending on the session, so pass the one you want with --permission-mode. The two that suit CI best are dontAsk and auto.
In dontAsk mode, anything that is not explicitly allowed is denied without prompting; file reads in the working directory and the built-in read-only commands still run. This suits pipelines that need strict, predictable behavior, like a job that only reviews code and never changes it.
In auto mode, a separate classifier model reviews each action and blocks the ones it judges unsafe. You tell it what counts as trusted with autoMode.environment, a list of plain-language descriptions, not URL patterns. Keep the built-in entries by including the literal string "$defaults", or your list replaces them. And note where this setting is read: from your user settings, managed settings or the --settings flag, never from a project's .claude/settings.json, so a repository cannot widen its own trust. In CI, pass it with --settings:
Know what auto mode already allows. By default it trusts the repository you are working in and its remotes, and the auto mode configuration guide says it allows pushes to any branch of that repository, including the default branch. If pushes to main must never happen, say so with a rule. A deny rule such as Bash(git push origin main*) blocks matching commands in every mode, though it matches the command as written, so keep branch protection on the remote as the real guarantee.
You can also layer on PreToolUse hooks from Part 4. They run your own script before each tool call, so you can inspect arguments, log them, or ask an external policy service. For regulated environments, a hook gives you an audit trail of every action the agent attempted.
How can I use Claude Code in CI/CD pipelines?
The official route on GitHub is the Claude Code GitHub Action, anthropics/claude-code-action@v1. The quickest setup is to run /install-github-app in a Claude Code session inside your repository: it installs the Claude GitHub App, stores your credential as a repository secret (ANTHROPIC_API_KEY for an API key, or CLAUDE_CODE_OAUTH_TOKEN for a subscription token from claude setup-token), and opens a pull request with the workflow file. After that, anyone with access can mention @claude in an issue or pull request comment to put it to work. The GitHub Actions documentation covers GitLab CI/CD and cloud providers too.
For automation without a mention, give the action a prompt. Here is a minimal review workflow for our side project:
claude_args passes ordinary CLI flags to Claude Code, here a turn limit and one pre-approved command. With a prompt, results appear in the workflow log by default; Claude posts on the pull request only when the prompt asks it to and it has a tool that can post, which is what Anthropic's official code-review plugin provides.
You can also call the CLI yourself in any CI system. Asking for JSON in the prompt is fragile, so let Claude Code enforce the shape instead: pipe the diff in, pass a JSON Schema with --json-schema, and read the validated answer from the structured_output field.
You can also let the agent fix things. A nightly job might run claude -p "Fix all ESLint errors and open a pull request on a new branch" in dontAsk mode, with allow rules for the edits, npm run lint, and the git commands it needs, such as Bash(git push origin auto-fix/*). The key is to keep the agent's scope narrow: it works on its own branch and opens a pull request, and a human reviews the change before it merges.
What tools can Claude Code use in a headless environment?
The same tools as in an interactive session, under the same names: Bash for shell commands, Read, Edit and Write for files, WebFetch and WebSearch for the web, and mcp__<server>__<tool> for each MCP server's tools. Every one of them goes through the permission check.
Our side project's review job uses Bash to run npm run lint, npm test and read-only git commands. We also configured an MCP server that connects to our issue tracker, so the agent can look up the ticket a pull request mentions, all within the headless run.
MCP servers come from the .mcp.json file at the repository root, as Part 5 described. In bare mode, or whenever you want an exact set, pass them with --mcp-config. Once loaded, their tools appear to the agent under the same permission rules as everything else.
With the Agent SDK you can go further and define custom tools in your own code, for example one that asks your feature flag service whether a feature is on before Claude suggests a change. Custom tools are permission-checked like built-in ones, so a misconfigured tool cannot bypass your rules.
How does output streaming work for automation?
The stream-json format suits long-running tasks. Instead of waiting for the final answer, Claude Code writes one JSON message per line as the run progresses. The docs pair it with --verbose in print mode:
These are Claude Code's own message objects, not raw API events. The stream usually starts with a system message of subtype init that lists the model, tools and MCP servers. Then come assistant messages (Claude's text and tool calls) and user messages (the tool results), with occasional system messages such as api_retry when a request is retried. The last line is a result message with the final text, the cost and the session metadata. If you want token-by-token text as well, add --include-partial-messages, and the raw streaming events arrive wrapped as {"type": "stream_event", "event": ...}.
You can pipe these messages into a logging system, a dashboard or a monitoring tool. For our nightly job, a small Node.js script reads the stream and posts short progress updates to a Slack channel ("running the linter," "found 3 issues," "applying fixes"), which keeps the team informed without flooding the channel with raw logs. When the job only needs the answer, it reads the final result line.
Quick Reference
| Property | Value |
|---|---|
| Headless entry point | claude -p "<prompt>" |
| Recommended for CI | --bare (skips hooks, MCP servers, CLAUDE.md and more; needs ANTHROPIC_API_KEY) |
| Output formats | text, json, stream-json (with --verbose) |
| Structured output | --output-format json --json-schema '<schema>', read .structured_output |
| Permission modes | default (Manual), acceptEdits, plan, auto, dontAsk, bypassPermissions |
| Pre-approve tools | --allowedTools "Read,Edit,Bash(npm test *)" |
| Auto mode trust list | autoMode.environment with "$defaults", from user or managed settings or --settings |
| MCP config | .mcp.json at the repository root, or --mcp-config |
| Limits | --max-turns <N>, --max-budget-usd <amount> |
| GitHub Action | anthropics/claude-code-action@v1, set up with /install-github-app |
| Cost and denials | total_cost_usd, usage and permission_denials in the JSON result |
Frequently Asked Questions
Q: Can I run Claude Code in a Docker container without a display?
Yes. The CLI has no graphical dependency. Install it in the container, set the ANTHROPIC_API_KEY environment variable, and run claude -p, ideally with --bare. For enterprise setups, you can point it at Amazon Bedrock, Google Cloud's Agent Platform (formerly Vertex AI) or Microsoft Foundry instead.
Q: What permission mode is safest for a CI pipeline that only reads code?
dontAsk, with no allow rules beyond what the review needs. File reads in the working directory and the built-in read-only commands such as ls and git status still run; everything else, including edits and network access, is denied. If the job needs one more command, such as npm test, allow exactly that with Bash(npm test *).
Q: How do I configure auto mode to trust my organization's repositories?
Add plain-language entries to autoMode.environment, keeping "$defaults" in the list, for example "Source control: github.com/my-org and all repos under it". Put it in your user settings, your organization's managed settings, or pass it with --settings in CI; a project's .claude/settings.json is ignored for this setting.
Q: How can I prevent Claude Code from making network calls to external services?
Deny the WebFetch and WebSearch tools, deny network commands such as Bash(curl *) and Bash(wget *), and leave out MCP servers that reach external APIs. In dontAsk mode, anything not explicitly allowed is already denied. For a boundary at the operating system level, run the job in a container without outbound access.
Q: Can I run Claude Code on Amazon Bedrock or Google Cloud?
Yes. These are cloud providers that host Claude models, not self-hosted models. You select the provider with an environment variable, for example CLAUDE_CODE_USE_BEDROCK=1 for Amazon Bedrock with your AWS credentials, or CLAUDE_CODE_USE_VERTEX=1 for Google Cloud's Agent Platform with a Google Cloud account. The GitHub Action has matching inputs, and the headless flow is otherwise the same.
Test yourself
Your nightly CI job runs Claude Code in dontAsk mode to fix lint errors and push a branch. The run finishes, but the branch never appears on GitHub, and the JSON result lists the agent's git push under permission_denials. How do you fix this without switching to a fully permissive mode?
Answer: In dontAsk mode anything not explicitly allowed is denied, and the run carries on, which is why it finished without the push. Add an allow rule for exactly the push the job needs, scoped to its own branches, for example Bash(git push origin auto-fix/*), passed with --allowedTools or in a settings file. Keep dontAsk, so every other command still needs its own rule. Add a deny rule for Bash(git push origin main*) as a second line of defense, and remember that rules match the command as written, so branch protection on main in GitHub remains the real guarantee. Do not reach for auto mode to solve this: it allows pushes to any branch of the working repository by default, including main, unless you add rules.
Next: Claude Code + Your IDE: A Workflow That Compounds. It shows how to combine the terminal agent with your editor, so each does what it is best at.
If you want this kind of breakdown every week, how real systems actually work under the hood, subscribe to Internals Decoded at internalsdecoded.com.
Sources
- Run Claude Code programmatically
- CLI reference
- Agent SDK overview
- Configure permissions
- Choose a permission mode
- Configure auto mode
- Claude Code GitHub Actions
- Connect Claude Code to tools via MCP
- Claude Code on Amazon Bedrock
- Claude Code on Google Cloud's Agent Platform