19 KiB
Spawning agents in Claude Code
Harness mechanics for dispatching, resuming and forking agents in Claude Code, and for the headless sessions (claude -p) that spire:gremlins drives. Other harnesses get their own spawning-<harness>.md when needed.
Each claim carries a status and a date:
- verified: read from the tool schema or the Claude Code docs, or reproduced in a local test, on that date; the row names which. Not tested at runtime unless the row says so.
- observed: seen in real runs; the mechanism may be assumed. Source given.
- assumed: inferred, not checked. Treat as a hypothesis.
- unknown: open; check before relying on it.
The harness changes. When a claim fails, fix it here with a new date.
Rules of thumb
- Foreground for sequential work. Pass
run_in_background: falsewhen the next step depends on the result. Background only for independent leaves whose result can go to the session root. - Resume only from the session root. Igor may
SendMessagea finished agent (the Athanor between phases, an adept). A subagent should not resume its own children; it dispatches fresh foreground agents instead. - Pinned agents: omit
model. The per-call alias overrides the agent's frontmatter pin. Passmodelto a pinned agent only when you mean to override it, and say so in the journal or run log. - Unpinned agents: pass
model. Otherwise they inherit the caller's model, which may be Fable. - Stamp the model id. Every agent reads its exact id from its system prompt and writes it into its output.
- Complex blocks go to gremlins via
spire:gremlins; never resume or fork a headless session by hand. The helper owns the flags, the cache bookkeeping and the usage ledger (see "Headless sessions" below). - Leaf briefs say "do this yourself; do not dispatch further agents." Only orchestrators (the Athanor) are meant to fan out.
- Notifications report a turn ending, not a job finishing. Read the result text before concluding anything. Check
ListAgentsbefore resuming.
Dispatch and notification
| Claim | Status |
|---|---|
Agent calls run in the background by default; run_in_background: false runs them in the foreground and returns the result to the caller. |
verified 2026-10-05 (Agent tool schema) |
| Background children of a subagent notify the session root, not their parent. The parent sees nothing and stalls until the root feeds it the result. | observed 2026-09-24 and 2026-09-28 (spacecraft journals, "athanor stalled twice … nudged with results"); dirigent memory, settled 2026-09-29. Mechanism assumed. |
An agent that hands work to a background child and then yields notifies completed at once, before any deliverable exists. Expect one more notification per background grandchild, each with its own task-id. |
observed 2026-09-22 (Spire harness notes) |
The same task-id may notify more than once (one per turn). Track the deliverable, not the count. |
observed 2026-09-22 |
Background grandchildren show up in ListAgents as flat, top-level agents of the session. If one is still writing your output path, do not resume its parent: that starts a second writer. TaskStop the child first if you mean to abandon it. |
observed 2026-09-22 |
TaskList is the todo list, unrelated to agents. TaskOutput on an agent dumps its whole transcript into your context; do not use it for agents. |
observed 2026-09-22 |
A general-purpose agent has the Agent tool and will delegate if its brief reads like an orchestration brief. |
observed 2026-09-22 |
Resume
| Claim | Status |
|---|---|
SendMessage to a finished agent's name or id resumes it with its context intact. A new Agent call starts fresh. |
verified 2026-10-05 (Agent tool schema) |
| A resume issued from inside a subagent runs in the background; its completion goes to the root and the subagent stalls. From the root (Igor) it is safe, because the root is where the notification lands. | observed once 2026-10-05 (spacecraft journal: orchestrator resumed its implementer via SendMessage, went background, yielded). Mechanism assumed from the routing above. |
Models
| Claim | Status |
|---|---|
The per-call model parameter accepts aliases only (opus, sonnet, haiku, fable) and takes precedence over the agent definition's model frontmatter. |
verified 2026-10-05 (Agent tool schema) |
With model omitted, a pinned agent runs on its pin; an unpinned agent inherits the caller's model unless a default subagent model is configured. |
verified 2026-10-05 (Agent tool schema) |
A full model id (e.g. claude-opus-5-5) can only be set in the agent definition's frontmatter, so pinning by full id means a pinned agent dispatched without model. |
verified 2026-10-05 (follows from the alias-only parameter) |
The opus alias overrode 4.6 pins in 184 of 184 spawns (they ran on 5.5). |
observed 2026-09-27 (spacecraft MVP retrospective) |
A 4.6 Igor passing model: "opus" to the then 4.6-pinned Athanor got a 4.6 run (the dirigent end review that passed with zero findings). |
observed 2026-10-05 (user's attribution; the dirigent report recorded the model as unknown). Consistent with both the alias following the caller and the pin holding, so it is not evidence for alias-over-pin. The 184/184 row and the schema are. |
What the opus alias resolves to (the caller's model, a session default, or the newest Opus). |
unknown. Check the stamped id of every plan, attack and review instead of relying on it. |
| Every agent sees its exact model id in its system prompt. | observed 2026-10-05 for general-purpose agents (prompt shows claude-opus-5-5); not checked for plugin agents with their own bodies |
Headless --model takes full ids (and the [1m] suffix), so gremlins pin by full id; the per-call alias limit above is an Agent-tool limit. |
observed 2026-10-16 (preflight report, P0.5) |
Context window of the bare claude-opus-5-5 pin versus a [1m] suffix (other Spire pins carry [1m]). |
resolved 2026-10-05: all Opus-pinned Spire agents (igor, adept, athanor, zathras) pinned to claude-opus-5-5[1m] (user decision; Pinky, when it exists, stays 4.6); the ~600k guesstimate in models.md assumes the larger window |
Fork (Agent tool)
Spire no longer uses Agent-tool forks (goal decision 2026-10-16): complex blocks go to gremlins, headless forks of a trained base (see "Headless sessions" below). The rows stay as a record of what the Agent tool offers.
| Claim | Status |
|---|---|
The Agent tool schema documents subagent_type: "fork": a fork always inherits the caller's model; the model parameter is ignored. |
verified 2026-10-05 (schema text) |
fork was not offered as an agent type on 2026-10-05 or on 2026-10-15. |
observed 2026-10-15 (agent-type lists of those sessions; design 2026-10-16-01 §9) |
| A fork inherits the current conversation only; no parameter names another agent to fork. | verified 2026-10-15 (docs, sub-agents) |
| A fork has the same system prompt, tools, model and history as the forking session. | verified 2026-10-15 (docs, sub-agents) |
An Agent-tool fork cannot fork again: "A fork can't spawn further forks." A headless --fork-session of a fork works. |
verified 2026-10-15 (docs, sub-agents) for the Agent tool; verified 2026-10-16 (research 2d, local test T6) for headless |
Fork mode is on by default in interactive sessions (v2.1.232 and later), off under -p and the SDK unless CLAUDE_CODE_FORK_SUBAGENT=1. With it on, all subagents run in the background and run_in_background is removed. |
verified 2026-10-15 (docs, sub-agents) |
Agent-tool forks and subagents use the 5 min cache bucket by default; the main conversation (including -p runs) gets 1 h on a Claude subscription within plan usage. |
verified 2026-10-16 (research F4a; docs, prompt caching) |
| Whether a foreground Agent-tool fork returns its result to the caller, and whether it sees the caller's preloaded skills. | unknown 2026-10-16 (never tested; Spire does not use Agent-tool forks) |
Headless sessions (claude -p)
spire:gremlins (command spawn) is the only supported way to start, resume or fork one; these rows say why its command line is what it is. Tested on Windows with claude 2.1.283 and 2.1.299. Research ids (1a to F5b) refer to docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md; P0.x ids to docs/workpad/reports/2026-10-16-09-headless-forks-preflight.md.
| Claim | Status |
|---|---|
--output-format json prints one result object: session_id, result, usage (with the cache fields), modelUsage, total_cost_usd, permission_denials, subagent_stats. With stream-json --verbose the first event is system/init with session_id, model, tools, agents, skills, plugins, permissionMode. |
verified 2026-10-16 (research 1a; docs, headless; local tests T1, T6) |
--resume <id> finds the session by id in any project on the machine; without --fork-session it appends to the same id. |
verified 2026-10-16 (research 1b; docs; local test T5) |
--session-id <uuid> lets the caller choose the id up front, so no output parsing is needed. |
verified 2026-10-16 (research 1d; docs, --help) |
--continue means the most recent conversation in the cwd, -p sessions included. Do not use it for fan-out; pass explicit ids. |
verified 2026-10-16 (research 1c; docs) |
--fork-session gives the run a new session id and a copy of the base history. |
verified 2026-10-16 (research 2a; --help 2.1.283; docs) |
| The base stays unchanged on disk and in later resumes; forks do not see each other. | verified 2026-10-16 (research 2b; docs, sessions; local tests T2 to T5) |
| Parallel forks of one base work and each gets its own id and transcript. Resuming the same id in two processes without forking interleaves both into one transcript. | verified 2026-10-16 (research 2c; local tests T2 and T3 in parallel; docs) |
| A fork of a fork works. | verified 2026-10-16 (research 2d; local test T6) |
| Forks hit the cache on the shared prefix (T2 and T3 each read 14,650 tokens against the base's 8,661 created). | verified 2026-10-16 (research 3a; local test) |
| A cache hit needs an identical tool set, model and system prompt. A fork with different flags read 0 and re-created 17,474; one that changed only the model read 4,293 and created 7,213. | verified 2026-10-16 (research 3b; local tests T6, T7) |
A fork does not inherit the base's permission mode, --allowedTools or --tools (T6 came up in default with all 43 tools). Pass them again, identically, on every fork. |
verified 2026-10-16 (research 4b; local test T6; docs, sessions) |
Also not restored on resume: --mcp-config, --settings, --plugin-dir, --fallback-model, --add-dir. The system prompt (including --append-system-prompt) is recorded at the first request and reused on resume and fork until compaction; later flag text is ignored (--system-prompt-snapshot, --help 2.1.283). |
verified 2026-10-16 (research 4c; docs, sessions and CLI reference) |
The model is restored on resume; --model on the fork overrides it (a haiku fork of a sonnet base ran on haiku). |
verified 2026-10-16 (research 4d; docs; local test T7) |
total_cost_usd and modelUsage on a resumed or forked run are cumulative for the conversation, base spend included (v2.1.277 and later). Per-call cost is a difference against the base's total; never sum results. --max-budget-usd counts only the call's own spend. |
verified 2026-10-16 (research 3d, F4e; docs, headless, cost tracking) |
The 1 h cache TTL is the default only for the main-conversation bucket (interactive turns and -p runs) on a Claude subscription within plan usage; usage credits, API key and cloud providers default to 5 min, as does the main conversation over the plan limit. |
verified 2026-10-16 (research F4a, 3c; docs, prompt caching; local ephemeral_1h writes in T1 to T7) |
The TTL is configurable: CLAUDE_CODE_PROMPT_CACHE_TTL or promptCacheTtl (main bucket), CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL or subagentPromptCacheTtl (everything else), each 5m or 1h, v2.1.242 and later. ENABLE_PROMPT_CACHING_1H=1 sets both; FORCE_PROMPT_CACHING_5M=1 overrides everything. |
verified 2026-10-16 (research F4b; docs, prompt caching) |
| A cache hit resets the TTL timer, so forks spaced under the TTL keep the base prefix warm. Parallel sessions in the same directory share a prefix; a different cwd or git-status snapshot changes it, so run every fork from the base's cwd. | verified 2026-10-16 (research F4c, F4d; docs, prompt caching) |
Under -p nothing can prompt: without a host or --permission-prompt-tool, anything that would prompt is denied, listed in permission_denials, and the run continues. dontAsk denies anything not pre-allowed. |
verified 2026-10-16 (research 4a; docs, headless and permission modes) |
system/init.model is not the model that ran: read modelUsage or assistant.message.model. |
verified 2026-10-16 (research 5d, F2a, F2c; local tests M1, M2) |
--permission-mode plan upgrades a Haiku session to Sonnet. The docs imply it (availableModels section) and do not state it as a rule. |
observed 2026-10-16 (research 5d, F2a, F2b; local tests T1 to T5, M1, M2; docs only indirect) |
Aliases resolve by CLI version: haiku is Haiku 4.5 on 2.1.283 and Haiku 5.5 on 2.1.299 (Haiku 5.5 needs v2.1.293 or later, Sonnet 5.5 v2.1.284 or later). |
verified 2026-10-16 (research F2c; local tests; docs, model config) |
Without --bare, -p loads installed plugins, skills, agents, hooks, MCP and CLAUDE.md like an interactive session. |
verified 2026-10-16 (research 5a; docs; local test T6) |
--bare skips all of that discovery and refuses OAuth (Not logged in); it needs ANTHROPIC_API_KEY. |
verified 2026-10-16 (research 5b, F3d; docs; local test E2) |
--model (alias or full id), --agents, --agent, --append-system-prompt[-file], --settings, --max-budget-usd, --max-turns and --json-schema exist in 2.1.283. |
verified 2026-10-16 (research 5c; --help; docs) |
A nested claude -p inherits CLAUDECODE=1 and CLAUDE_CODE_CHILD_SESSION=1; no recursion guard blocks it, and the session is still persisted. |
verified 2026-10-16 (research 6a; docs, env vars; local env listing) |
Agent depth is counted per process from its main conversation; the parent's depth is not passed down. A nested claude -p can use the Agent tool, and a second level nests too. At the depth limit the Agent tool is simply absent: no error. |
verified 2026-10-16 (research F1a to F1d; local tests D1 to D3; docs, env vars) |
| A nested process does not count against the parent's Agent depth or concurrency caps. | assumed 2026-10-16 (research 6b; inferred from per-process subagent_stats, not tested with a deep parent) |
--bg does not combine with -p. |
verified 2026-10-16 (research 6c; docs, headless) |
A headless run is invisible to the parent's task notifications, ListAgents and TaskStop: the orchestrator must block on Bash or poll. |
assumed 2026-10-16 (research 6c; not a tracked Agent, never tested from a subagent) |
Transcripts land in ~/.claude/projects/<cwd with every non-alphanumeric character as '-'>/<session-id>.jsonl, one file per fork, each holding the copied base history under its own sessionId; 30-day retention. |
verified 2026-10-16 (research 6d; docs, sessions; local listing) |
The nested process inherits the Desktop host's env: CLAUDE_CODE_MESSAGING_SOCKET, CLAUDE_CODE_ENTRYPOINT=claude-desktop and the OAuth-refresh variables, among others. Removing twelve of those vars (F3a) with env -u drops the Artifact tools (35 to 32 built-ins); auth still works. |
observed 2026-10-16 (research 6e as corrected by F3a, F3b; local tests T6, E1) |
The messaging_socket_path in system/init is the nested process's own pipe, different on every run and never the host's. This corrects the first reading of 6e. |
verified 2026-10-16 (research F3c; local tests T6, E1) |
| Whether the nested process's pipe can reach the host app, and which single env var gates the Desktop Artifact tools. | unknown 2026-10-16 (research F3c; not isolated) |
The PATH claude (2.1.283, native installer) and the Desktop host's bundled claude.exe (2.1.299, reachable as $CLAUDE_CODE_EXECPATH) are separate installs. |
verified 2026-10-16 (research F5a; local) |
| A fork made by another binary or on another model misses the base's cache: a 2.1.299 fork of a 2.1.283 base read 0 and created 15,020. Pin one binary and a full model id for the base and all its forks. | verified 2026-10-16 (research F5b; local test V1; docs, prompt caching) |
Observed in the Phase 0 preflight on this Windows host (2026-10-16):
| Claim | Status |
|---|---|
uv run adds VIRTUAL_ENV and a venv Scripts entry at the head of PATH, and sets no UV_* vars; stdout stays clean (one JSON line). |
observed 2026-10-16 (preflight report, P0.0) |
No job objects on this host (IsProcessInJob false for the uv run launcher and every supervisor). |
observed 2026-10-16 (preflight report, P0.1) |
The Bash tool's TaskStop kills by process tree (parent links). A supervisor created through CIM Win32_Process.Create (parent WmiPrvSE.exe) survived the stopped Bash call; rungs that start it as a child of the launcher died with it. |
observed 2026-10-16 (preflight report, P0.1) |
A supervisor surviving the end of the agent's turn (the hand-back) was not tested in Phase 0; the parentage under WmiPrvSE.exe makes it expected. |
unknown 2026-10-16 (preflight report, "Not tested / open"; Phase 4 smoke) |
An unhidden CIM spawn opens a visible Windows Terminal window. The fix is Win32_ProcessStartup.ShowWindow=0 plus CREATE_NO_WINDOW on every child; no window was seen in 26 checks, including a claude to bash to PING tree. |
observed 2026-10-16 (preflight report, P0.1) |
A CIM-created process gets the registry default user env, not the caller's (58 of the Bash env's variables missing, among them every CLAUDE* var and CLAUDE_CODE_EXECPATH), so the launcher must pass the full scrubbed env to the supervisor itself. |
observed 2026-10-16 (preflight report, P0.1) |
The Bash tool backgrounds a call that passes its timeout; it does not kill it. |
observed 2026-10-16 (preflight report, P0.1(d)) |
An unlisted model (not admitted by availableModels) is substituted silently: the run uses a permitted model and emits a system/informational warning ("Model … is restricted by your organization's settings. Using … instead."). |
observed 2026-10-16 (preflight report, P0.5) |
An ! injection runs in an Agent-tool subagent's skill preload, but the skill's allowed-tools did not pre-approve the subagent's later uv run call (denied: "This command requires approval"). The probe dir was an untrusted workspace; Phase 4 re-checks in a trusted project. |
observed 2026-10-16 (preflight report, P0.8) |
Agent types
| Claim | Status |
|---|---|
Plugin agent types (athanor, adept, researcher, …) can be missing in remote sessions, where /reload-plugins is not available. Fallback: general-purpose with an explicit model, and the agent file's body pasted into the brief. |
observed 2026-10-05 (reliquary Igor journal, 21:00) |
Explore and Plan are read-only (no Edit/Write). Do not use them for tasks that must write a file. |
observed 2026-09-22 (Spire model tiers) |