Claude Code without a terminal
claude -p runs the same agent loop once and exits, which makes it a command you can pipe, script and schedule. The flags decide whether it stays predictable.
A command, not a conversation
claude -p "prompt" runs the agent once and exits. The loop is the same one you use interactively, with tools and all. What changes is that nobody is there to answer a question, so everything the session would have asked has to be decided up front.
It exits with code 0 on success and reads standard input, capped at 10MB. That second part is the useful one:
$ git diff main | claude -p "list risky changes in this diff"✻ Reads the diff from stdin✻ Answers once and exits# No Bash permission was needed to read the diff.
Piping small context in means Claude does not need permission to fetch it. The same loop is also available as Python and TypeScript Agent SDK packages, for when a shell script is no longer enough.
Choosing what comes back
--output-format has three values.
textis the plain answer.jsonreturns the result, the session id andtotal_cost_usd, with costs per model.stream-jsonreturns newline-delimited events as they happen.
For scripts, json is usually the one. Pair it with --json-schema '{...}' and the structured result lands in structured_output. One trap: the schema format keyword is accepted but not enforced, and if a run ends without valid structured output the field is simply absent. Check that it is there before you use it.
A run finishes but your script finds no structured_output field. What is the right reading?
Chaining calls
The json output carries a session_id. Capture it and the next call continues the same conversation:
sid=$(claude -p "review src/auth" --output-format json | jq -r .session_id)
claude -p "now fix what you found" --resume "$sid"--continue (-c) picks up the latest session, --resume (-r) takes an id, a name or an absolute path to a session .jsonl file, and --fork-session branches instead of continuing. --no-session-persistence leaves no session behind, which suits a throwaway run.
The flags that keep it bounded
A script that loops forever or spends without limit is worse than no script. Print mode has the guards:
--max-turnscaps the number of turns.--max-budget-usdcaps spend, using a client-side estimate. Both only work in print mode.--allowedToolsand--disallowedToolsset what Claude may use. A bare name in--disallowedToolsremoves the tool from context entirely.--permission-prompts nonedenies anything that would have prompted and tells Claude not to retry.
Denials are reported as permission_denied events and in permission_denials in the result, so a script can see what was refused. Without a permission-prompt tool, repeated blocks drop the action and the run continues, rather than stopping.
Exiting cleanly
A -p run waits for background commands, subagents, Monitor watches and pending /loop wakeups before it exits. Idle waiting is cut at 10 minutes, tunable with CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS.
When you stop a run from outside, the signal matters. SIGTERM exits with code 143 and leaves the turn unfinished. SIGINT ends the turn cleanly. If you did interrupt one, CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1 continues it on resume.
Which signal ends a headless turn cleanly?
Chapter complete
You've finished chapter 2 of 6 in Parallel and unattended.
The Claude Code action runs the agent inside a GitHub workflow, either waiting for an @claude mention or running a prompt on any event. Setup is short; the access and secret choices are where it goes wrong.
Next chapter