Skip to content

Team & Swarm

Beyond simple sub-agent spawning and the coordinator pattern, Claude Code implements a full team/swarm system — persistent multi-agent teams where members run concurrently with different backend strategies. This is the most complex multi-agent pattern in the codebase.

A team is a persistent configuration stored as a JSON file, with members that can be spawned across different execution backends:

graph TD
L[Leader Agent] --> TC[TeamContext in AppState]
TC --> M1[Member: researcher<br/>InProcess Backend]
TC --> M2[Member: implementer<br/>Tmux Backend]
TC --> M3[Member: reviewer<br/>iTerm2 Backend]
M1 ---|AsyncLocalStorage| P1[Same Node.js process]
M2 ---|tmux session| P2[Separate terminal pane]
M3 ---|iTerm2 tab| P3[Separate iTerm2 tab]

Teams are persisted as JSON files managed by src/utils/swarm/teamHelpers.ts:

// TeamFile structure (from teamHelpers.ts)
type TeamFile = {
name: string
description: string
members: TeamMember[]
}
type TeamMember = {
agentId: string // Format: "name@team" (e.g., "researcher@project-team")
name: string
agentType: string
model?: string
prompt: string
color?: string
planModeRequired: boolean
worktreePath?: string // Git worktree for isolation
sessionId?: string
subscriptions?: string[]
backendType: 'tmux' | 'iterm2' | 'in_process'
isActive: boolean
mode?: string
}

The agentId follows a deterministic format name@team, generated by formatAgentId():

src/utils/agentId.ts
export function formatAgentId(name: string, teamName: string): string {
return `${name}@${teamName}`
}

The in-process backend runs teammates within the same Node.js process using AsyncLocalStorage for context isolation:

src/utils/swarm/spawnInProcess.ts
export async function spawnInProcessTeammate(
config: InProcessSpawnConfig,
context: SpawnContext,
): Promise<InProcessSpawnOutput> {
const agentId = formatAgentId(name, teamName)
const taskId = generateTaskId('in_process_teammate')
// Independent AbortController — not linked to leader's query
const abortController = createAbortController()
// Create teammate context for AsyncLocalStorage
const teammateContext = createTeammateContext({
agentId, agentName: name, teamName, color,
planModeRequired, parentSessionId, abortController,
})
// Create and register task state in AppState
const taskState: InProcessTeammateTaskState = {
...createTaskStateBase(taskId, 'in_process_teammate', description),
type: 'in_process_teammate',
status: 'running',
identity,
prompt,
model,
abortController,
awaitingPlanApproval: false,
permissionMode: planModeRequired ? 'plan' : 'default',
isIdle: false,
shutdownRequested: false,
messages: [],
// ...
}
registerTask(taskState, setAppState)
return { success: true, agentId, taskId, abortController, teammateContext }
}

The Tmux backend spawns teammates as separate terminal sessions in tmux panes. Each teammate runs as an independent Claude Code process in its own pane.

Similar to Tmux but uses iTerm2’s native tab/pane management on macOS, providing a more integrated experience for iTerm2 users.

src/utils/swarm/spawnInProcess.ts
export type InProcessSpawnConfig = {
name: string // Display name, e.g., "researcher"
teamName: string // Team this member belongs to
prompt: string // Initial task/instructions
color?: string // UI color for the teammate
planModeRequired: boolean // Must enter plan mode before implementing
model?: string // Optional model override
}

Each teammate carries a TeammateIdentity that persists across its lifecycle:

// Stored as plain data in AppState
const identity: TeammateIdentity = {
agentId, // "researcher@project-team"
agentName: name, // "researcher"
teamName, // "project-team"
color,
planModeRequired,
parentSessionId, // Links back to the leader's session
}

The killInProcessTeammate() function handles graceful shutdown:

src/utils/swarm/spawnInProcess.ts
export function killInProcessTeammate(
taskId: string,
setAppState: SetAppStateFn,
): boolean {
setAppState((prev: AppState) => {
const task = prev.tasks[taskId] as InProcessTeammateTaskState
if (!task || task.status !== 'running') return prev
// 1. Abort the controller to stop execution
task.abortController?.abort()
// 2. Call cleanup handler
task.unregisterCleanup?.()
// 3. Call pending idle callbacks to unblock waiters
task.onIdleCallbacks?.forEach(cb => cb())
// 4. Remove from teamContext.teammates
const { [agentId]: _, ...remainingTeammates } = prev.teamContext.teammates
return {
...prev,
teamContext: { ...prev.teamContext, teammates: remainingTeammates },
tasks: {
...prev.tasks,
[taskId]: {
...task,
status: 'killed',
endTime: Date.now(),
messages: task.messages?.length
? [task.messages[task.messages.length - 1]!] // Keep only last message
: undefined,
abortController: undefined, // Release references
unregisterCleanup: undefined,
currentWorkAbortController: undefined,
},
},
}
})
// Post-state cleanup
if (teamName && agentId) {
removeMemberByAgentId(teamName, agentId) // Remove from team file
}
evictTaskOutput(taskId) // Clean up disk output
emitTaskTerminatedSdk(taskId, 'stopped') // SDK notification
unregisterPerfettoAgent(agentId) // Release trace entry
return killed
}

When teams are disbanded or sessions end, comprehensive cleanup runs:

flowchart TD
A[cleanupTeamDirectories] --> B[Read team config.json]
B --> C{For each member}
C --> D[destroyWorktree]
C --> E[Remove task directories]
D --> F{Has .git file?}
F -->|yes| G[git worktree remove --force]
F -->|no| H[rm -rf worktree path]
C --> I[Delete team config file]

Worktree destruction (destroyWorktree in teamHelpers.ts) is particularly careful — it reads the .git file to find the main repository, then uses git worktree remove --force. If that fails, it falls back to rm -rf.

Team members are registered in Perfetto traces for hierarchy visualization:

// During spawn
if (isPerfettoTracingEnabled()) {
registerPerfettoAgent(agentId, name, parentSessionId)
}
// During kill
unregisterPerfettoAgent(agentId)

This enables viewing the entire team hierarchy — leader and all teammates — in performance trace tooling.

The team system integrates deeply with Claude Code’s reactive state:

  • teamContext on AppState holds the live team configuration and teammate map
  • Each teammate’s InProcessTeammateTaskState is stored in AppState.tasks
  • The InProcessTeammateTask React component (Task #14) drives the actual agent execution loop
  • State changes trigger UI updates showing teammate status, spinner verbs, and tool counts

The state machine for a teammate:

spawned → running → idle ↔ running → completed/killed
↑ |
└─ pending user messages

Teammates alternate between running (actively executing tools) and idle (waiting for instructions), with the leader able to send messages to idle teammates via SendMessage.