AGENT SKILL

spawn-session

Spawns a background Claude Code helper in the project or another folder with the configured Remote Control and the boot-written helper system prompt that a hand-built `claude --bg` omits, watches it until idle, and relays its report to the operator. Use when the operator says "spawn a helper", "spawn a new session", "run this in a background session", asks for a new session with `--model` or `--effort`, asks to switch a running helper's model or effort, or names `/spawn-session`.

License 许可
MIT
所属仓库
gtapps/hermitd
最近更新
2026-11-12
Stars
★ 84
Forks
16

01 前言 Front Matter

name
spawn-session
description
Spawns a background Claude Code helper in the project or another folder with the configured Remote Control and the boot-written helper system prompt that a hand-built `claude --bg` omits, watches it until idle, and relays its report to the operator. Use when the operator says "spawn a helper", "spawn a new session", "run this in a background session", asks for a new session with `--model` or `--effort`, asks to switch a running helper's model or effort, or names `/spawn-session`.

02 资源文件 3

03 SKILL.md 正文

Spawn Session

Launch a background Claude Code session in a folder, subscribe to its idle notice, and relay the report through /hermitd:watch.

Usage

/hermitd:spawn-session <prompt-or-/skill> [--cwd <abs-dir>] [--worktree] [--name <n>] [--model <m>] [--effort <e>] [--background <abs-file>] [--task <id>] [--strict-mcp-config]
/hermitd:spawn-session --switch <n> --record <T-id> [--model <m>] [--effort <e>]

From <dir>, the launch folder (<abs>, the hermit project root, unless --cwd names another), that composes:

claude --bg --name <n> [--worktree <n>] [--permission-mode <p>] [--no-chrome] [--remote-control <n>] [--model <m>] [--effort <e>] [--mcp-config <dir>/.mcp.json] [--strict-mcp-config] --append-system-prompt-file <abs>/.hermit/state/helper-system-prompt.md '<prompt>'

--no-chrome is present on fresh launches when config.chrome is false (absent means false). Never add it to claude --bg --resume: resume must remain flagless to retain saved options.

--worktree <n> is present only when the operator passed --worktree, and gives the helper its own worktree of <dir> from the start. --remote-control <n> is present when config.json's remote is true and absent otherwise; --model and --effort only when the operator passed them. --mcp-config <dir>/.mcp.json and --strict-mcp-config only when the operator passed --strict-mcp-config: both flags when <dir>/.mcp.json exists, and --strict-mcp-config alone when it does not. Strict mode drops plugin and user-scope MCP servers from the helper. A background session cannot answer the project-server approval dialog, and a folder named by --cwd brings its own .mcp.json to that dialog. A helper report counts only when its sender's session name equals the target of a live peer-idle registry entry; the epoch-suffixed helper name is what distinguishes one spawn from a reused name. The helper never writes tasks/; the resident records progress and results after that sender check.

Six limits sit on that command:

  • The helper starts in <dir>. In a repository's main checkout, Claude Code refuses its Edit and Write until it calls EnterWorktree: the worktree.bgIsolation setting, "worktree" by default (https://code.claude.com/docs/en/settings-reference#worktree-bgisolation). "none" in that project's settings lets helpers edit the checkout directly, on whatever branch the resident has checked out; this skill never sets it. A linked worktree is edited in place under either value, and Bash git commands are not fenced by the setting. Outside a git repository the block still applies, so a non-git <dir> leaves the helper read-only.
  • Any file the helper must Read is passed as an absolute path in the prompt, spelled as an @<abs-path> mention so Claude Code injects it at launch; a worktree does not carry the hermit's untracked state.
  • <abs>/.hermit/state/helper-system-prompt.md is written at boot and gated like config.json. Pass it as --append-system-prompt-file (a system-prompt file is not expanded, so @<abs-path> mentions still belong in the prompt). The appended text reaches the helper's main conversation and forks, not its non-fork subagents.
  • --permission-mode <p> from config.json's permission_mode keeps the helper in this session's permission class, which is what lets its idle notice reach here rather than being held for an operator who is not watching. config.json accepts one value the CLI has no choice for, default, so it and null and an absent key all mean: leave the flag off entirely and let the helper take the box default. scripts/hermitd-start.ts resolves default and null the same way; match that rather than inventing a second answer. It does not agree on an absent key, which it reads as auto rather than as no flag. bypassPermissions is the one value that does not pass through: it becomes --permission-mode auto because a helper has no approval surface of its own, and auto is the only mode that stays unattended behind a gate.
  • The prompt is one single-quoted argument. An apostrophe in it ends the quote, so replace every ' with '\'' before composing. Anything after the closing quote is a second command the operator never asked for.
  • The launch is not pre-approved, and what the operator sees depends on the mode they run in. On auto, the shipped default, the classifier decides and no prompt reaches them on any channel. On acceptEdits or manual the native approval is relayed to their DM and is allow-once, so every spawn asks again. On bypassPermissions there is none. Say what is about to be spawned before running it either way: it is the only thing that makes the launch legible when an approval does arrive, and the only record when none does.

Use only the launch options documented here. Never add bypass flags, tool preapprovals, or settings overrides to widen the helper's permissions. If launch or execution is blocked, report the blocker; do not retry through a script, alternate invocation, or weaker permission mode. The exceptions are § Follow-ups, "Resume with new instructions", for an operator follow-up, § Switch a running helper for a model or effort switch, and untrusted-folder.md for a refused workspace.

Commands

  • helper-switch: bun ${CLAUDE_PLUGIN_ROOT}/scripts/helper-switch.ts
  • trust-workspace: bun ${CLAUDE_PLUGIN_ROOT}/scripts/trust-workspace.ts
  • task-note: bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts note .hermit
  • task-list: bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts list .hermit

Plan

  1. Parse --cwd, --worktree, --name, --model, --effort, --background, --task, and --strict-mcp-config from the invocation. --cwd must be an absolute path to an existing directory. --background must name an existing absolute file; append it to the prompt as an @<abs-path> mention. --task <id> is resolved in step 2 before any launch. Remaining text is the prompt. Empty prompt: stop with a one-line ask for the work to run.

    • When --name is omitted, derive <n> from the prompt: drop a leading / and any <plugin>: namespace, lowercase, replace every non-[a-z0-9] run with -, keep the first five nonempty tokens joined by -, cap the slug at 40 characters, trim any leading or trailing -, then append - plus the full epoch (date +%s). The trim is what keeps a prompt like #220 fix the parser from producing a name the launch command reads as a flag. The epoch is not truncated because the watch registry keys on the full name. With --worktree it also matters because claude --worktree <n> silently reuses an existing .claude/worktrees/<n>, its branch and uncommitted state included, so a repeated name is a wrong-branch start with no error. If no token survives the slug is session, which is what makes the fallback session-<epoch>. Example: /tackle-issue PROP #220 becomes tackle-issue-prop-220-1788889689.
    • <m> / <e> are omitted when the operator does not name them, so the helper takes the box defaults.
  2. Resolve <abs> with git rev-parse --show-toplevel rather than reading the Bash tool's working directory, which persists across calls and can sit in a subdirectory. <dir> is --cwd when given, else <abs>. No further check on <dir>: a folder the CLI refuses is reported with the CLI's own message, except for the untrusted-folder branch in step 3. With --worktree, run git -C <dir> rev-parse --verify HEAD; on failure, refuse with one line before composing any launch: <dir> is not a git repository or has no commits; make an initial commit, then retry. An unborn HEAD makes the background launch report success and then crash-loop on worktree creation. When --task was passed, validate <id> with task.ts list <abs>/.hermit --id <id> --json; refuse a missing or closed record before launch. Append <abs>/.hermit/tasks/<id>.md to the helper prompt as an absolute background path. Read <p> from <abs>/.hermit/config.json (permission_mode), dropping the flag for default, null or an absent key, mapping bypassPermissions to auto (Six limits), and passing every other value through unchanged. Read remote from the same config and include --remote-control <n> only when the key is present and true; false, null and an absent key all leave the flag off, which is the resident session's own answer for that config. When the operator passed --strict-mcp-config, include --mcp-config <dir>/.mcp.json --strict-mcp-config if <dir>/.mcp.json exists, and --strict-mcp-config alone if it does not. Check that <abs>/.hermit/state/helper-system-prompt.md exists; boot writes it. If it is missing, refuse with one line: restart with hermitd-start so boot writes the helper system-prompt file.

  3. cd <dir> as its own Bash call, then run the command in Usage as the next one, so the launch stands alone in the transcript and in any approval that does reach the operator. With --cwd, then cd <abs> as the next call whatever the launch returned: the task calls below resolve the relative .hermit against the Bash tool's persisted directory. Print the returned bg id and claude logs <id>, claude attach <id>, claude stop <id> hints. For a Workspace not trusted. Run refusal, follow untrusted-folder.md. Home-directory and unresolvable refusals are reported without that branch. If the spawn is declined or fails, stop; do not watch, open a record, or patch a Decision. After every successful launch, read the full session id <sid> once:

    claude agents --json | jq -r --arg id <bg-id> '.[] | select(.id==$id) | .sessionId'
    

    The printed bg id is only the first 8 characters of <sid> and is not a resume handle. An empty result means no <sid>: say so, write the lines below without (<sid>), and continue to step 4. Without --task, inside an open record's turn, append Handed to helper <n> (<sid>). as a progress note by piping it into bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts note .hermit <T-id>, where <T-id> is that open record's id, not the bg id (the note is timestamped for you). After the handoff note, bind the record with bun ${CLAUDE_PLUGIN_ROOT}/scripts/task.ts note .hermit <T-id> --owner helper:<n>. On invalid_owner (a custom name outside [A-Za-z0-9._-]{1,64}) or owner_busy, leave it resident-owned and tell the operator why. With --task <id>, open or reuse a helper-work task with --dedupe-key "helper:<task-id>", requester from the current authorized turn, and a title naming the helper work. This record holds the helper's report, not the proposal's implementation result. Note Handed to helper <n> (<sid>); helper record <helper-task-id>. on the source task, and bind the helper-work record with --owner helper:<n>. A repeated launch for the same source task reuses this helper record.

  4. Invoke /hermitd:watch session <n> "<first 40 chars of the operator prompt>" --id <bg-id>. When the watch reports the helper blocked on its waitingFor, add nothing: the watch's helper poll announces it in chat. Whenever step 3 bound or opened a record, also pass --record <T-id>. That skill owns the subscription (watch/session-watch.md § Starting a session watch) and the idle-notice relay (watch/notices.md § Handling idle notices); do not re-implement either. When it declines the subscription, pass on the reason it gives rather than asserting one: the helper may still be running, or it may have finished its first turn before the subscription landed. Either way, give the claude logs <id> id as the way to check on it. When the decline is No session named <n> is reachable from here and the launch printed a bg id, run claude logs <id> and show the operator the last few ANSI-stripped lines; name the state those lines show (a boot dialog it cannot answer, a crash, a finished turn) rather than one they do not. A declined subscription writes no registry entry, so no report can ever reach the record opened in step 3: cancel it with the decline reason (task.ts cancel .hermit <T-id> --actor <requester> --reason-stdin) and say whether the tail leaves the helper running unwatched or stuck, rather than leaving a commitment waiting on a report that cannot arrive.

Follow-ups

Send follow-ups by SendMessage with notify_when_idle: true, then read watch § Registering an existing idle subscription with the sent text. The watch relay retries a blocked or declined follow-up once with that stored request.

Resume with new instructions

Inputs: helper name, instructions, watch note and flags, and optional blocked-reply input from watch's helper poll handler. Return the outcome to the caller so it can handle a resume that could not start.

  1. Read the helper's row in claude agents --json. Ready means status is idle and state is neither working, blocked nor scheduled. With the blocked-reply input, also accept status waiting with state blocked: a helper stuck on a prompt or question, which a message cannot reach. Take its id as the bg id and its sessionId as the resume handle. Not ready or no row: end without resuming; never stop a busy helper.
  2. Run claude stop <bg-id>, then in one bounded Bash call poll claude agents --json every 2s for up to 30s until no row has that id. Still listed at the deadline: end without resuming and report that outcome. Then call TaskStop on the old watch entry's poll_task_id when present; an error means it already exited.
  3. From the project root, run claude --bg --resume <sessionId> '<instructions>' with no other flag. Apply the launch limits' '\'' quoting rule to every apostrophe in the instructions. The saved options include the helper system prompt; extra flags start a copy instead. Launch failure or any output other than a normal resume (for example "started a copy"): do not try again; tell the operator what it printed and end with that outcome.
  4. Remove the helper's old watch entry if still live and write the registry, then invoke /hermitd:watch session <name> "<note>" --id <bg-id> plus the given flags. Use the resumed session's bg id (the first 8 characters of its session id). Do not store these prompt instructions as followup: they were delivered by resume, not by message. If the watch declines, return that the resumed helper runs unwatched; a launched resume counts as reached even so.

Switch a running helper

For --switch, Read switch.md and follow that procedure instead of the launch Plan. It preserves the helper's conversation, name and thread binding.

Stuck helper

claude logs <id>, claude stop <id>, and the watch expiry notice. Never tmux. Idle is not stuck: watch's idle-notice relay leaves an idle helper running, except for § Follow-ups, "Resume with new instructions", and § Switch a running helper. A boot dialog in claude logs <id> is handled as in this terminal: relaunch with the option that makes the dialog moot when one exists, otherwise give the operator claude attach <id> (for a hermit in Docker, docker exec -it <container> claude attach <id>), because Remote Control is not connected before boot.

04 同仓库更多 Skills