Claude Code can spawn agents, and those agents can be grouped into a team where each member has its own pane and its own worktree. Once you know it works, the question becomes: how do you avoid forgetting one of the half-dozen flags every time you spawn a team at 2am?
nf-agents is the skill I wrote to answer that. It does not invent any new capability. It collapses the recipe into five named modes, runs a pre-flight check before any spawn, and hard-codes the patterns I kept reaching for.
The problem the skill solves
Without a skill, “spawn a team” is roughly seven decisions you make every time:
- Use a single
Agentcall, orTeamCreateplus severalAgentcalls? - Pass
team_nameto each agent (or accidentally forget and end up with isolated agents that cannot message each other)? - Pass
isolation: "worktree"(or accidentally let an agent edit your live working copy)? - Are you inside
tmux? Are you in iTerm2 withit2installed? Neither? Spawn fails differently in each case. - Is
teammateModeset to a split-pane backend, or toin-process(which silently changes everything)? - Is the
WorktreeCreatehook present so worktrees are based on your currentHEADand notorigin/main? - What are you actually asking each teammate to do? Standby and wait, or execute a brief autonomously?
Get any one of these wrong and you discover it five minutes later: panes that look right but cannot send messages to each other, an agent that mutated your live branch, a worktree that was based on stale main and is now ten commits behind your work.
The skill turns those seven decisions into one: pick a mode, answer the questions the skill prompts you with, done.
The five modes
| Mode | Use when |
|---|---|
standby | You have several parallel domains in flight but you do not yet know each task. Spawn a team, give each member a role, drive them interactively. |
tasks | You know exactly what each member should do. Spawn pre-briefed members that execute autonomously and report back when done. |
solo | One task, no parallelism, but you still want a worktree so the agent can commit and push without touching your working copy. |
status | Inspect the current team. Member names, busy/idle, recent updates. |
teardown | Shut the team down. Sends a cooperative shutdown signal, calls TeamDelete, surfaces any zombies for manual cleanup. |
Standby is the default. The model that fits my brain is “spawn the team, then think about what each member should work on first.”
Pre-flight checks (run on every spawn)
Each of these maps to a specific past failure:
# 1. teammateMode must enable split-pane mode (NOT "in-process")
MODE=$(jq -r '.teammateMode // "auto"' ~/.claude/settings.json)
[ "$MODE" = "in-process" ] && echo "FAIL: set teammateMode to 'tmux' or 'auto' first"
# 2. Split-pane backend available - ONE of these must hold:
# a. inside tmux session -> tmux split backend
# b. iTerm2 + it2 CLI >= 3.6.6 -> iTerm2 native split backend
if [ -n "${TMUX:-}" ]; then
echo "OK: tmux backend"
elif [ "${TERM_PROGRAM:-}" = "iTerm.app" ] && command -v it2 >/dev/null; then
echo "OK: iTerm2 backend"
else
echo "FAIL: no split-pane backend"
fi
# 3. WorktreeCreate hook present (so worktrees base on current HEAD)
jq -e '.hooks.WorktreeCreate' ~/.claude/settings.json >/dev/null \
|| echo "WARN: WorktreeCreate hook missing"
# 4. Current cwd is a git repo (required for isolation: "worktree")
git rev-parse --git-dir >/dev/null 2>&1 || echo "FAIL: not in a git repo"
Any failure is surfaced before the spawn is attempted. The skill does not auto-fix the environment, terminal launch context is the user’s responsibility.
A first run, end to end
You type:
/nf-agents standby
The skill asks (one batch, four questions):
- Team name. Suggested kebab-case (
worker-team). - Members. Names plus a one-line role each (
api-worker: handles incoming webhooks,feed-worker: handles RSS ingestion). - Target branch. Defaults to your current branch.
- Backend. iTerm2 split (default, lighter) or tmux session (persists if you close the terminal).
It then runs the pre-flight, calls TeamCreate, and for each member spawns:
Agent({
name: "<member-name>",
team_name: "<team-name>",
subagent_type: "fullstack-engineer",
isolation: "worktree",
prompt: "<standby brief>"
})
The standby brief is templated. It tells the member: do not start any code work, wait for the user to type into your pane or for SendMessage from the team lead, when you finish a task call TaskUpdate(status: "completed") and SendMessage the lead a one-paragraph summary, open PRs against the target branch, never auto-merge, never push directly to the target branch.
Acknowledge in two lines, then idle. The lead returns to idle. You start driving members.
The rest of this post walks through the four design decisions inside the skill that took the longest to get right and explains the incidents that drove each one.
Decision 1: Backend choice (tmux vs iTerm2 split)
When Claude Code spawns a team, every member needs a pane. The pane has to live somewhere, and where that “somewhere” is depends on the environment when you launched claude, not on a setting you can flip mid-session.
The decision tree is small but easy to misread:
Environment when claude was launched | Backend chosen |
|---|---|
TMUX env var is set (claude was launched inside tmux) | tmux split-window in the same tmux session |
TERM_PROGRAM=iTerm.app, it2 CLI present, TMUX unset | iTerm2 native split (separate panes in iTerm2) |
| Neither | Spawn fails, or falls through to in-process if teammateMode: auto |
The setting teammateMode: tmux is misleading: it does not force tmux. It means “any split-pane backend, do not fall back to in-process.” The binary auto-detects which split backend to use.
The persistence question is the real one
The reason backend choice matters is not aesthetics, it is whether the team survives a closed terminal:
| Backend | Close terminal, what survives |
|---|---|
| iTerm2 split | All claude PTYs are killed. Team teardown cascades. Individual transcripts persist on disk under ~/.claude/projects/.../<session-id>.jsonl and you can claude --resume <id> to pick up a single session, but the team registry and the SendMessage graph between members are gone. |
| tmux session | tmux server keeps running detached. All claude processes alive. tmux attach -t <name> reconnects exactly where you left off. Survives terminal close, even SSH disconnect. |
The skill asks about this before spawning:
Backend: do you want iTerm2 split (default, lighter, no persistence) or tmux session (persist, attach from another machine or mobile)?
If you pick tmux but claude was not launched inside tmux, the skill aborts and tells you to tmux new -s <name> first, then re-run claude inside it. It does not auto-fix the environment because the launch context is the user’s responsibility, and silently re-launching claude would orphan the current conversation.
When I pick which
- Daily work, single machine, a few hours: iTerm2 split. Lighter, and I do not need to attach from elsewhere.
- I might close the lid and come back tomorrow, or I want to peek at the team from my phone over SSH: tmux.
- Long-running team (more than a day): tmux, no question.
- One parallel task, do not need to watch live output: standalone
Agent, no team, the backend question does not apply.
Decision 2: Worktree base branch (and why a hook had to override the default)
isolation: "worktree" tells Claude Code to create a temporary git worktree for each agent. The agent commits, pushes, and PRs from inside the worktree, leaving your live working copy untouched.
The thing that took an incident to learn: by default, Claude Code bases the agent’s worktree on origin/<default-branch> (typically origin/main), not on your current HEAD.
That sounds reasonable until you actually use it. Imagine the workflow:
- You are on a feature branch with eight commits of in-progress work.
- You want an agent to add a small refactor to that feature.
- You spawn the agent with
isolation: "worktree". - The worktree is created from
origin/main, not from your current branch. - The agent does the refactor, commits, pushes, opens a PR.
- You merge the PR back into your feature branch.
- The merge brings in the refactor, plus a complete reversion of your eight in-progress commits, because the worktree never knew they existed.
The fix is a WorktreeCreate hook in ~/.claude/settings.json that intercepts the worktree creation command and rewrites it to base on the parent session’s current HEAD instead. Roughly:
git worktree add --no-track -b worktree/<slug> <path> HEAD
The --no-track is deliberate: the worktree branch should not silently track any upstream. The first push from the agent must use git push -u origin worktree/<slug> to set its upstream explicitly, after which subsequent pushes are plain git push.
The skill checks for the hook on every spawn:
jq -e '.hooks.WorktreeCreate' ~/.claude/settings.json >/dev/null \
|| echo "WARN: WorktreeCreate hook missing - worktrees will base on origin/<default-branch>"
If the hook is missing, the skill warns but does not block. (Some users genuinely want the default behavior, particularly if they rebase their work onto main constantly.) For my workflow the hook is required, so the warning is also a reminder to re-verify after every Claude Code version upgrade, since the binary occasionally changes its hook resolution.
The agent’s commit and PR contract
Once the worktree is on the right branch, the agent’s contract is small:
- Auto-commit and auto-push are allowed, follow the team’s commit format.
- First push uses
-u origin worktree/<slug>. Subsequent pushes are plain. - PRs go back to the parent branch the worktree was based on, not to
main. - Never auto-merge. Never push directly to the parent branch.
- After PR is open, the agent’s job is done. The user reviews and merges.
The skill bakes all of this into the standby and tasks prompt templates, so every spawned member starts with this contract loaded.
Decision 3: Standby vs tasks (when to use which)
Both modes spawn a team. The difference is who is driving:
Standby is for “I have several domains and I will figure out each task as I go.” Members get a role, an idle protocol, and wait. You then either:
- Type directly into a member’s pane (works for tmux and iTerm2 splits, the model behind that pane is a real Claude session you can talk to).
- Send a
SendMessagefrom the lead pane:SendMessage({to: "api-worker", message: "Write a unit test for the webhook signature validator..."}).
Tasks is for “I know exactly what each member should do.” The brief is in the spawn prompt. The agent reads it, executes it, calls TaskUpdate(status: "completed") plus SendMessage(to: "team-lead", message: "<summary + PR url>") when done.
The split matters because the brief is shaped differently. Standby briefs are about identity and protocol (“you are api-worker, your role is incoming webhooks, wait for direction”). Task briefs are about scope and acceptance (“read these files, change this behavior, here are the acceptance criteria, here is the reporting protocol”).
Vague task briefs are the most common failure I see. The agent ends up asking the user clarifying questions mid-execution, which defeats the point of autonomy. A good task brief includes:
- Scope: one paragraph, “what to change.”
- Acceptance criteria: bullet list, “the change is done when X, Y, Z.”
- Pointers: file paths, doc links, memory file names the agent should read first.
- Reporting protocol:
TaskUpdateplusSendMessageon done, PR target branch.
If you would not give the brief to a junior engineer expecting them to deliver without asking, do not give it to an agent.
Decision 4: The cooperative shutdown protocol
When you are done with a team you call teardown. The skill’s job is to:
- Send
SendMessage({type: "shutdown_request", reason: "..."})to each member, once. - Wait roughly ten seconds.
- Call
TeamDelete({team_name})to free the registry name. - Surface any zombies and tell the user how to clean them up by hand.
The key word is cooperative. A teammate has to actively call SendMessage({type: "shutdown_response", approve: true}) to terminate. If it only writes “Acknowledged, shutting down” as plain text without calling the tool, its process keeps running. The pane is a zombie: the worktree the team registry pointed it at has been deleted by TeamDelete, so the zombie cannot do useful work, but the OS process is still resident.
This is the part of the skill that has the most stern wording, because it is the part most likely to leave you with leaked processes:
- Do not retry the same
shutdown_request. A misunderstanding ten seconds ago will be the same misunderstanding ten seconds from now. - Tell the user explicitly which member did not approve. Naming the zombie makes it actionable.
- Suggest manual cleanup in this order: click the iTerm2 pane and
Cmd+W, focus the tmux pane andCtrl-B x, orps aux | grep claudeand kill the PID withkill(thenkill -9only if SIGTERM is ignored). - Do not try to auto-kill. The lead does not have the teammate’s PID and does not manage iTerm2 or tmux directly. Pretending it can leads to “I killed something but I am not sure what.”
The escape hatch
If multiple zombies refuse to approve and Cmd+W-ing each pane is tedious, there is a one-action nuclear option: type /exit in the lead pane.
Claude Code shows a dialog, “Background work is running, Exit anyway / Stay.” Pick Exit anyway. The lead terminates, a cascade signal kills every member, every pane closes, and in iTerm2 the entire window or tab closes too.
The cost is that the lead session is gone. The transcript persists at ~/.claude/projects/.../<session-id>.jsonl and you can claude --continue later, but the live conversation context is lost.
I recommend the escape hatch only when there is no further valuable context in the lead session anyway. If the lead has been planning something complex, do not nuke it for the sake of zombie cleanup, do the manual Cmd+W walk.
Mistakes the skill exists to prevent
These are the patterns that cost me time before the skill existed. The skill encodes a check or a default for each:
- Spawning
Agentwithoutteam_namewhen you wanted a team. The result looks like an isolated background agent: it cannotSendMessageto anyone. The skill always asks “team or solo” up front so this is impossible. - Manual
tmux split-windowplusclaude --continuein another pane. The panes look like a team, but they have no shared messaging registry, they are just independent sessions. The skill always usesAgent + team_nameso the registry exists. - Forgetting
isolation: "worktree"for code-editing teammates. Their work mutates your live working copy directly. The skill defaultsisolation: "worktree"for any agent that will write files. - Vague task briefs. Covered above. The skill’s templates include the structure I have learned to write.
- Auto-merging the agent’s branch. Never do this. The skill’s prompt template is explicit: PR only, never auto-merge.
What you should keep even if you never use my skill
Four patterns generalize cleanly:
- Pre-flight checks before any irreversible action. Spawning a team that lacks split-pane backend ends with you killing eight processes by hand. Four
bashlines at the top of the skill removes the entire class of failure. - Named modes instead of free-form arguments. “Spawn a team for me” is ambiguous.
standby,tasks,soloare not. The skill’s user (you, six hours later) does not have to remember which arguments do what. - A
WorktreeCreatehook that bases agent worktrees on your currentHEAD. Re-verify after every Claude Code upgrade. - A reporting protocol baked into every spawn prompt.
TaskUpdateplusSendMessageon completion, PR not auto-merge, never push to parent branch directly.
The skill itself is just one way to encode those, and the encoding is the cheap part. The patterns are the value.
Get the skill
White-labeled, ready to drop into ~/.claude/skills/. Published in the claude-skills-toolkit repo.
# Option 1: download the latest release zip
curl -L https://github.com/llawliet11/claude-skills-toolkit/releases/latest/download/nf-agents.zip -o nf-agents.zip
unzip nf-agents.zip -d ~/.claude/skills/
# Option 2: clone the repo
git clone https://github.com/llawliet11/claude-skills-toolkit.git
cp -r claude-skills-toolkit/nf-agents ~/.claude/skills/
Verify in a new Claude Code session: type /nf-agents and the skill should appear in the slash-command list. The skill’s own README.md covers required environment, the WorktreeCreate hook, and customization.
If you adopt it and find a failure mode I have not yet hit, open an issue on the repo or let me know.