Appearance
Text size
Typeface
Line spacing
2
Chapter 2 · 4 min

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.

  • text is the plain answer.
  • json returns the result, the session id and total_cost_usd, with costs per model.
  • stream-json returns 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.

Check yourself

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-turns caps the number of turns.
  • --max-budget-usd caps spend, using a client-side estimate. Both only work in print mode.
  • --allowedTools and --disallowedTools set what Claude may use. A bare name in --disallowedTools removes the tool from context entirely.
  • --permission-prompts none denies 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.

Check yourself

Which signal ends a headless turn cleanly?

Read further