Non-Interactive Mode
Passing -p runs Claude Code as a command-line program: it reads a prompt and optional stdin, does the work, prints a result, and exits with a status code your script can branch on.
What it is
Add -p (or --print) to any claude invocation and there is no chat session. Claude works the task and prints the result. Non-interactive mode reads stdin, so you can pipe data in and redirect output out like any other Unix tool. Piped stdin is capped at 10MB; larger inputs go in a file you reference by path.
Three output formats. text is the default and prints the response. json returns a structured object with the result, session ID, usage, and cost. stream-json emits newline-delimited events for real-time consumption, with the final line a result message. Adding --json-schema to --output-format json forces the response into a schema you define, delivered in the structured_output field.
--bare is the mode to use in CI. It skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md, so a teammate’s ~/.claude config cannot change what your pipeline does. Bare mode never reads OAuth credentials or the keychain either, so set ANTHROPIC_API_KEY. It gives Claude the Bash, file read, and file edit tools, and you add anything else back with explicit flags. The docs say it will become the default for -p in a future release.
What it does for you
- It puts Claude in a pipeline.
git diff main | claude -p "report typos"is a linter you did not have to write. - It makes the result machine-readable.
--output-format json --json-schemareturns a validated object, so the next step in your script does not have to parse prose. - It gives CI a pass/fail signal. Claude Code exits 0 on success and non-zero on failure, so a job can gate on the run without inspecting the text.
How it works
01Claude Code starts without an interactive UI
Without
--bareit loads the same context an interactive session would, including anything configured in the working directory or~/.claude. Terminal-only commands such as/loginare unavailable, but user-invocable skills and custom commands work: put/namein the prompt string.02stdin is read if present
Piped input is appended to the prompt, capped at 10MB. If stdin cannot be read, Claude Code warns to stderr and continues with the command-line prompt.
03Permissions come from flags, not prompts
There is nobody to approve anything. Use
--allowedTools "Bash,Read,Edit"for specific tools, or--permission-modefor a baseline.dontAskdenies anything not pre-approved;acceptEditslets Claude write files but still needs rules for other shell commands.04The result is printed in the format you chose
With
json,total_cost_usdand a per-model breakdown are included, both client-side estimates. Withstream-json --verbose --include-partial-messages, tokens arrive as they are generated.05Background work is bounded at exit
A background Bash task is terminated about five seconds after the final result and stdin closes. Background subagents and workflows are waited for, capped at ten minutes by default; adjust with
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS.06The process exits with a status code
0 on success, non-zero on failure. An invalid flag is reported to stderr before the run starts; a failure inside the run, such as missing authentication, is printed as the result on stdout. A SIGTERM aborts the turn, kills the process tree of any running Bash command, runs
SessionEndhooks, and exits 143.
How to implement it
01Start from the smallest prompt that produces a decision
A CI step wants a verdict, not an essay. Ask for the specific finding and the specific format, and say what to output when there is nothing to report.
02Add --bare and set the API key
Bare mode makes the run reproducible across machines. Set
ANTHROPIC_API_KEYin the environment, because bare mode does not use a subscription login.03Pre-approve exactly the tools the task needs
Pass
--allowedTools "Read,Grep,Glob"for a review, and addBash(...)rules only for the specific commands. The rule syntax is the same aspermissions.allow.04Choose an output contract
Use
--output-format json --json-schemawhen a later step consumes the result, and parse it withjq -r .structured_output. Use plain text only when a human reads it.05Decide what the exit code means
Claude Code’s exit code reports whether the run succeeded, not whether the review passed. Derive the pass/fail from the structured output and exit accordingly.
Examples
name: Claude review
on: pull_request
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Review the diff
id: review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr.diff
claude --bare -p "Review the diff in /tmp/pr.diff for correctness
defects only. Ignore style. For each finding give the file, the
line, and one sentence on what breaks. Return no findings if the
diff is clean." \
--allowedTools "Read,Grep,Glob" \
--output-format json \
--json-schema '{
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": { "type": "string" },
"line": { "type": "integer" },
"severity": { "enum": ["high", "medium", "low"] },
"issue": { "type": "string" }
},
"required": ["file", "line", "severity", "issue"]
}
}
},
"required": ["findings"]
}' > /tmp/review.json
- name: Fail on high-severity findings
run: |
jq -e '.structured_output.findings
| map(select(.severity == "high"))
| length == 0' /tmp/review.json > /dev/null \
|| { jq -r '.structured_output.findings[]
| "\(.file):\(.line) [\(.severity)] \(.issue)"' \
/tmp/review.json; exit 1; }# Pipe in, redirect out
cat build-error.txt | claude -p 'explain the root cause concisely' > cause.txt
# A project-specific linter, wired into package.json
# "lint:claude": "git diff main | claude -p \"you are a typo linter...\""
npm run lint:claude
# Extract structured data and pull one field out
claude -p "Extract the exported function names from src/api.ts" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq -r '.structured_output.functions[]'
# Stream tokens as they are generated
claude -p "Explain this codebase" --output-format stream-json --verbose \
--include-partial-messages \
| jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
# Continue the same conversation across invocations
session=$(claude -p "Start a review of src/auth" --output-format json | jq -r '.session_id')
claude -p "Now focus on the token refresh path" --resume "$session"Use it when
- A pull-request review step in CI that fails the job on high-severity findings.
- A pre-commit or build script that pipes the diff through Claude as a project-specific linter.
- Bulk extraction across many files, where each invocation returns a schema-validated object.
- Explaining a build failure by piping the log in and redirecting the explanation to a file.
- A scheduled maintenance run that summarises what changed in a repository overnight.
Avoid it when
- The task needs judgement calls you would want to intervene on. There is no prompt, so an ambiguous request gets resolved without you and the run either succeeds wrongly or aborts.
- A deterministic tool already does the job. A linter, a formatter, or a type checker is faster, cheaper, and does not vary between runs.
- You need the run to be reproducible but you cannot use
--bare. Without it, a hook in someone’s~/.claudeor a server in the project.mcp.jsonchanges what the pipeline does. - The work depends on long-running background processes. Background Bash tasks are killed about five seconds after the final result.
Common mistakes
SYMPTOMThe run aborts partway through because a tool needed approval.
FIXThere is nobody to prompt. Pass
--allowedToolslisting every tool the task needs, or set a baseline with--permission-mode.acceptEditscovers writes but not arbitrary shell commands.SYMPTOMA
Bash(git diff*)allow rule also permitsgit diff-index.FIXThe space before the wildcard enforces a word boundary. Write
Bash(git diff *), with a space, so the prefix must be followed by a space or end of string.SYMPTOMThe CI job passes even though Claude reported problems.
FIXClaude Code’s exit code reports whether the run succeeded, not whether the review passed. Return a schema and derive the exit code from the data with
jq -e.SYMPTOMThe same command behaves differently on two machines.
FIXWithout
--bare, Claude Code loads local hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md. Add--bareand pass the context you want back in with explicit flags.SYMPTOM
--json-schemasilently returns prose instead of structured output.FIXOn v2.1.205 and later an invalid schema errors with the validator’s diagnostic; earlier versions ignored it. Check the exit message, and note that
formatkeywords are accepted as annotations but not enforced.
Best practices
- Use
--barein CI and setANTHROPIC_API_KEY, so the run does not depend on the host’s configuration. - Pre-approve the narrowest tool set the task needs, and remember the space before
*in Bash rules. - Return a schema whenever another step consumes the result, and parse it with
jqrather than regex. - Derive your own pass/fail from the structured output; do not read Claude Code’s exit code as a verdict.
- Write large inputs to a file and reference the path, since piped stdin is capped at 10MB.
- Capture
session_idfrom the JSON output when you need follow-up invocations to continue the same conversation.
Try it in five minutes
Build a typo linter that fails a script, using structured output.
- 1.In a repository with uncommitted changes, run
git diff | claude -p "list any typos in this diff, one per line, or print NONE". - 2.Re-run it with
--output-format json --json-schema '{"type":"object","properties":{"typos":{"type":"array","items":{"type":"string"}}},"required":["typos"]}'. - 3.Pipe the result through
jq -r '.structured_output.typos[]'and confirm you get a clean list. - 4.Wrap it in a shell script that exits 1 when the array is non-empty, using
jq -e. - 5.Add
--bareand confirm the script still works withANTHROPIC_API_KEYset — this is the version that will behave the same on a CI runner.
Related concepts
Verified against code.claude.com/docs/en/headless on 2026-08-09. See content/SOURCES.md for the full table.
← / → MOVE BETWEEN CONCEPTS