Reverse-Engineering Claude Code Agent Teams: Architecture and Protocol

Banner for Reverse-Engineering Claude Code Agent Teams: Architecture and Protocol

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:

  1. Reading the official documentation
  2. Examining actual artifacts left on disk by previous team sessions
  3. Letting Claude analyze the Claude Code binary (v2.1.47) for implementation details (hah!)

Table of Contents

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 pendingin_progresscompleted (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 teammates
  • CLAUDE_CODE_PLAN_MODE_REQUIRED: set to true if plan approval is required

Context Initialization

Teammates load the same project context as any fresh session:

  • CLAUDE.md files 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, teamName
  • parentSessionId, color
  • planModeRequired

Key internal functions:

  • isTeammate() / isTeamLead(): role detection
  • waitForTeammatesToBecomeIdle(): synchronization primitive for the lead
  • getTeammateContext() / 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

  1. Lead sends shutdown_request to a teammate
  2. Teammate can approve (exits gracefully) or reject (continues working with an explanation)
  3. Team cleanup via TeamDelete removes ~/.claude/teams/{team-name}/ and ~/.claude/tasks/{team-name}/
  4. 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

On-Disk Artifacts (Claude Code v2.1.47)

Observed at /Users/tau/.claude/:

  • Team directories with config.json and inboxes/{agent-name}.json files
  • Task directories with .lock, .highwatermark, and individual task JSON files
  • Sample task assignment message from team-lead to cp-agent, timestamped 2026-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.

Hive Codebase