This is Part 10 — the final installment of “Deep Dive: How Claude Code Works Under the Hood.” We’ve journeyed from the basic agent loop through permissions, context management, tool execution, error recovery, task systems, and agent teams. Today we cover the last two architectural pieces — worktree isolation and MCP — and then step back to see the full picture.
The File Conflict Problem
Here’s a scenario that breaks naive multi-agent setups. You have two agents working simultaneously: one refactoring the authentication module, one adding a new API endpoint. Both need to modify src/routes/index.ts. Agent A writes its version. Agent B writes its version. One of them loses their changes.
This isn’t hypothetical. It happened to me within the first week of experimenting with concurrent agents at ZenoLab. Two agents cheerfully clobbered each other’s work, and I spent an hour manually merging the results from git history.
Claude Code solves this structurally, not with file locking or merge algorithms, but with worktree isolation — each agent gets its own working copy of the codebase.
Two Operational Planes
The architecture separates concerns into two planes:
- Control Plane (
.tasks/): Task definitions, status, dependencies, scheduling. Shared across all agents. This is the coordination layer. - Execution Plane (
.worktrees/): Isolated file system copies where agents actually do their work. Each agent writes to its own worktree, never touching another agent’s files.
project/
├── .tasks/
│ ├── task_001.json # Shared — all agents read this
│ ├── task_002.json
│ └── task_003.json
├── .worktrees/
│ ├── task_001/ # Agent A works here
│ │ └── (full repo copy on its own branch)
│ ├── task_002/ # Agent B works here
│ │ └── (full repo copy on its own branch)
│ └── events.jsonl # Activity log
└── src/ # Main working tree
└── ...
The control plane is shared and read by everyone. The execution plane is partitioned — each task gets its own worktree. No conflicts possible.
Bidirectional Binding
Tasks and worktrees are bound to each other:
// In .tasks/task_001.json
{
"id": "task_001",
"title": "Refactor auth module",
"worktree": ".worktrees/task_001",
"branch": "task/refactor-auth"
}
// The worktree knows its task too
// .worktrees/task_001/.task_ref → "task_001"
This bidirectional binding means you can go from a task to its worktree (to see what files changed) or from a worktree to its task (to see the objective and status). It’s a simple but powerful navigational aid when you’re debugging what happened during a multi-agent session.
Git Worktrees: The Underlying Mechanism
Claude Code uses git worktrees — a built-in Git feature that most developers don’t know exists. A git worktree is a separate working directory linked to the same repository, with its own branch and index.
# Create a worktree for a task
git worktree add .worktrees/task_001 -b task/refactor-auth
# The worktree is a full working copy
ls .worktrees/task_001/
# src/ package.json tsconfig.json ... (everything)
# But it shares the git history with the main repo
cd .worktrees/task_001 && git log # Same commits as main
The brilliant thing about git worktrees is they’re cheap. They share the .git object store with the main repository, so creating one doesn’t duplicate the full repo history. It’s essentially a new checkout — new working files, same underlying git database.
Each worktree gets its own branch. Agent A works on task/refactor-auth, Agent B works on task/add-endpoint. They can both modify src/routes/index.ts without conflict because they’re on separate branches. When the work is done, the changes merge through normal git merge workflows.
The Full Lifecycle
Here’s the complete lifecycle of a task with worktree isolation:
1. Create task → .tasks/task_001.json created (status: pending)
2. Create worktree → git worktree add .worktrees/task_001 -b task/refactor-auth
3. Bind them → task.worktree = ".worktrees/task_001"
4. Execute → Agent works exclusively in .worktrees/task_001/
5. Commit → Agent commits changes on task/refactor-auth branch
6. Close → Worktree removed, branch ready for merge/PR
Step 4 is the key constraint: the agent’s file operations are scoped to its worktree. It reads and writes files within .worktrees/task_001/, not in the main working tree. This is enforced by the tool execution layer — file paths are rewritten to point into the correct worktree.
function resolveFilePath(requestedPath, agentContext) {
if (agentContext.worktree) {
// Rewrite path to agent's worktree
return path.join(agentContext.worktree, requestedPath);
}
return requestedPath;
}
Event Logging
All worktree activity is logged to .worktrees/events.jsonl:
{"timestamp":"2026-04-28T10:00:00Z","event":"worktree_created","task":"task_001","branch":"task/refactor-auth"}
{"timestamp":"2026-04-28T10:05:23Z","event":"file_modified","task":"task_001","path":"src/auth/handler.ts"}
{"timestamp":"2026-04-28T10:12:45Z","event":"commit","task":"task_001","sha":"a1b2c3d","message":"Refactor auth to use JWT"}
{"timestamp":"2026-04-28T10:13:01Z","event":"worktree_closed","task":"task_001","branch":"task/refactor-auth"}
This event log is invaluable for post-mortem analysis. When something goes wrong in a multi-agent session, you can reconstruct exactly what each agent did, in what order, to which files. It’s the flight recorder of the agent system.
MCP: Model Context Protocol
Now for the second major topic in this final post. If worktree isolation solves “how do agents work without stepping on each other,” MCP solves “how do you give agents new capabilities without modifying the core system.”
MCP (Model Context Protocol) is the plugin architecture that lets Claude Code connect to external tools, data sources, and services. Instead of hardcoding every capability into the agent, MCP defines a standard interface for external providers.
Think of it like USB for AI agents. You don’t redesign your laptop every time you want to connect a new device. You plug it in through a standard port. MCP is that port.
Three Key Pieces
The MCP implementation in Claude Code has three main components:
1. MCPClient
The client manages connections to external MCP servers. Each server provides one or more tools.
class MCPClient {
constructor() {
this.servers = new Map();
}
async connect(serverConfig) {
const server = await MCPServer.connect({
uri: serverConfig.uri,
transport: serverConfig.transport, // stdio, HTTP, WebSocket
auth: serverConfig.auth,
});
// Discover available tools
const tools = await server.listTools();
this.servers.set(serverConfig.name, { server, tools });
return tools;
}
async callTool(serverName, toolName, params) {
const { server } = this.servers.get(serverName);
return server.callTool(toolName, params);
}
}
The client handles connection management, tool discovery, and invocation. It supports multiple transport protocols — stdio for local processes, HTTP for remote servers, WebSocket for persistent connections.
2. Tool Name Normalization
When a tool is discovered from an MCP server, its name gets normalized to prevent collisions. A “search” tool from a GitHub server and a “search” tool from a Jira server need to be distinguishable.
function normalizeToolName(serverName, toolName) {
return `mcp__${serverName}__${toolName}`;
// e.g., "mcp__github__search_issues"
// e.g., "mcp__jira__search_issues"
}
The namespacing is simple but essential. The model sees mcp__github__search_issues and mcp__jira__search_issues as distinct tools, and can choose the right one based on context.
3. Unified Router
This is where MCP tools merge with native tools. The unified router presents all available tools — native and MCP-provided — through a single interface.
class UnifiedToolRouter {
constructor(nativeTools, mcpClient) {
this.nativeTools = nativeTools;
this.mcpClient = mcpClient;
}
getAvailableTools() {
const native = this.nativeTools.list();
const mcp = this.mcpClient.getAllTools();
return [...native, ...mcp]; // Model sees all tools equally
}
async execute(toolName, params) {
if (toolName.startsWith('mcp__')) {
const [, serverName, actualTool] = toolName.split('__');
return this.mcpClient.callTool(serverName, actualTool, params);
}
return this.nativeTools.execute(toolName, params);
}
}
From the model’s perspective, there’s no difference between native tools and MCP tools. They appear in the same tool list, are called the same way, and return results in the same format. The routing is transparent.
Plugin Discovery
How does Claude Code find MCP servers? Through plugin manifests:
// .claude-plugin/plugin.json
{
"name": "my-project-tools",
"version": "1.0.0",
"servers": [
{
"name": "database",
"command": "node",
"args": ["./tools/db-server.js"],
"transport": "stdio"
},
{
"name": "deploy",
"command": "python",
"args": ["./tools/deploy_server.py"],
"transport": "stdio"
}
]
}
At startup, Claude Code scans for plugin manifests, launches the configured servers, discovers their tools, and registers everything in the unified router. This happens automatically — no manual tool registration needed.
You can have project-level plugins (in the project’s .claude-plugin/ directory) and user-level plugins (in your home directory). Project plugins travel with the repo, so team members get the same extended capabilities.
Security: Same Permission Gate
Here’s something that could easily go wrong: external tools running with full system access. Claude Code avoids this by routing all MCP tools through the same permission system as native tools.
async execute(toolName, params) {
// Permission check applies to ALL tools, native or MCP
await checkPermission(toolName, params);
if (toolName.startsWith('mcp__')) {
return this.mcpClient.callTool(serverName, actualTool, params);
}
return this.nativeTools.execute(toolName, params);
}
An MCP tool that tries to delete files goes through the same approval flow as a native file deletion. An MCP tool that makes network requests goes through the same network permission check. There’s no “plugin bypass” — external capabilities get the same security scrutiny as built-in ones.
This was a critical design decision. The moment you let plugins bypass security is the moment you have a vulnerability that scales with the number of plugins installed.
The Conceptual Hierarchy
To keep the mental model clean, Claude Code organizes external capabilities in a three-level hierarchy:
Plugin (package of capabilities)
└── Server (runtime process providing tools)
└── Tool (individual capability)
- A Plugin is a package — like an npm package but for agent capabilities. It declares one or more servers.
- A Server is a running process that speaks MCP. It provides one or more tools.
- A Tool is an individual capability: search issues, run query, deploy service.
This hierarchy means you can reason about capabilities at whatever level makes sense. “Install the database plugin” (package level). “The deploy server is slow” (process level). “The search_issues tool returned wrong results” (capability level).
The Complete Architecture
We’ve reached the end. Let me step back and trace the full architecture we’ve built up across this series.
Part 1-2: The Foundation The core agent loop: prompt the model, get a response, execute tool calls, feed results back. Simple, but the seed of everything that follows. The system prompt and CLAUDE.md that shapes behavior.
Part 3-4: Safety & Context Permission systems that prevent dangerous operations. Context management with compaction that keeps the agent working within model limits. The conversation as a managed resource, not an infinite buffer.
Part 5-6: Tools & Execution Tool execution with validation, sandboxing, and result formatting. The tool router that dispatches calls to the right handler. Diff-based editing that prevents full-file overwrites.
Part 7: Hardening Error recovery with classification, budget-based retries, and visible state machines. The three recoverable failure types: truncation, context overflow, and transport errors. Recovery counters preventing infinite loops.
Part 8: Persistent Work Task DAGs with dependency resolution and status tracking. Background execution with daemon threads. The drain-before-call pattern. Cron scheduling for future intent.
Part 9: Multi-Agent Coordination Persistent teammate agents with file-based messaging. Structured protocols with tracking IDs. Autonomous task claiming from shared boards. Shutdown handshakes and identity re-injection.
Part 10: Isolation & Extension Worktree isolation preventing file conflicts between concurrent agents. MCP providing a plugin architecture for external capabilities. Unified routing and consistent security.
The Progression
What strikes me, having written all of this out, is the progression. Each layer is a response to a real limitation of the previous one:
- The basic loop can’t prevent dangerous actions → add permissions
- Long conversations break the model → add context management
- Tools need safety rails → add validation and sandboxing
- Things fail in production → add classified error recovery
- Simple prompts can’t track complex work → add persistent tasks
- One agent is a bottleneck → add agent teams
- Concurrent agents corrupt files → add worktree isolation
- Hardcoded capabilities limit extensibility → add MCP plugins
None of these features exist in isolation. They form a coherent architecture where each piece depends on and enhances the others. Worktree isolation wouldn’t work without the task system. Agent teams wouldn’t work without the messaging protocol. Error recovery wouldn’t work without the classified approach. It’s layers, all the way down.
Closing Thoughts
When I started this series, I wanted to answer a simple question: how does Claude Code actually work? Not the marketing version, but the engineering version. What data structures, what patterns, what tradeoffs?
Ten posts later, the answer is: it’s a carefully layered system that solves each problem at the right level of abstraction. It’s not a single clever algorithm. It’s dozens of well-chosen patterns — agent loops, permission gates, context compaction, error classification, task DAGs, file-based messaging, git worktrees, plugin protocols — composed into something that feels seamless from the outside.
As an indie developer running ZenoLab, the lessons I’ve taken from studying this architecture have directly improved my own agent-based tools. The classify-then-act pattern for errors. File-based messaging for simplicity. Budget-based retries. The drain-before-call pattern. These aren’t Claude Code secrets — they’re general-purpose patterns that any developer building with AI agents can use.
If you’ve followed along for all ten parts, thank you. I hope this series has demystified what happens behind the scenes and given you practical patterns you can apply in your own work. The era of AI-powered development tools is just getting started, and understanding the engineering underneath gives you a real edge — whether you’re building agents, using them, or both.
Until next time, keep building.