# 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:

```terminal
$ 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.

**Question:** A run finishes but your script finds no structured_output field. What is the right reading?

- The field is empty on purpose, which means success
- The run ended without valid structured output, so the script must handle the missing field
- The JSON schema was too strict and was ignored
- The cost estimate overflowed

**Answer:** The run ended without valid structured output, so the script must handle the missing field. When there is no valid structured output the field is absent. A script has to check for it rather than assume it.


## Chaining calls

The json output carries a `session_id`. Capture it and the next call continues the same conversation:

```bash
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.

> **Know what -p loads**
Without `--bare`, `-p` shows no trust dialog but still runs project hooks and connects `.mcp.json` servers, even in a folder you never trusted. Use `--bare` in CI and scripts so every machine behaves the same; the docs expect it to become the default. It never reads OAuth or the keychain, so it needs `ANTHROPIC_API_KEY` or an `apiKeyHelper`.


## 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.

> **Debug a broken setup**
If a headless run behaves oddly, `--safe-mode` starts with every customization off: CLAUDE.md, skills, plugins, hooks, MCP and output styles. If the problem disappears, add things back until it returns.


> **Make a script that reports its cost**
Run `claude -p "summarize this repo in two lines" --output-format json --max-turns 3` and read `total_cost_usd` and `session_id` from the output. Then pass that id to `--resume` with a follow-up question.


**Question:** Which signal ends a headless turn cleanly?

- SIGTERM
- SIGKILL
- SIGINT
- SIGHUP

**Answer:** SIGINT. SIGINT ends the turn cleanly. SIGTERM exits with code 143 and leaves the turn unfinished.
