Introduction
Claude Code (v2.1.47) ships with an experimental feature called Agent Teams: multiple Claude Code sessions coordinate on shared work through a lead-and-teammates topology. I've been building Hive, a multi-agent coding orchestrator with similar goals but a very different architecture, so I wanted to understand how Anthropic's approach works under the hood.
This post documents what I found through:
- Reading the official documentation
- Examining actual artifacts left on disk by previous team sessions
- Letting Claude analyze the Claude Code binary (v2.1.47) for implementation details (hah!)
Table of Contents
- 1. Architecture Overview
- 2. The Shared Task List
- 3. Inter-Agent Communication
- 4. Agent Spawning and Lifecycle
- 5. Quality Gates and Hooks
- 6. Token Economics
- 7. Architecture Summary
- Sources
1. Architecture Overview
An agent team consists of four components:
| Component | Role |
|---|---|
| Team lead | The main Claude Code session that creates the team, spawns teammates |
| Teammates | Separate Claude Code instances, each with its own context window |
| Task list | Shared work items stored as individual JSON files on disk |
| Mailbox | Per-agent inbox files for message delivery |
The entire coordination layer is file-based. The filesystem at ~/.claude/ is the sole coordination substrate:
~/.claude/
├── teams/{team-name}/
│ ├── config.json # team membership registry
│ └── inboxes/{agent-name}.json # per-agent mailbox
└── tasks/{team-name}/
├── .lock # flock() for concurrent task claiming
├── .highwatermark # auto-increment counter
├── 1.json # individual task files
├── 2.json
└── ...
This is a fundamentally decentralized design. The lead is just another Claude session with extra tools (TeamCreate, TeamDelete, SendMessage). There is no background process. Coordination emerges from shared file access.
In an active session, if you ask Claude to spin up a team to do some kind of task and then run the following in another window, you can observe the filesystem update in real time.
watch -n 0.5 'tree ~/.claude/teams/ 2>/dev/null; echo "---"; tree ~/.claude/tasks/ 2>/dev/null'
For example, with the following prompt:
can you spanw an agent team to examine this code base?
- have one look for bugs
- have one look for complexity
- have one look for good things to call out and play devil's advocate against the other two agents
I observed this:
teams
└── code-review
├── config.json
└── inboxes
├── bug-hunter.json
├── complexity-analyst.json
├── devils-advocate.json
└── team-lead.json
Team Config
The team config at ~/.claude/teams/{team-name}/config.json contains a members array that teammates read to discover each other:
{
"members": [
{ "name": "team-lead", "agentId": "abc-123", "agentType": "leader" },
{
"name": "researcher",
"agentId": "def-456",
"agentType": "general-purpose"
}
]
}
Names are the primary addressing mechanism (UUIDs exist but aren't used for routing). All messaging and task assignment uses the name field.
2. The Shared Task List
File Format
Each task is stored as an individual JSON file in ~/.claude/tasks/{team-name}/. Here's a real example from a previous session:
{
"id": "1",
"subject": "Hunt for bugs across the codebase",
"description": "...",
"activeForm": "Hunting for bugs",
"owner": "bug-hunter",
"status": "completed",
"blocks": [],
"blockedBy": []
}
Task schema:
| Field | Type | Description |
|---|---|---|
id |
string | Numeric ID, auto-incremented via .highwatermark |
subject |
string | Imperative-form title (e.g., "Run tests") |
description |
string | Detailed requirements and acceptance criteria |
activeForm |
string | Present-continuous form for spinner display ("Running tests") |
status |
string | pending → in_progress → completed (or deleted) |
blocks |
string[] | Task IDs that this task blocks |
blockedBy |
string[] | Task IDs that must complete before this task can start |
Concurrency Control
Two special files provide coordination:
.lock: A 0-byte file used for filesystem-level mutual exclusion (flock()). Present in all 42 task directories observed on my machine..highwatermark: Contains a single integer (e.g.,"3","13"). The next available task ID for auto-incrementing.
Task Claiming
Task claiming uses file locking to prevent race conditions. Teammates prefer lowest-ID-first ordering. A task with a non-empty blockedBy array cannot be claimed until all blocking tasks are in a terminal state.
Observation: Most Task Directories Are Empty
Of 42 task directories on my machine, only 5 contained actual task JSON files. The remaining 37 had only .lock and .highwatermark. This likely means tasks are cleaned up after completion, or these were sessions where Claude used the internal task list (available since the task list feature launch) without decomposing into subtask files.
3. Inter-Agent Communication
Mailbox Pattern
Each agent has a JSON array file at ~/.claude/teams/{team-name}/inboxes/{agent-name}.json. Here's a real inbox from a previous session where a team-lead dispatched work to a controlplane-agent:
[
{
"from": "team-lead",
"text": "{\"type\":\"task_assignment\",\"taskId\":\"1\",\"subject\":\"Phase 2: Control-plane - remove participants/presence\",\"description\":\"Remove multiplayer code from the control-plane package...\",\"assignedBy\":\"team-lead\",\"timestamp\":\"2026-02-18T02:37:16.890Z\"}",
"timestamp": "2026-02-18T02:37:16.890Z",
"read": false
}
]
Note the JSON-in-JSON encoding: the text field is a JSON string containing a serialized message object. The outer envelope has from, text, timestamp, and read fields.
Message Types
The type field inside the text payload supports:
| Type | Direction | Purpose |
|---|---|---|
task_assignment |
lead → teammate | Assign a task with full details |
message |
any → any | Direct message to one recipient |
broadcast |
lead → all | Same message to every teammate |
shutdown_request |
lead → teammate | Request graceful shutdown |
shutdown_response |
teammate → lead | Approve or reject shutdown |
plan_approval_request |
teammate → lead | Submit plan for review |
plan_approval_response |
lead → teammate | Approve or reject with feedback |
idle_notification |
teammate → lead | Auto-sent when teammate's turn ends |
Delivery Mechanism
Write path: The sender appends a new entry to the recipient's inbox JSON array file.
Read path: The recipient polls their own inbox file. New messages are injected as synthetic conversation turns (they appear as if a user sent them).
Broadcast: Literally writes the same message to every teammate's inbox file. Token cost scales linearly with team size.
Communication is just file append + file read. Latency between send and receive depends on the recipient's poll interval.
Peer DM Visibility
When a teammate sends a DM to another teammate, a brief summary is included in the lead's idle notification. This gives the lead visibility into peer collaboration without the full message content.
4. Agent Spawning and Lifecycle
How Teammates Are Created
Each teammate is a separate claude CLI process. The lead spawns them via the Task tool with team_name and name parameters. Environment variables are set on the spawned process:
CLAUDE_CODE_TEAM_NAME: auto-set on spawned teammatesCLAUDE_CODE_PLAN_MODE_REQUIRED: set totrueif plan approval is required
Context Initialization
Teammates load the same project context as any fresh session:
CLAUDE.mdfiles from the working directory- MCP servers
- Skills
- The spawn prompt from the lead
The lead's conversation history does NOT carry over. Each teammate starts fresh with only the spawn prompt as context.
Internal Implementation
From binary analysis of Claude Code v2.1.47, the teammate context is managed via AsyncLocalStorage with these fields:
agentId,agentName,teamNameparentSessionId,colorplanModeRequired
Key internal functions:
isTeammate()/isTeamLead(): role detectionwaitForTeammatesToBecomeIdle(): synchronization primitive for the leadgetTeammateContext()/setDynamicTeamContext(): runtime context management
Idle Detection
After every LLM turn, a teammate automatically goes idle and sends an idle_notification to the lead. This is the normal resting state, rather than an error or staleness condition. Sending a message to an idle teammate wakes it (the next poll cycle picks up the inbox message).
Shutdown Protocol
- Lead sends
shutdown_requestto a teammate - Teammate can approve (exits gracefully) or reject (continues working with an explanation)
- Team cleanup via
TeamDeleteremoves~/.claude/teams/{team-name}/and~/.claude/tasks/{team-name}/ - Cleanup fails if any teammates are still active; they must be shut down first
Permission Inheritance
Teammates inherit the lead's permission mode at spawn time. If the lead runs --dangerously-skip-permissions, all teammates do too. Individual modes can be changed post-spawn but not configured per-teammate at spawn time.
5. Quality Gates and Hooks
Agent Teams integrates with Claude Code's hook system for quality enforcement:
TeammateIdle Hook
Fires when a teammate is about to go idle. Exit code 2 sends stderr as feedback and prevents idle, keeping the teammate working.
{
"hook_event_name": "TeammateIdle",
"teammate_name": "researcher",
"team_name": "my-project"
}
TaskCompleted Hook
Fires when a task is being marked complete. Exit code 2 prevents completion and feeds stderr back as feedback.
{
"hook_event_name": "TaskCompleted",
"task_id": "task-001",
"task_subject": "Implement user authentication",
"task_description": "Add login and signup endpoints",
"teammate_name": "implementer",
"team_name": "my-project"
}
This fires in two situations: (1) when any agent explicitly marks a task completed via TaskUpdate, or (2) when an agent team teammate finishes its turn with in-progress tasks.
Hook Handler Types
| Type | Description |
|---|---|
command |
Shell script. JSON on stdin, exit codes for decisions. |
prompt |
Single-turn LLM evaluation. Returns {ok, reason}. |
agent |
Multi-turn subagent with read tools. Up to 50 turns. |
6. Token Economics
Agent teams use approximately 7× more tokens than standard sessions when teammates run in plan mode. Each teammate maintains its own full context window as a separate Claude instance.
Baseline Reference
- Average Claude Code usage: ~$6/developer/day
- Agent teams: roughly proportional to team size on top of baseline
7. Architecture Summary
| Dimension | Claude Code Agent Teams |
|---|---|
| Coordination substrate | Flat files (~/.claude/tasks/, ~/.claude/teams/) |
| Task format | One JSON file per task + .lock for claiming |
| Messaging | JSON inbox files (append + poll) |
| Agent lifecycle | Self-managing CLI processes |
| Work isolation | Shared working directory |
| Merge strategy | None (agents edit files directly) |
| Retry/escalation | Manual (lead decides, or user intervenes) |
| Topology | Lead + flat peers, peer-to-peer messaging |
| Scheduling | Self-claim (teammates grab next task) |
| State durability | Files only; no in-process teammate resumption |
| Quality gates | Shell hooks (TeammateIdle, TaskCompleted) |
| Token tracking | Per-session only, no cross-agent aggregation |
| Stall detection | Manual (user notices teammate stopped) |
| Concurrency control | Implicit (team size = teammate count) |
| Dependency model | blocks/blockedBy on task files |
Sources
Official Documentation
- Teams of Claude Code sessions: Architecture
- Interactive mode — Task list
- Agent teams — Assign and claim tasks
- Agent teams — Context and communication
- Agent teams — Shut down teammates
- Agent teams — Clean up the team
- Agent teams — Permissions
- Agent teams — Limitations
- Agent teams — Best practices
- Hooks
- Costs — Agent team token costs
- Settings
- Sub-agents
On-Disk Artifacts (Claude Code v2.1.47)
Observed at /Users/tau/.claude/:
- Team directories with
config.jsonandinboxes/{agent-name}.jsonfiles - Task directories with
.lock,.highwatermark, and individual task JSON files - Sample task assignment message from
team-leadtocp-agent, timestamped2026-02-18T02:37:16.890Z
Binary Analysis
Claude Code binary v2.1.47. Internal functions identified via string analysis: getTeamName, getAgentName, getAgentId, isTeammate, isTeamLead, waitForTeammatesToBecomeIdle, getTeammateContext, setDynamicTeamContext, createTeammateContext. AsyncLocalStorage context fields: agentId, agentName, teamName, parentSessionId, color, planModeRequired.