Atlas
TechnicalHow-to

Claude Code Headless in CI: -p Mode and Safe Permissions

Run Claude Code headless in CI with claude -p: bare mode, dontAsk and allowedTools permissions, JSON output, exit codes and cost caps for unattended jobs.

The short version

  • To run Claude Code with nobody watching, use claude -p with a prompt and set the permissions yourself before the run starts.
  • Add --bare so the run skips hooks, MCP servers, plugins and CLAUDE.md found on the runner. Without it, a -p run executes a checked-out repo's hooks with no trust prompt.
  • Set the permission mode explicitly, since the built-in starting mode can be auto. Start with dontAsk and allow only the tools the job needs, using prefix rules such as Bash(git diff *).
  • Request --output-format json and check the exit code. A failure inside the run, such as missing authentication, is printed on stdout as the result.
  • Cap spend with --max-budget-usd (an estimate that can be exceeded) and set a CI job timeout. Pin the Claude Code version, because several behaviours need v2.1.259 or v2.1.286.
On this page
  1. The short version
  2. How Claude Code non-interactive mode works
  3. Use bare mode so runs are reproducible
  4. Safe permissions for unattended runs
  5. Get output your pipeline can parse
  6. Keep runs bounded
  7. Multi-step jobs: resume by session ID
  8. A checklist for headless CI jobs
  9. Summary
  10. Questions people ask

To run Claude Code headless in CI, call claude -p "<prompt>" and set the permissions yourself. Add --bare so the run ignores any hooks, MCP servers and CLAUDE.md it finds on the runner. Choose a permission mode such as dontAsk, and use --allowedTools to list only the tools the job needs. Request --output-format json so your pipeline can parse the result, and branch on the exit code.

This guide covers each of those settings and the problems that appear when nobody is around to answer a permission prompt. The flags come from Anthropic's documentation on running Claude Code programmatically and from its CLI reference. For Codex, including approval modes and sandboxing, see our Codex CLI setup guide.

How Claude Code non-interactive mode works

Add -p (or --print) to a claude command to run it non-interactively. Claude Code takes the prompt, works on it, prints the result and exits:

bash
claude -p "What does the auth module do?"

Three behaviours matter in CI:

  • Exit codes. Claude Code exits with 0 on success and with a non-zero code when the run fails, so your script can branch on the status. An invalid flag produces an error on stderr before the run starts. A failure during the run, such as missing authentication, is printed as the result on stdout. Text on stdout doesn't mean the run succeeded, so check the exit code.
  • Stdin works. You can pipe data in and redirect the answer out. Piped stdin is capped at 10MB. Past that limit, Claude Code exits with an error and a non-zero status. For larger inputs, write the content to a file and give its path in the prompt.
  • Some commands are unavailable. Skills and custom commands still work if you put /skill-name in the prompt. Built-in commands that only run in the terminal interface, such as /login, don't.

Piping also cuts down the permissions a job needs. The docs show a package.json script that pipes a diff into Claude, so Claude doesn't need Bash permission to read it:

json
{
  "scripts": {
    "lint:claude": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
  }
}

If your pipeline can collect the data itself, pass the data to Claude rather than giving Claude a tool to fetch it.

Use bare mode so runs are reproducible

Without extra flags, claude -p finds and loads the same things an interactive session would: hooks, skills, custom commands, subagents, installed plugins, MCP servers, auto memory and CLAUDE.md, from both the working directory and ~/.claude.

This matters most in CI. According to the documentation, a -p session without --bare runs the hooks in a project's .claude/settings.json and connects the servers in its .mcp.json, even in a folder you have never trusted. It shows no workspace trust dialog and asks for no approval per server. So if your pipeline checks out code you don't control, such as a pull request from a fork, that repository's configuration runs on your runner.

--bare skips all of that loading:

bash
claude --bare -p "Summarize README.md" --allowedTools "Read"

There is one exception. For a directory you add with --add-dir, bare mode still loads skills from its .claude/skills/ folder, but it skips that directory's .claude/commands/ and .claude/agents/.

In bare mode, Claude has the Bash, file read and file edit tools. Any other context has to be passed with a flag:

To loadUse
System prompt additions--append-system-prompt, --append-system-prompt-file
Settings--settings <file-or-json>
MCP servers--mcp-config <file-or-json>
Custom agents--agents <file-or-json>
A plugin--plugin-dir <path>, --plugin-url <url>

Bare mode also changes how the session behaves:

  • Only MCP servers given on the command line connect.
  • Background tasks don't run, so a command that reaches its timeout stops instead of moving to the background.
  • Claude doesn't get the system reminders that normally come with tool results. For example, it isn't told when a file it read earlier has changed on disk.

Before v2.1.286, these limits were only partly in place.

Anthropic recommends --bare for scripted and SDK calls and says it will become the default for -p in a future release. Setting it now means your jobs won't change behaviour when that happens.

Authentication in bare mode

Bare mode never reads OAuth credentials or the system keychain, so a subscription login on the runner won't work. For the Anthropic API, set ANTHROPIC_API_KEY in the environment to a key created in the Claude Console, or supply an apiKeyHelper in the --settings JSON. Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry still read their own provider credentials. Keep the key in your CI system's secret store, not in the workflow file.

Safe permissions for unattended runs

In a headless run, nobody can answer a prompt. Decide in advance what is allowed, and make sure everything else is denied. Otherwise a run can hang, or be approved by a mode you didn't choose.

Always set a permission mode

If nothing sets a permission mode, the run starts in Claude Code's built-in starting mode, which can be auto. Pass the mode you want explicitly. The headless docs describe three options:

  • dontAsk denies every call that would otherwise prompt, and the docs suggest it for locked-down CI runs. Actions that need no approval in Manual mode still run, such as file reads in your working directories and the read-only command set. So do actions covered by your --allowedTools entries or permissions.allow rules. Some things are denied even when an allow rule matches, including AskUserQuestion and reads from network paths.
  • acceptEdits lets Claude write files without prompting and auto-approves common filesystem commands such as mkdir, touch, mv and cp. Other shell commands (apart from read-only ones) and network requests still need an allow rule.
  • auto has a classifier review most actions in place of a person.
bash
claude -p "Apply the lint fixes" --permission-mode acceptEdits

For most CI jobs, start with dontAsk and add allow rules one at a time. Use acceptEdits for jobs whose purpose is to change files, such as applying formatter or lint fixes on a branch.

Scope tools with allowedTools

--allowedTools approves specific tools ahead of time:

bash
claude -p "Run the test suite and fix any failures" \
  --allowedTools "Bash,Read,Edit"

A bare Bash entry approves shell commands without asking, which is usually more than a CI job needs. The flag uses Claude Code's permission rule syntax, so you can approve command prefixes instead:

bash
claude -p "Look at my staged changes and create an appropriate commit" \
  --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"

The space before * matters. Bash(git diff *) matches commands that start with git diff. Without the space, Bash(git diff*) would also match git diff-index.

If a run starts in auto mode, Claude Code drops a bare Bash entry as a broad allow rule, and the classifier checks each command instead.

Turn prompts off completely

--permission-prompts none is meant for runs where nobody can answer. It needs Claude Code v2.1.259 or later, and earlier versions reject it as an unknown option. Permission rules, PermissionRequest hooks and the permission mode still decide each call first. Anything left that would prompt is denied, Claude is told that nobody can approve it and that it shouldn't retry, and the run continues. Tools that need a person's answer, such as AskUserQuestion, are removed:

bash
claude -p "Update the dependency pins and run the tests" --permission-mode auto --permission-prompts none

In a plain -p run with no permission host, these requests are denied anyway, and the flag adds the instruction not to retry. The flag matters most in runs that do have a host, meaning an Agent SDK canUseTool callback or a --permission-prompt-tool. Without the flag, the run waits for that host to answer each request.

With stream-json output, each denial appears as a permission_denied system message, and the final result lists all of them in permission_denials. If you save that list as a CI artifact, you can see which rule to add or which request needs a closer look.

Get output your pipeline can parse

--output-format accepts text (the default), json or stream-json. The json format returns the result along with the session ID and other metadata. To get a fixed shape, add --json-schema. The answer then appears in structured_output:

bash
claude -p "Extract function names from auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output'

If the schema is invalid, claude exits with an error. The format keyword (for example "format": "email") is accepted as an annotation but not enforced, so validate those fields yourself.

The JSON payload also includes total_cost_usd and a cost breakdown per model. Anthropic describes these as client-side estimates that can differ from your actual bill. Use them to track spending, not as billing data.

A review job can add a role prompt and still keep the default system prompt:

bash
gh pr diff "$1" | claude -p \
  --append-system-prompt "You are a security engineer. Review for vulnerabilities." \
  --output-format json

Fail the build when a server or plugin didn't load

When you pass --mcp-config, Claude Code checks each entry at startup and skips invalid ones, such as a url entry with no type. The run then continues and exits cleanly. In a terminal you would see a warning on stderr. When a CI runner captures or redirects stderr, though, no warning is printed.

The skipped entries are then reported only in the system/init event, which you can read from stream-json output:

  • mcp_server_errors lists skipped servers (v2.1.219 or later).
  • plugin_errors lists plugins that failed to load.

Both keys are left out when there are no errors, so fail the job if either one is present and non-empty. Otherwise a review step can pass without the tool it depends on.

Keep runs bounded

CI jobs need limits on time and cost. The docs describe these controls:

  • Background work. After its turn, a -p run can stay open while background work that Claude started finishes. By default, Claude Code stops whatever is still running after 10 minutes of continuous idle waiting and drops its partial result. Some work is exempt. For example, a background command started by the main conversation keeps the run open until the command exits or hits its own time limit. CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS changes the 10-minute cap, and setting it to 0 removes the cap. In bare mode, no background tasks run.
  • Budget. --max-budget-usd stops the run once estimated spend reaches the amount you set. It checks against the client-side estimate, and spend can go past the cap, so leave headroom. Once the cap is reached, Claude Code also stops any remaining background work instead of waiting for it. --max-turns limits the number of agentic turns and exits with an error when the limit is reached.
  • Cancellation. If your runner sends SIGTERM, Claude Code exits with code 143. The current turn is left unfinished with no result, Bash process trees are terminated, and SessionEnd hooks run. If you want the turn to end properly, send SIGINT first.

Multi-step jobs: resume by session ID

Some jobs work better in stages, such as reviewing first and summarising afterwards. Capture the session ID, then resume that session explicitly:

bash
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"

Use --resume with an ID rather than --continue. --continue loads the most recent conversation in the current directory, which may not be the one you want when several jobs share a runner. After a resume, the reported cost covers the whole conversation, earlier runs included. Those earlier totals don't count toward --max-budget-usd.

A checklist for headless CI jobs

  1. Use claude --bare -p, and load prompts, settings and MCP servers only through explicit flags.
  2. Authenticate with ANTHROPIC_API_KEY (or your cloud provider's credentials) from the CI secret store.
  3. Set --permission-mode yourself, starting with dontAsk.
  4. In --allowedTools, use prefix rules like Bash(git diff *) instead of a bare Bash.
  5. Pipe data in where you can, so Claude needs fewer tools.
  6. Use --output-format json, add --json-schema when later steps depend on the output's shape, and check the exit code.
  7. Fail the job on non-empty mcp_server_errors or plugin_errors, and save permission_denials.
  8. Set --max-budget-usd with headroom, plus a job timeout in your CI system.
  9. Pin the Claude Code version on your runners, for example with claude install <version>.

The same principles apply when you run many agents instead of one. Atlas runs each engineer as its own Claude Code session with a written role and its own working folder. It leaves production go-lives, spending, deleting data and sending anything outside to a person.

Summary

Running Claude Code headless comes down to two things. Make each run independent of the machine it lands on, and make permissions an explicit allowlist instead of relying on a default. --bare, an explicit --permission-mode, narrow --allowedTools rules and JSON output cover most of that. The other checks catch failures that would otherwise go unnoticed: the exit code, mcp_server_errors and permission_denials.

Several behaviours described here depend on recent versions, including --permission-prompts (v2.1.259) and the full bare-mode limits (v2.1.286). Pin your version, and reread the programmatic usage docs whenever you upgrade.

Questions people ask

How do I run Claude Code in CI without a person present?

Call claude -p with your prompt. This runs Claude Code non-interactively, and it exits when it's done. Add --bare, set a permission mode such as dontAsk, list the tools the job needs with --allowedTools, request --output-format json and branch on the exit code.

What does the --bare flag do in Claude Code?

It stops Claude Code from loading hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory and CLAUDE.md from the machine, so the run behaves the same everywhere. Without it, a -p run executes a repository's hooks and connects its MCP servers with no trust prompt, even when the repository is a pull request from a fork.

Which permission mode should I use for a CI job?

Set one explicitly, because the built-in starting mode can be auto. For most jobs, start with dontAsk, which denies every call that would otherwise prompt, and add allow rules one at a time. acceptEdits suits jobs whose purpose is to change files, such as applying lint fixes.

How do I authenticate Claude Code in bare mode?

Bare mode never reads OAuth credentials or the system keychain, so a subscription login won't work. Set ANTHROPIC_API_KEY from your CI secret store, or supply an apiKeyHelper in the --settings JSON. Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry still read their own provider credentials.

How can I limit cost and runtime for a headless Claude Code run?

--max-budget-usd stops the run once estimated spend reaches the amount you set. Leave headroom, because spend can go past the cap. --max-turns limits agentic turns and exits with an error when the limit is reached. Also set a job timeout in your CI system.

Sources

  1. Run Claude Code programmatically - Claude Code Docs
  2. CLI reference - Claude Code Docs
  3. Choose a permission mode - Claude Code Docs

Researched and drafted with Claude, then checked against the sources above. Atlas is an independent product, not affiliated with Anthropic or OpenAI. Claude and Claude Code are trademarks of Anthropic; Codex is a product of OpenAI.