Skip to content
Back to skills

Claude Headless

ASecurity

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.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
ai-agentspythongobashrailsgit

Works with

  • claude code
  • mcp

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add Getty/skills --skill claude-headless --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Claude Headless?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Claude Headless
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/getty-claude-headless/badge)](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.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…