markdown.engineering
Lesson 21

Coordinator Mode

⏱ ~30 min ⚡ Advanced 📂 coordinatorMode.ts · tools.ts

🗺 What Is Coordinator Mode?

Coordinator mode is a first-class operating mode in Claude Code where Claude acts exclusively as a dispatcher — it never runs tools itself. Every side-effecting operation is delegated to worker agents that the coordinator spawns and directs. The coordinator's job is to think, plan, synthesize, and communicate; the workers' job is to execute.

One environment variable gates the whole mode. CLAUDE_CODE_COORDINATOR_MODE=1 activates it. The function isCoordinatorMode() reads this at runtime — no caching — so you can flip it in a resumed session and the change takes effect immediately.

This separation is not cosmetic. The coordinator receives a different system prompt, a different tool set, and a different context block compared to a regular Claude Code session. Understanding how each piece is wired together is the subject of this lesson.

flowchart TD U["🧑 User"] C["📋 Coordinator
CLAUDE_CODE_COORDINATOR_MODE=1"] W1["💻 Worker A
research"] W2["💻 Worker B
implementation"] W3["💻 Worker C
verification"] U -->|"request"| C C -->|"Agent(...)"| W1 C -->|"Agent(...)"| W2 C -->|"SendMessage(...)"| W2 C -->|"Agent(...)"| W3 W1 -->|"task-notification"| C W2 -->|"task-notification"| C W3 -->|"task-notification"| C C -->|"synthesis + status"| U style U fill:#1a1816,stroke:#c47a50,color:#b8b0a4 style C fill:#22201d,stroke:#c47a50,color:#c47a50 style W1 fill:#1f241d,stroke:#6e9468,color:#6e9468 style W2 fill:#241f2a,stroke:#8e82ad,color:#8e82ad style W3 fill:#262117,stroke:#b8965e,color:#b8965e

The coordinator speaks only to the user and directs workers. Workers speak only to the coordinator (via task-notification XML messages). The coordinator never executes Bash, edits files, or reads the filesystem directly.

🔑 Activation and Session Matching

Two exported functions control whether coordinator mode is on and keep it consistent across session resumes.

Show: isCoordinatorMode() and matchSessionMode() — coordinatorMode.ts
coordinator/coordinatorMode.ts TypeScript
export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

/**
 * Checks if the current coordinator mode matches the session's stored mode.
 * If mismatched, flips the environment variable so isCoordinatorMode()
 * returns the correct value for the resumed session.
 * Returns a warning message if the mode was switched, or undefined if not.
 */
export function matchSessionMode(
  sessionMode: 'coordinator' | 'normal' | undefined,
): string | undefined {
  if (!sessionMode) return undefined  // old session, no tracking

  const currentIsCoordinator = isCoordinatorMode()
  const sessionIsCoordinator  = sessionMode === 'coordinator'

  if (currentIsCoordinator === sessionIsCoordinator) return undefined

  // Flip the env var — isCoordinatorMode() reads it live, no caching
  if (sessionIsCoordinator) {
    process.env.CLAUDE_CODE_COORDINATOR_MODE = '1'
  } else {
    delete process.env.CLAUDE_CODE_COORDINATOR_MODE
  }

  logEvent('tengu_coordinator_mode_switched', { to: sessionMode })

  return sessionIsCoordinator
    ? 'Entered coordinator mode to match resumed session.'
    : 'Exited coordinator mode to match resumed session.'
}

Notice that matchSessionMode() mutates process.env directly instead of returning a new mode value. Because isCoordinatorMode() has no internal cache — it reads the env var on every call — writing to process.env.CLAUDE_CODE_COORDINATOR_MODE is sufficient to flip the entire mode for the rest of the process lifetime.

Feature gate required. Even with the env var set, coordinator mode only activates if the COORDINATOR_MODE Bun bundle feature is enabled. In production builds this gate controls which organizations or experiments see the mode. In development builds it is on by default.

🚫 Tool Restrictions: What the Coordinator Can Use

This is the most important mechanical difference from a normal session. The coordinator's allowed tool set is defined as a constant in constants/tools.ts:

constants/tools.ts TypeScript
/**
 * Tools allowed in coordinator mode — only output and agent management
 * tools for the coordinator itself.
 */
export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
  AGENT_TOOL_NAME,         // 'Agent'       — spawn a new worker
  TASK_STOP_TOOL_NAME,     // 'TaskStop'    — kill a running worker
  SEND_MESSAGE_TOOL_NAME,  // 'SendMessage' — continue an existing worker
  SYNTHETIC_OUTPUT_TOOL_NAME,  // internal output tool
])

Four tools. That is the entire interface available to the coordinator. No Bash, no Read, no Edit, no Glob, no Grep. The coordinator cannot touch the filesystem, run commands, or call MCP tools directly. It can only spawn workers, message them, and stop them.

Agent

Spawn a new worker

Creates a fresh worker with its own context, tool access, and system prompt. Use subagent_type: "worker". Workers run asynchronously by default and notify the coordinator on completion.

SendMessage

Continue an existing worker

Sends a follow-up message to a worker identified by its task_id. The worker retains full context from its prior run — use this for corrections and follow-ups rather than spawning fresh.

TaskStop

Stop a running worker

Terminates a worker mid-execution. Use when the user changes requirements after launch, or when an approach is clearly wrong. A stopped worker can still be continued via SendMessage.

SyntheticOutput

Internal output signal

Used internally to surface structured output to the coordinator's own turn. Not typically called explicitly in coordinator prompts — it is part of the multi-agent notification machinery.

Workers have full tool access. The restriction applies only to the coordinator. Workers spawned via Agent receive the full ASYNC_AGENT_ALLOWED_TOOLS set (Bash, Read, Edit, Glob, Grep, WebSearch, Skill, Notebook, Worktree, and more) plus MCP tools from connected servers.

📦 Worker Context: getCoordinatorUserContext()

When coordinator mode is active, getCoordinatorUserContext() injects a context block into the coordinator's conversation that describes what tools the workers have access to. This is how the coordinator knows what to delegate and at what level of specificity.

Show: getCoordinatorUserContext() — coordinatorMode.ts
coordinator/coordinatorMode.ts TypeScript
// Tools hidden from workers even though they are in ASYNC_AGENT_ALLOWED_TOOLS
const INTERNAL_WORKER_TOOLS = new Set([
  TEAM_CREATE_TOOL_NAME,    // 'TeamCreate'
  TEAM_DELETE_TOOL_NAME,    // 'TeamDelete'
  SEND_MESSAGE_TOOL_NAME,   // 'SendMessage'
  SYNTHETIC_OUTPUT_TOOL_NAME,
])

export function getCoordinatorUserContext(
  mcpClients: ReadonlyArray<{ name: string }>,
  scratchpadDir?: string,
): { [k: string]: string } {
  if (!isCoordinatorMode()) return {}

  // Simple mode: workers only get Bash, Read, Edit
  const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
    ? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME].sort().join(', ')
    : Array.from(ASYNC_AGENT_ALLOWED_TOOLS)
        .filter(name => !INTERNAL_WORKER_TOOLS.has(name))
        .sort()
        .join(', ')

  let content = `Workers spawned via the Agent tool have access to these tools: ${workerTools}`

  if (mcpClients.length > 0) {
    const serverNames = mcpClients.map(c => c.name).join(', ')
    content += `\n\nWorkers also have access to MCP tools from connected MCP servers: ${serverNames}`
  }

  if (scratchpadDir && isScratchpadGateEnabled()) {
    content += `\n\nScratchpad directory: ${scratchpadDir}\nWorkers can read and write here without permission prompts.`
  }

  return { workerToolsContext: content }
}

Three things happen in this function:

  • Tool enumeration. The worker tool list is computed at runtime by filtering ASYNC_AGENT_ALLOWED_TOOLS, removing the INTERNAL_WORKER_TOOLS set (TeamCreate, TeamDelete, SendMessage, SyntheticOutput) which are coordinator-only primitives.
  • MCP server advertisement. If any MCP clients are connected, their names are appended. This tells the coordinator it can instruct workers to call MCP tools by name.
  • Scratchpad injection. If a shared scratchpad directory exists and the feature gate is on, the path is injected so the coordinator can tell workers to read/write there for durable cross-worker knowledge without permission prompts.
Simple mode (CLAUDE_CODE_SIMPLE=1) is a stripped-down variant. Workers only get Bash, Read, and Edit — no web search, no notebooks, no worktree management. This is the mode for SDK consumers who want a minimal, predictable agent surface.

📜 The Coordinator System Prompt

The heart of coordinator mode is its system prompt — returned by getCoordinatorSystemPrompt(). It is a 300+ line document that defines the coordinator's role, its tool contracts, the four-phase task workflow, and detailed prompt-engineering guidelines for writing worker instructions. Below are the six major sections annotated.

Section 1 — Role definition
coordinator/coordinatorMode.ts — getCoordinatorSystemPrompt() Text
You are a coordinator. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible — don't delegate work that
  you can handle without tools

Every message you send is to the user. Worker results and system
notifications are internal signals, not conversation partners —
never thank or acknowledge them. Summarize new information for
the user as it arrives.

The key constraint: the coordinator always addresses the user, not workers. Worker task-notification messages are treated as internal signals, not conversation turns.

Section 2 — Tool contracts and task-notification format

Worker results arrive as user-role messages containing a <task-notification> XML envelope. This is how the coordinator knows a worker finished:

<task-notification> <task-id>agent-a1b</task-id> <status>completed</status> <!-- completed | failed | killed --> <summary>Agent "Investigate auth bug" completed</summary> <result>Found null pointer in src/auth/validate.ts:42...</result> <usage> <total_tokens>4820</total_tokens> <tool_uses>11</tool_uses> <duration_ms>18432</duration_ms> </usage> </task-notification>

The <task-id> value is the agent ID — pass it to SendMessage as to to continue that specific worker. <result> and <usage> are optional.

Section 3 — Four-phase task workflow
Phase
Who
Purpose
Research
Workers (parallel)
Investigate codebase, find files, understand the problem
Synthesis
Coordinator
Read findings, craft specific implementation specs with file paths and line numbers
Implementation
Workers
Make targeted changes per spec, run tests, commit
Verification
Workers
Prove the code works independently — do not rubber-stamp implementation

Synthesis is the coordinator's most important phase. The system prompt explicitly forbids phrases like "based on your findings" — the coordinator must understand the research and translate it into precise specs before handing work back to workers.

Section 4 — Parallelism rules

The system prompt treats concurrency as a first-class design principle:

  • Read-only tasks (research) — run as many in parallel as needed, freely
  • Write-heavy tasks (implementation) — one at a time per overlapping set of files
  • Verification — can run alongside implementation on different file areas

To launch workers in parallel, make multiple Agent tool calls in a single coordinator turn. Workers run asynchronously; the coordinator does not block waiting for one before launching another.

Parallel launch example (coordinator turn) Text
Let me investigate from two angles.

Agent({ description: "Investigate auth bug", subagent_type: "worker",
        prompt: "Find null pointer sources in src/auth/. Report file paths,
                 line numbers, types. Do not modify files." })

Agent({ description: "Research auth test coverage", subagent_type: "worker",
        prompt: "Find all test files for src/auth/. Report gaps around
                 session expiry. Do not modify files." })

Investigating both in parallel — I will report back with findings.
Section 5 — Continue vs. spawn fresh decision logic
Situation Action Why
Research explored exactly the files that need editing Continue Worker already has files in context AND gets a clear plan
Research was broad but implementation is narrow Spawn fresh Avoid dragging exploration noise into focused implementation
Correcting a failure or extending recent work Continue Worker has the error context and knows what it just tried
Verifying code a different worker wrote Spawn fresh Verifier should see code with fresh eyes
First attempt used entirely wrong approach Spawn fresh Wrong-approach context pollutes the retry
Unrelated task Spawn fresh No useful context to reuse
Section 6 — Worker prompt guidelines (good vs. bad)

Workers cannot see the coordinator's conversation. Every prompt must be fully self-contained.

Bad prompts

  • "Fix the bug we discussed"
  • "Based on your findings, implement the fix"
  • "Create a PR for the recent changes"
  • "Something went wrong with the tests, can you look?"

Good prompts

  • "Fix null pointer in src/auth/validate.ts:42. The user field is undefined when Session.expired is true. Add null check before user.id access — return 401 with 'Session expired' if null. Run tests, commit, report hash."
  • "Verify the null-check fix: run auth tests with the feature enabled, check edge cases (expired + cached token), investigate any failures — do not dismiss as unrelated."
Never write "based on your findings." This phrase delegates understanding to the worker instead of the coordinator doing its job. After research, the coordinator must synthesize findings into specific file paths, line numbers, and exact changes — then write a prompt that proves it understood.

Coordinator vs. Single-Agent Mode

Both modes use Claude Code's same underlying infrastructure — sessions, tools, permissions, MCP clients. The difference is in the role the main instance plays.

Dimension Single-Agent Coordinator
Role Does the work itself Dispatches and synthesizes; never executes
Tool count Full tool set (Bash, Read, Edit, Glob, ...) 4 tools only: Agent, SendMessage, TaskStop, SyntheticOutput
Filesystem access Direct read/write None — must delegate to workers
System prompt Standard Claude Code prompt getCoordinatorSystemPrompt() — task workflow, parallel dispatch, synthesis rules
Worker context Not injected getCoordinatorUserContext() injects worker tool list + MCP servers + scratchpad
Parallelism Sequential tool calls Multiple concurrent workers via parallel Agent calls
Session resume Mode irrelevant matchSessionMode() flips env var to restore correct mode for resumed sessions
Simple mode variant N/A CLAUDE_CODE_SIMPLE=1 limits workers to Bash + Read + Edit only
Scratchpad N/A Shared directory injected into worker context — permission-free cross-worker storage

📋 Full Example Session

This annotated example traces a complete auth bug fix through all four phases. Each "You:" block is a separate coordinator turn. "User:" blocks are task-notification messages arriving between turns.

Show: Full annotated coordinator session
Example: Auth null pointer fix Coordinator Session
// User request
User: "There's a null pointer in the auth module. Can you fix it?"

// Turn 1 — coordinator launches research workers in parallel
You:
  Let me investigate from two angles.

  Agent({ description: "Investigate auth bug", subagent_type: "worker",
    prompt: "Investigate src/auth/. Find where null pointer exceptions
             could occur around session handling and token validation.
             Report specific file paths, line numbers, and types.
             Do not modify files." })

  Agent({ description: "Research auth tests", subagent_type: "worker",
    prompt: "Find all test files related to src/auth/. Report structure,
             coverage, and gaps around session expiry. Do not modify files." })

  Investigating from two angles — I will report back with findings.

// task-notification from Worker A
User:
  <task-notification>
  <task-id>agent-a1b</task-id>
  <status>completed</status>
  <summary>Agent "Investigate auth bug" completed</summary>
  <result>Found null pointer in src/auth/validate.ts:42. The user field
  on Session (types.ts:15) is undefined when sessions expire but the
  token remains cached. validate() accesses user.id without null check.
  </result>
  </task-notification>

// Turn 2 — synthesize finding, continue same worker with specific spec
You:
  Found the bug — null pointer in validate.ts:42. Still waiting on test coverage.

  SendMessage({ to: "agent-a1b",
    message: "Fix the null pointer in src/auth/validate.ts:42.
              The user field on Session (src/auth/types.ts:15) is
              undefined when sessions expire but the token is still
              cached. Add a null check before user.id access — if
              user is null, return 401 with 'Session expired'.
              Run the auth test suite, commit, report the hash." })

  Fix is in progress. Still waiting for test coverage findings.

// task-notification from Worker B
User:
  <task-notification>
  <task-id>agent-b2c</task-id>
  <status>completed</status>
  <result>Test file: src/auth/__tests__/validate.test.ts. Session expiry
  paths are tested at lines 58 and 72 but only for the happy path —
  no test for expired session + cached token.</result>
  </task-notification>

// Turn 3 — spawn fresh verifier once Worker A reports back
// (Worker A's implementation-heavy context should not contaminate verification)
You:
  Test coverage found: validate.test.ts lacks expired session + cached token cases.
  I will have the verifier add those once the fix lands.

// After Worker A reports fix committed:
You:
  Fix is committed. Launching verification.

  Agent({ description: "Verify auth fix", subagent_type: "worker",
    prompt: "Verify the null-check fix in src/auth/validate.ts:42.
             Add test cases to validate.test.ts (lines 58 and 72 area)
             covering expired session + cached token — expect 401
             with 'Session expired'. Run full auth suite with the
             feature enabled. Investigate any failures — do not dismiss
             as unrelated. Report pass/fail and the updated test hash." })

  Verification in progress.

📝 The Scratchpad: Cross-Worker Knowledge

When the tengu_scratch feature gate is active and a scratchpad directory is provided, coordinator mode injects the scratchpad path into the worker context. Workers can read and write there without permission prompts, making it a shared workspace for durable findings.

Circular dependency note. The scratchpad gate check is duplicated in coordinatorMode.ts as a local isScratchpadGateEnabled() function instead of importing from utils/permissions/filesystem.ts. The comment explains: importing that file would create a circular dependency chain (filesystem → permissions → ... → coordinatorMode). The actual scratchpad path is injected via dependency injection from QueryEngine.ts, which sits higher in the dependency graph.

The coordinator can instruct workers to write findings, partial results, or structured data to the scratchpad so that later workers (or the coordinator on subsequent turns) can read it without re-doing work. This is especially useful for long-running parallel research tasks where multiple workers need to share context.

🎯 Key Takeaways

  • The coordinator is a pure dispatcher — it has exactly 4 tools (Agent, SendMessage, TaskStop, SyntheticOutput) and cannot touch the filesystem, run commands, or call MCP tools directly.
  • Activation is a single env var: CLAUDE_CODE_COORDINATOR_MODE=1. isCoordinatorMode() reads it live with no caching, so writing to process.env flips the mode immediately.
  • getCoordinatorUserContext() injects a runtime-computed list of worker tools into the coordinator's context so it knows what to delegate. In simple mode (CLAUDE_CODE_SIMPLE=1), workers get only Bash, Read, and Edit.
  • The coordinator's system prompt enforces a four-phase workflow: Research (workers, parallel) → Synthesis (coordinator) → Implementation (workers) → Verification (workers).
  • Synthesis is the coordinator's most important job. It must produce specific file paths and line numbers — never vague "based on your findings" delegation.
  • The continue vs. spawn fresh decision is based on context overlap: high overlap → continue the worker; low overlap or fresh verification → spawn new.
  • matchSessionMode() ensures resumed sessions restore the correct mode by mutating process.env and logging a tengu_coordinator_mode_switched analytics event.
  • The scratchpad is a permission-free shared directory injected into worker context when the tengu_scratch gate is active — useful for durable cross-worker knowledge without file permission prompts.

🧪 Quiz

Q1. Which of these tools can the coordinator use in coordinator mode?
A Bash, Read, Edit, Glob
B Agent, SendMessage, TaskStop, SyntheticOutput
C Agent, Bash, Read, SendMessage
D All tools — restrictions only apply to workers
Correct. COORDINATOR_MODE_ALLOWED_TOOLS in constants/tools.ts defines exactly four tools. The coordinator has no filesystem or shell access — all execution is delegated to workers.
Q2. What does matchSessionMode() do when the resumed session was in coordinator mode but the process was started in normal mode?
A Sets process.env.CLAUDE_CODE_COORDINATOR_MODE = '1' and returns a warning message
B Returns an error and refuses to resume
C Starts a new session instead of resuming
D Logs the mismatch but does nothing — the mode is controlled only at startup
Correct. Because isCoordinatorMode() reads process.env with no caching, writing to it via matchSessionMode() is enough to flip the mode mid-process. The function also fires a tengu_coordinator_mode_switched analytics event.
Q3. Why does coordinatorMode.ts duplicate the scratchpad gate check instead of importing from utils/permissions/filesystem.ts?
A Performance — importing filesystem.ts adds significant startup latency
B The scratchpad feature is only available in coordinator mode
C Importing filesystem.ts would create a circular dependency chain
D The Statsig SDK cannot be called from the permissions module
Correct. The comment in source explicitly states: "Importing filesystem.ts creates a circular dependency (filesystem → permissions → ... → coordinatorMode)." The actual scratchpad path is injected via dependency injection from QueryEngine.ts which lives higher in the dependency graph.
Q4. A coordinator finishes receiving research from Worker A, which explored exactly the files that need to be edited. What should the coordinator do next?
A Spawn a fresh worker with the prompt "Based on Worker A's findings, implement the fix"
B Continue Worker A via SendMessage with a synthesized spec including specific file paths and line numbers
C Spawn a fresh worker and pass Worker A's full output as context
D Ask the user to confirm the findings before proceeding
Correct. High context overlap → continue. Worker A already has the relevant files loaded. The coordinator synthesizes the findings into a specific spec and sends it via SendMessage. Option A is the anti-pattern ("based on your findings") — the coordinator must synthesize, not delegate understanding.
Q5. When CLAUDE_CODE_SIMPLE=1 is set, what tools do workers have access to in coordinator mode?
A All tools from ASYNC_AGENT_ALLOWED_TOOLS minus internal worker tools
B Only Agent and SendMessage
C No tools — simple mode disables all workers
D Bash, Read (FileRead), and Edit (FileEdit) only
Correct. In getCoordinatorUserContext(), when CLAUDE_CODE_SIMPLE is truthy, the worker tool list is hardcoded to [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME]. This is the stripped-down surface for SDK consumers who want predictable, minimal worker behavior.
0 / 5 answered