Blog · October 4, 2026 · 10 min read · by Vilva Athiban P B
Claude Code Headless Mode: Running claude -p in Scripts and CI, and the Three Signals That It Finished
Most of what we write about on this site assumes you are sitting in front of an interactive Claude Code session, waiting for it to finish. Headless mode is the other way to run it: claude -p takes a prompt, does the work, prints the result and exits, which is what you want in a shell script, a cron job, a pre-commit hook or a CI pipeline. It is also the mode where "is it done yet?" stops being a question for your eyes and becomes a question for your tooling.
This guide covers the flags that matter in 2026, the output formats, how permissions work when nobody is there to answer a prompt, how to chain runs, and the three signals (exit code, result event, hook) that tell you a headless run has finished. Everything here is taken from the current Claude Code documentation for the -p CLI path; the Python and TypeScript Agent SDKs expose the same engine with native objects, and we point to them where they are the better fit.
The basics: -p, stdin and exit codes
Add -p (or --print) to any claude command and it runs non-interactively:
claude -p "What does the auth module do?"
# stdin is read too, so you can pipe like any Unix tool
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txtClaude Code exits with code 0 on success and non-zero when the run fails, so scripts can branch on $?. Two details trip people up. An invalid flag is reported on stderr before anything starts, but a failure inside the run, such as missing authentication, is printed as the result on stdout, so do not assume stdout is always a successful answer. And piped stdin is capped at 10 MB; above that the process exits with a clear error and a non-zero status. For bigger inputs, write the content to a file and name the path in the prompt.
If a supervisor stops the run with SIGTERM, Claude Code exits with code 143, kills the process tree of any running Bash command, runs only its SessionEnd hooks, and leaves the in-progress turn unfinished. Send SIGINT instead if you want the turn ended cleanly.
Use --bare in scripts and CI
By default claude -p loads the same context an interactive session would: hooks, skills, custom commands, subagents, installed plugins, MCP servers, auto memory and CLAUDE.md, from both the project and ~/.claude. That is convenient on your own machine and a reproducibility problem everywhere else. A teammate's hook or a project .mcp.json will run in a -p session with no trust dialog and no per-server approval prompt.
--bare skips all of that discovery. Claude gets Bash, file read and file edit, plus whatever you pass explicitly: --append-system-prompt, --settings, --mcp-config, --agents, --plugin-dir. It starts faster, behaves the same on every machine, and the docs say it will become the default for -p in a future release. Two consequences to plan for: bare mode never reads OAuth credentials or the keychain, so set ANTHROPIC_API_KEY (or an apiKeyHelper in the settings JSON) rather than relying on your subscription login; and no background tasks run, so a command that hits its timeout stops instead of moving to the background.
claude --bare -p "Summarize README.md" --allowedTools "Read"Permissions when nobody can click Allow
A headless run has no one to answer a permission prompt. There are three layers for deciding what it may do, and you will usually combine two of them.
--allowedToolspre-approves specific tools using the same rule syntax assettings.json."Read,Edit"lets Claude read and edit without asking;"Bash(git diff *)"allows any command starting withgit diff. The space before the*matters: without it,Bash(git diff*)would also matchgit diff-index.--permission-modesets the baseline.acceptEditswrites files and runs common filesystem commands without prompting.dontAskdenies everything that would otherwise prompt, which is the right default for a locked-down CI job.autohas a classifier review most actions; note that in auto mode a bareBashentry in--allowedToolsis dropped and each command is evaluated instead. Pass the mode you want explicitly, because the built-in starting mode can itself beauto.--permission-prompts none(v2.1.259 or later) tells the run not to wait on any permission host at all. Anything unresolved is denied, Claude is told nobody can approve it and not to retry, tools that need a human such asAskUserQuestionare removed, and the final result lists the denials inpermission_denials. This is the flag for scheduled jobs.
# unattended, classifier-reviewed, never blocks on a prompt
claude -p "Update the dependency pins and run the tests" \
--permission-mode auto --permission-prompts none
# review staged changes and commit, with narrowly scoped Bash rules
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 *)"If you already use auto mode interactively, the same classifier is what --permission-mode auto gives you headless; the difference is only what happens to the small set of actions it would have escalated to you.
Structured output: json, stream-json and schemas
--output-format takes text (default), json (one object with the result, session ID and metadata) or stream-json (newline-delimited events as they happen). The JSON payload includes total_cost_usd and a per-model breakdown, which is the simplest way to track spend in scripts; when you continue a conversation the figure covers the whole conversation, earlier runs included, and it is a client-side estimate rather than your bill.
claude -p "Summarize this project" --output-format json | jq -r '.result'
# force a shape with a JSON Schema; the answer lands in .structured_output
claude -p "Extract the main function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'An invalid schema now fails fast with Error: --json-schema is not a valid JSON Schema; before v2.1.205 it was silently ignored. The format keyword is accepted but treated as an annotation, not enforced.
For streaming, add --verbose --include-partial-messages to stream-json and filter the text deltas:
claude -p "Explain recursion" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'The stream is also where the operational signals live. The first event is normally system/init, which lists the model, tools, MCP servers (with status) and plugins, plus plugin_errors and mcp_server_errors arrays that are omitted when empty, so a CI gate can fail the job on a non-empty array. system/api_retry events report retries with the attempt number, delay and error category. Subagent messages carry a parent_tool_use_id so you can rebuild the nesting tree. The last line is always a result message.
Chaining runs
claude -p "Review this codebase for performance issues"
claude -p "Now focus on the database queries" --continue
claude -p "Generate a summary of all issues found" --continue
# or pin a specific session
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"Since v2.1.223 the session is found by ID across any project on the machine, so the two commands can run from different directories. --resume also accepts the absolute path of a session's .jsonl transcript. Skills and custom commands work by putting /skill-name in the prompt string; terminal-only commands such as /login do not, and /model, /effort and friends take their value as an argument.
Knowing when a headless run is done
Interactively you watch the terminal, or let a notification watch it for you. Headless, you have three cleaner signals, and which one you use depends on who needs to know.
- The exit code. For a script, this is it. Chain the next step with
&&, or capture$?and branch. The process does not return until the final result is produced, with two caveats below. - The
resultevent. Withstream-json, a long-running caller (a dashboard, a bot, an orchestrator) can treat the result line as the completion event and the preceding events as progress. It carries the final text, cost and session metadata. - A
Stophook. Hooks still fire in-pmode unless you used--bare, so the same Stop hook that pings you interactively will ping you from a cron job. This is the signal to use when a human, not a script, is the consumer: a long refactor kicked off from a laptop lid-closed, a nightly job that should wake someone only when it fails.
The caveats concern background work. If Claude started a background Bash task during the run (a dev server, a watch build), that shell is terminated about five seconds after the result is returned and stdin closes. If it started a background subagent or workflow, claude -p stays open until that finishes, because its output is part of the result, up to ten minutes of continuous idle waiting by default; CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS changes the ceiling, and 0 removes it. A Monitor watch is waited on until it times out or the ceiling ends the wait. So "done" for a -p run means "the result is final and the process has exited", which is a stronger guarantee than an interactive session's idle prompt, and it is why the exit code is trustworthy. The environment variables guide lists the other knobs that affect headless runs.
Three patterns worth copying
A project linter. Pipe a diff so Claude needs no Bash permission at all:
{
"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.\""
}
}A PR security pass. Keep Claude Code's default behaviour and add a role with --append-system-prompt:
gh pr diff "$1" | claude -p \
--append-system-prompt "You are a security engineer. Review for vulnerabilities." \
--output-format jsonA scheduled job. Combine --bare, an API key in the environment, --permission-prompts none, a narrow --allowedTools list and json output, then have the wrapper script decide whether the result deserves a human. If you would rather Claude Code own the schedule, the /loop and scheduled tasks guide covers that path; the two are complementary, one lives in your crontab and the other inside Claude Code.
When not to use -p
- Anything that needs a conversation. If the task will raise questions, run it interactively or through the SDK with a
canUseToolcallback that can answer them; headless with--permission-prompts nonewill deny and move on. - Long-lived orchestration. Once you are parsing
stream-jsonwith jq, tracking session IDs in files and handling retries, you have written half an SDK client. The Python and TypeScript Agent SDKs give you typed messages, interrupt support and tool-approval callbacks for the same engine. - Untrusted repositories without
--bare. A plain-prun executes the repo's hooks and connects its MCP servers. In CI on third-party code, that is a supply-chain door;--barecloses it.
Headless mode is where Claude Code stops being a tool you watch and becomes a tool you call. The exit code and the result event tell your scripts it is done; a Stop hook, or a service built on one, tells you.
Keep reading
Desk Exercises for Programmers: 15 Moves to Do While Your AI Agent Codes
7 min read
Exercise While Coding: Turn Claude Code Wait Time Into Micro-Workouts
7 min read
Claude Code Notch Notifications on Mac: See When Claude Is Done Without Watching the Terminal
7 min read
Claude Code Tips and Tricks: 12 Ways to Use Claude Code Effectively
8 min read
Claude Code Multiple Sessions: How to Run Agents in Parallel Without Losing Track
6 min read
A Claude Code Workflow That Doesn't Involve Watching the Terminal
5 min read
Claude Code Hooks: A Practical Guide to Automating Your Agent Workflow
7 min read
Claude Code Auto Mode: Fewer Permission Prompts Without Living Dangerously
7 min read
Claude Code Subagents: How to Delegate Work to Specialized Agents
7 min read
Claude Code Context Management: Treat the Context Window Like a Budget
6 min read
Per-Subagent Model Selection: Route the Grunt Work Down, Keep the Judgment Up Top
7 min read
Skills and Plugins: How to Teach Claude Code Your Way of Working
7 min read
Claude Code Background Tasks: Run Long Commands Without Blocking Your Session
6 min read
Codex CLI Notifications: How to Get a Ding When Codex Is Done or Needs Input
7 min read
Cursor Notification When Done: Every Way to Get Notified When Cursor Finishes
6 min read
Claude Code Notifications: How to Get Notified When Claude Code Finishes or Needs Your Input
6 min read
Want to Be Notified When Claude Responds? How Claude Notifications Work on Web, Desktop, and Mobile
5 min read
Claude Code Notification Scripts: Copy-Paste Recipes for Every Platform
6 min read
Get a Ding the Moment Codex Needs Your Response
6 min read
Get Notified the Moment Claude Code Is Waiting for Your Input
6 min read
“Notifications Are Turned Off for Claude” — Here Is the Fix
6 min read
Codex Sound When Done: Make Codex CLI Play a Sound When It Finishes
6 min read
terminal-notifier + Claude Code: Native macOS Alerts When Your Agent Finishes
6 min read
Gemini CLI Notifications: How to Get a Sound or Alert When Gemini Finishes
6 min read
Get Claude Code Notifications on Your Phone
6 min read
Claude Code Remote Control, Explained
7 min read
preferredNotifChannel: Claude Code's Built-In Notification Setting
6 min read
Claude Code Notifications in tmux and Over SSH
7 min read
Claude Code Effort Levels: Why Your Setting Keeps Getting Ignored
8 min read
Codex vs Claude Code Notifications: How Each One Tells You It Is Done
8 min read
Claude Code Notifications Not Working: A Diagnostic Checklist
10 min read
Claude Code Notifications Inside Your Editor's Terminal
7 min read
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS Explained
7 min read
CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP Explained
8 min read
Cursor Alerts Explained: Every Alert Cursor Raises, and How to Control Them
7 min read
Claude Code Environment Variables: The Groups That Actually Matter
10 min read
Claude Code Notifications on WSL and Windows
9 min read
Claude Code Checkpoints and /rewind
8 min read
Claude Remote Control: How It Works, Turning It Off, and Auto Mode
7 min read
Claude Code /loop: Poll a Deploy, Babysit a PR, and Get Told When It's Done
8 min read
Setting Up NotchFit With OpenAI Codex CLI: Hooks, $notchfit-plan, and the Daily Flow
7 min read
Claude Mods Explained: What the New Plugin Layer Can Do, and How to Turn On 'You Should Know'
9 min read
NotchFit vs Mac Break Reminder Apps: A Clock or a Signal From Your Coding Agent?
8 min read
Building a Movement Habit at a Desk: Use the Agent Run as the Cue, One Set as the Unit, and Streaks Carefully
8 min read