Who this is for
This guide is for developers who already use Claude Code in the terminal and want to call it from shell scripts, scheduled jobs or CI pipelines. After reading it you should be able to run a one-off prompt, get output that a program can parse, grant the tools a run needs without a prompt, and continue an earlier conversation.
What non-interactive mode is
Running claude on its own opens an interactive session. Adding -p (or --print) to any claude command changes that: Claude Code handles the prompt, prints the result and exits[1]. The Agent SDK runs on the same tools, agent loop and context management, and it comes in two forms: a CLI for scripts and CI/CD, and Python and TypeScript packages for finer control[1]. This article covers the CLI only.
A minimal run asks a question about a codebase and prints the answer:
claude -p "What does the auth module do?"Claude Code exits with code 0 when a run succeeds and with a non-zero code when it fails, so a shell script can branch on the result[1]. Mistakes in the command itself are different: an unknown flag is reported on stderr before anything runs, while a failure inside the run, such as missing authentication, comes back as the result on stdout[1].
Start faster with bare mode
A normal -p run loads everything an interactive session would: hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory and CLAUDE.md. --bare skips that discovery and cuts startup time[1]. The documentation recommends it for scripted and SDK calls, and says it will become the default for -p in a future release[1].
Bare mode has a cost. Anything you still need has to be passed explicitly. The documentation lists the flags: --append-system-prompt or --append-system-prompt-file for extra system prompt text, --settings for settings, --mcp-config for MCP servers, --agents for custom agents, and --plugin-dir or --plugin-url for a plugin[1]. Hooks are the exception that catches people out: they do not run in bare mode even when you pass them through --settings or a plugin loaded with a flag[1].
claude --bare -p "Summarize README.md" --allowedTools "Read"The example above also pre-approves the Read tool so the call does not stop for a permission prompt. Bare mode also changes what Claude sees. It is not given the system reminders that Claude Code normally adds, such as the list of available skills, and it cannot run background commands or background subagents[1].
The reason to care is safety as much as speed. Without --bare, a -p run executes 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. There is no trust dialog and no per-server approval in this mode[1]. If you run unfamiliar repositories in CI, that is the first thing to check.
Authentication in bare mode
Bare mode never reads OAuth credentials or the system keychain. For the Anthropic API, put an ANTHROPIC_API_KEY in the environment, using a key created in the Claude Console, or give an apiKeyHelper inside the --settings JSON[1]. Amazon Bedrock, Google Cloud and Microsoft Foundry keep reading their own provider credentials as usual[1].
Give the run its tools
By default, a call to a tool that needs approval will stop and wait. --allowedTools lets Claude use the listed tools without asking. Listing Read and Edit allows reads and edits, except reads from network paths[1]. Here is a run that can run tests and edit files:
claude -p "Run the test suite and fix any failures" \
--allowedTools "Bash,Read,Edit"Tool rules can be narrower. The rule syntax supports prefix matches, so Bash(git diff *) covers any command that starts with git diff. The space before the * matters: without it, Bash(git diff*) would also match git diff-index[1].
For a whole-session baseline, use --permission-mode. auto has a classifier review most actions, dontAsk denies every call that would otherwise prompt, and acceptEdits lets Claude write files without asking[1]. For a scheduled job nobody watches, add --permission-prompts none. Anything that would prompt is then denied unless a PermissionRequest hook allows it, and Claude is told not to retry. The flag needs Claude Code v2.1.259 or later[1].
Read the output in a script
The default text format is plain text. --output-format json returns the result with the session ID and metadata, and stream-json returns newline-delimited JSON events as they happen[1].
The JSON payload also includes total_cost_usd and a per-model cost breakdown. The documentation calls both figures client-side estimates, so use them for trends rather than billing[1]. A pipe works with any command-line tool. The example below sends a build log in and writes the explanation to a file:
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txtStdin is capped at 10MB. For bigger inputs, write the content to a file and mention its path in the prompt[1].
If you need a fixed shape, add --json-schema to a JSON run. The answer then arrives in a structured_output field:
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"]}'Check that structured_output exists before you use it: if Claude declined the request or the run ended without valid output, the field is missing[1]. An invalid schema makes claude exit with an error. The format keyword is accepted, but it is only an annotation and is not enforced[1].
Continue a conversation
--continue picks up the most recent conversation. --resume takes a session ID and picks up that conversation, and it also accepts the path to a session's .jsonl transcript[1]. To run several conversations in parallel, capture the session ID from a JSON run:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"The two commands can run from different directories, because Claude Code finds the session by its ID in any project on the machine[1].
Long-running work
When the turn ends and stdin closes, a -p run may keep waiting for background work it started: background commands, background subagents, workflows and Monitor watches. By default it stops still-running work after 10 minutes of continuous idle waiting and discards the partial result. CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS changes that limit, and setting it to 0 removes it[1].
Stopping a run needs care too. SIGTERM makes Claude Code exit with code 143 and leaves the turn unfinished, with no result recorded. To end the turn cleanly, send SIGINT first, or call interrupt() from the Agent SDK[1].
Common problems
MCP servers are the usual surprise. With --mcp-config, -p waits for pending servers before the first turn, up to 30 seconds by default through MCP_TIMEOUT. Servers that fail validation are dropped from the run without stopping it. The system/init event lists them in mcp_servers and mcp_server_errors, so a CI job can fail when one is missing[1].
Plugins have the same check through the plugins and plugin_errors fields[1]. Built-in commands that need a terminal, such as /login, do not exist in -p mode, but skills and custom commands still work when you put /skill-name in the prompt[1].
Next steps
The next steps in the official documentation point to the Agent SDK quickstart, the CLI reference, and the GitHub Actions and GitLab CI/CD pages[1].
Sources
- 1Run Claude Code programmaticallyAnthropic Claude Code docs
Cite this page: Claude Code headless mode with claude -p. TellWorld, https://www.tellworld.net/practice/practice-claude-code-headless. Updated 2026-11-14