Use when running Claude Code headless — claude -p, JSON output, claude calling claude, or a nested run that hangs, denies a tool or picks the wrong model.
Scanned 10/4/2026
npx -y skills add Getty/skills --skill claude-headless --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Claude Headless?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/getty-claude-headless)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: claude-headless
description: "Use when running Claude Code headless — claude -p, JSON output, claude calling claude, or a nested run that hangs, denies a tool or picks the wrong model."
---
# Headless Claude Code
`claude -p "<prompt>"` runs one non-interactive turn-loop and exits. Calling it
from inside a running Claude Code session works — no environment cleanup
needed. Facts below verified against claude 2.1.261; when a flag misbehaves,
`claude --help` is the authority for the installed version.
## Matching the parent's model
A child does **not** inherit the parent session's model — it reads the saved
user default (`~/.claude/settings.json`, key `model`; this is where `/model`
saves). To guarantee the same model, pass it explicitly and verify:
1. The system prompt's environment block states the exact model ID — e.g.
`MiniMax-M3-512k`, or `claude-fable-5` when on Claude. No environment
variable carries it.
2. Pass `--model <exact-id>` using that **exact string**, verbatim. Do not
substitute an Anthropic-family alias (`fable`, `opus`, `sonnet`, `haiku`)
when the parent is on a different provider; those aliases resolve inside
the Anthropic namespace only, and would silently swap the model.
3. Verify from the output: with `--output-format json`, the `modelUsage`
object is keyed by the model ID that actually ran — assert it is the one
you asked for.
```bash
claude -p "Reply with exactly one word: PONG" \
--model MiniMax-M3-512k --output-format json \
| jq -er '.result, (.modelUsage | keys[0])'
```
**Default to the model you're already on.** The frontier tier
(`fable` / `claude-fable-5`) is the most expensive slot in the Anthropic
lineup and a reliable way to set your token budget on fire for no good
reason — a one-shot `-p` call on `fable` writes a fresh cache before it
even answers. Escalate to `fable` only when the task genuinely needs
frontier reasoning; for everything else, let the parent's tier stand.
The system prompt's "alternative models" list (typically the recent Claude
family plus Haiku) is the switch menu — your current model is often *not*
on that list when it belongs to another provider. Pick the cheapest tier
appropriate for the task; per-token pricing varies by provider, so use that
provider's rate card, not Claude's.
## JSON output
`--output-format json` (requires `-p`) returns one object; the fields that
matter: `result` (the final text), `is_error`, `session_id` (feed to
`--resume`), `total_cost_usd`, `num_turns`, `stop_reason`, `modelUsage`,
`usage` (token detail), `permission_denials`. `--output-format stream-json`
emits events as they happen and additionally requires `--verbose`;
`--json-schema <schema>` forces structured output.
## Permissions — decide before launch
A one-shot print run cannot ask. An unapproved tool call is denied (see
`permission_denials`), so the run limps or stops instead of prompting. Choose
one:
- `--allowedTools "Bash(git *),Read,Edit"` — pre-approve exactly what the task
needs (permission-rule syntax; the deny syntax is `--disallowedTools`).
- `--permission-mode <mode>` — `auto`, `dontAsk`, `acceptEdits`, `plan`,
`bypassPermissions`.
- `--dangerously-skip-permissions` (= `bypassPermissions`) — only inside an
isolated environment (container, throwaway VM), never on a working checkout
you care about.
## Staying in the conversation
The single turn is a choice, not a limit. `--input-format stream-json` keeps
stdin open, so one process takes several turns and remembers the earlier ones:
```python
p = subprocess.Popen(
["claude","-p","--input-format","stream-json","--output-format","stream-json",
"--verbose","--replay-user-messages","--model","claude-haiku-4-5-20251001"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
def say(text): # one message in
p.stdin.write(json.dumps({"type":"user","message":{"role":"user",
"content":[{"type":"text","text":text}]}}) + "\n")
p.stdin.flush() # read stdout until type == "result"
```
`--replay-user-messages` echoes each message back so the driver knows it
landed; closing stdin ends the run. `--include-partial-messages` streams
chunks as they arrive, `--forward-subagent-text` surfaces what its subagents
say. Both need `--output-format stream-json`.
This is also the one way a print run can *ask*: point
`--permission-prompt-tool` at an MCP tool of your own and every tool decision
comes to the driver as a question instead of being silently denied.
Because the channel is a pipe, it survives ssh — and that is how you drive an
agent on a host whose Claude is somebody else's login:
```bash
ssh -T host 'export PATH=$HOME/.local/bin:$PATH; cd /srv/project; exec claude -p \
--input-format stream-json --output-format stream-json --verbose'
```
Write the same JSON lines into ssh's stdin. Use `-T` (no tty) and `exec` so the
remote claude owns the connection's stdio and its signals. Verified across
hosts: turn two still knew what turn one had been told. Nothing is shared but
the ssh session — no account, no Remote Control.
## The child is a peer
A `-p` child is a full session, not an isolated command. It appears in
`claude agents --json` — as `kind: interactive`, named after its directory
(`p5-alien-libgit2-13`) — and it can call `ListAgents` and `SendMessage` to
reach the session that started it and every other session on the machine.
So a headless run does not have to report only through its exit JSON; it can
talk back while it works, and you can talk to it. See `claude-cross-session`.
## What the child inherits
Settings resolve against the **child's cwd**, not the parent's session:
managed → user (`~/.claude/settings.json`) → project (`.claude/settings.json`)
→ local (`.claude/settings.local.json`), plus the cwd's `CLAUDE.md`, skills,
agents, and hooks. Consequences:
- Run the child in the directory whose context it should have; `cd` is the
main inheritance lever.
- `--setting-sources user,project,local` narrows which scopes load;
`--settings <file-or-json>` injects overrides; `--bare` skips hooks,
plugins, MCP, and CLAUDE.md discovery entirely — pass what it needs
explicitly (`--append-system-prompt`, `--mcp-config`, `--agents`).
- Session-scoped state of the parent (its model, its conversation) never
transfers; context the child needs goes into the prompt, the system-prompt
flags, or files it can read.
## Guard rails and follow-ups
- `--max-turns <n>` and `--max-budget-usd <amount>` bound a runaway child.
- `session_id` from the JSON + `claude -p --resume <session-id> "<next>"`
continues that conversation; `--continue` takes the most recent one.
`--session-id <uuid>` picks the id up front. `--no-session-persistence`
for fire-and-forget runs.
- Background Bash commands the child starts are killed ~5 s after its final
result — a headless run must finish its work in the foreground.
- Inside any Claude Code Bash call, `CLAUDECODE=1` and
`CLAUDE_CODE_SESSION_ID` are set — a script can detect it is being run *by*
Claude and avoid spawning recursively by accident.
## Related
- `claude-cross-session` — when you do not want a result but a counterpart:
starting a session elsewhere with `claude --bg` and talking to it while it
runs.
- `getty-agent-team` — subagents *within* a session (briefing-preloaded);
reach for headless spawning only when a separate process is genuinely
needed: clean context, different cwd, CI, or another program driving Claude.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!