๐ Featured findings (jump to bottom): ๐ฌ Grove โ the hidden training-data pipeline ยท ๐ก๏ธ Anti-Distillation โ Anthropic's 5-layer defense
Harness Engineering: A Comprehensive Guide Based on Claude Code
A Comprehensive Textbook on AI Agent Infrastructure Design
"The model is the agent. The code is the harness. Build great harnesses. The agent will do the rest."
๐ฃ As featured in Chinese AI media โ This Harness Engineering analysis has been republished by Chinese AI publications including QingkeAI (้็จAI) and others, with 20,000+ reads and 2,000+ shares across WeChat. Original article.
This tutorial is based on reverse engineering and systematic analysis of the Claude Code source code (~512,664 lines of TypeScript). Written in the style of an academic textbook, it provides an in-depth examination of every design decision, engineering trade-off, and implementation detail of an AI Agent Harness. The text adheres to academic writing conventions, constructing a theoretical framework atop the code analysis and distilling scattered implementation details into reusable design principles.
Intended Audience: AI engineers, Agent system architects, and researchers interested in LLM application infrastructure.
Prerequisites: Familiarity with TypeScript, experience with LLM API calls, and a working knowledge of basic system design concepts.
Figure 0-1: Harness Engineering Architecture Overview โ The LLM is surrounded by six layers of Harness infrastructure: the Tool System (43+), the Permission Model (5 modes), the Hooks System (26 events x 4 types), the Sandbox (file + network + process isolation), Context Engineering (CLAUDE.md + memory + 4-level compaction), and Settings & Configuration (7-level hierarchy).
Table of Contents
- Chapter 1: What Is Harness Engineering?
- Chapter 2: Claude Code Architecture Overview
- Chapter 3: Agent Loop โ The Heart of the Harness
- Chapter 4: Tool System โ The Agent's Hands
- Chapter 5: Permission Model โ Constraint Architecture
- Chapter 6: Hooks System โ Lifecycle Extensibility
- Chapter 7: Sandbox & Security โ The Safety Net
- Chapter 8: Context Engineering โ The Art of Information Management
- Chapter 9: Settings & Configuration โ Harness Tunability
- Chapter 10: MCP Integration โ Extending the Harness Boundary
- Chapter 11: Sub-Agent System โ Multi-Agent Orchestration
- Chapter 12: Skills & Plugins โ The Extension Ecosystem
- Chapter 13: Building Your Own Harness โ A Practical Guide
- Chapter 14: Advanced Patterns and Design Philosophy
- Chapter 15: Building a Mini Harness from Scratch (Hands-on Lab)
- Chapter 16: Competitive Analysis
- Conclusion: From Reader to Builder
- References
- Appendix A: Claude Code Source File Index
- Appendix B: Reference Resources
- Appendix C: Complete ToolUseContext Type Definition
- Appendix D: 13 Guard Rules Reference Model
Chapter 1: What Is Harness Engineering?
Imagine you are about to train a wild horse. You would not simply mount it โ you would first build fences, prepare reins, and lay out a track. This "infrastructure" is not the horse itself, but without it, even the finest horse remains untamed.
AI Agents are no different. The model (LLM) is the horse โ powerful yet unbroken. Harness Engineering is the discipline of building fences, crafting reins, and laying tracks.
1.1 Definition
Harness Engineering is the engineering discipline of designing the environment, constraints, feedback loops, and infrastructure that enable AI Agents to operate reliably at scale.
The term was formally introduced by the OpenAI engineering team in early 2026. They described internal systems comprising "over one million lines of code, none of which were written by humans" โ engineers no longer wrote code directly but instead "designed systems that enabled AI Agents to write code reliably."
A simple analogy helps illustrate the concept:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Agent = Model (LLM) โ
โ Harness = Everything Else โ
โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Claude โ โโโ โ Tools, Permissions, โ โ
โ โ Opus/ โ โโโ โ Hooks, Sandbox, โ โ
โ โ Sonnet โ โ Memory, Settings, โ โ
โ โ โ โ MCP, Skills, Agents โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Model Harness โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1.2 Three Pillars
To understand Harness Engineering, it is most instructive to decompose it into three pillars. Consider the analogy of constructing a building: Context Engineering is the foundation (ensuring that the right information is in place), Architectural Constraints are the load-bearing walls (ensuring structural integrity), and Entropy Management is the building maintenance (preventing degradation over time).
mindmap
root((Harness Engineering))
Context Engineering
้ๆไธไธๆ
CLAUDE.md
AGENTS.md
่ฎพ่ฎกๆๆกฃ
ๅจๆไธไธๆ
ๆฅๅฟไธๆๆ
Git ็ถๆ
CI/CD ็ถๆ
ไธไธๆๅ็ผฉ
ๅ็บง็ฎก้
ๆ้ๅ ่ฝฝ
่ฎฐๅฟ็ณป็ป
Architectural Constraints
ๆ้ๆจกๅ
5 ็งๆจกๅผ
7 ็บง่งๅๅฑ็บง
AI ๅ็ฑปๅจ
ๅทฅๅ
ท็บฆๆ
Schema ้ช่ฏ
ๅนถๅๅฎๅ
จๆ ่ฎฐ
ๅปถ่ฟๅ ่ฝฝ
ๅฎๅ
จ่พน็
ๆฒ็้็ฆป
็กฌ็ผ็ ๆ็ป
็บตๆทฑ้ฒๅพก
Entropy Management
ๅฎๆๆธ
็
ๆญปไปฃ็ ๆฃๆต
ๆๆกฃไธ่ดๆง
็บฆๆ้ช่ฏ
ไพ่ตๅฎก่ฎก
ๆจกๅผๅผบๅถ
ๆง่ฝ็ๆง
่ฆ็็ๅฎๅซ
ๅๅฝๆฃๆต
Figure 1-2: Engineering Time Allocation Across the Three Pillars โ Context Engineering commands the largest share (45%), because "information the Agent cannot see might as well not exist." Architectural Constraints come second (35%). Entropy Management accounts for 20% but is critical for long-term stability.
Pillar One: Context Engineering
Manages the accessibility, structure, and timing of information. Key techniques include:
- Static Context: Repository documentation, CLAUDE.md/AGENTS.md files, design documents
- Dynamic Context: Logs, metrics, directory mappings, CI/CD status
- Core Principle: "Information the Agent cannot access within its context does not exist"
Pillar Two: Architectural Constraints
Establishes boundaries through mechanical enforcement rather than suggestions:
- Dependency hierarchies (Types -> Config -> Repo -> Service -> Runtime -> UI)
- Deterministic linter enforcement of custom rules
- LLM-based auditors reviewing Agent compliance
- Structural tests and pre-commit hooks
A counterintuitive benefit: Constraining the solution space makes the Agent more efficient, not less โ by preventing fruitless exploration.
Pillar Three: Entropy Management
Periodic cleanup Agents address code degradation:
- Documentation consistency verification
- Constraint violation scanning
- Pattern enforcement
- Dependency auditing
1.3 Harness Engineering vs. Related Disciplines
| Discipline | Relationship |
|---|---|
| Prompt Engineering | A subset of Context Engineering (single interaction vs. system-level) |
| ML Engineering | An independent discipline; assumes the model is already deployed |
| Agent Engineering | Complementary; Harness engineers build infrastructure for Agents |
| DevOps | Overlapping infrastructure skills applied to the AI context |
1.4 Pause and Reflect
Before continuing, consider the following question:
If you were building an AI coding assistant today, where would you spend 80% of your engineering time โ improving the model, or improving the systems surrounding the model?
If your answer is "the model," Harness Engineering challenges that intuition. The LangChain case study demonstrated that modifying only the Harness โ without changing the model โ yielded a 14 percentage point improvement on benchmarks. The model is a given; the Harness is what you can control.
1.5 Quantitative Evidence: ROI of Harness Investment
Before delving into "why now," let the data speak for itself:
Figure 1-1: ROI Comparison of Harness Optimization vs. Model Optimization โ In both Terminal Bench scores and development cycle reduction, Harness optimization yields returns far exceeding model optimization, while requiring only one-tenth the engineering effort.
| Metric | Model Optimization Only | Harness Optimization Only | Combined |
|---|---|---|---|
| Terminal Bench 2.0 Score | +3-5% (model upgrade) | +14% (LangChain case) | +18-20% |
| Development Cycle Reduction | Negligible | 10x (OpenAI million-line case) | >10x |
| Engineer Time Investment | Months (training/fine-tuning) | 1-2 hours (Level 1 Harness) | Months |
| Transferability | Model-specific | Reusable across models | Partially reusable |
Key Insight: The return on investment (ROI) of Harness optimization far exceeds that of model optimization. A carefully crafted CLAUDE.md file takes only 30 minutes to write but can boost Agent performance on a given project by 20-40%. By contrast, model fine-tuning requires weeks of effort and substantial computational resources, yet is effective only for specific tasks.
1.6 Why Now?
Three converging factors have given rise to this need:
- Model Commoditization โ Competitive advantage is shifting from models to systems
- Production Deployment โ Agents are moving from demos to customer-facing reliability requirements
- Benchmark Limitations โ Standard metrics cannot capture multi-hour, multi-step Agent stability
Real-world impact: By modifying only the Harness architecture (without switching models), LangChain improved its Terminal Bench 2.0 score from 52.8% to 66.5%, vaulting from the top 30 to the top 5.
1.5 Implementation Tiers
| Tier | Scope | Investment | Contents |
|---|---|---|---|
| Level 1 | Individual | 1-2 hours | CLAUDE.md + pre-commit hooks + test suite |
| Level 2 | Small team | 1-2 days | AGENTS.md specification + CI constraints + shared templates |
| Level 3 | Organization | 1-2 weeks | Custom middleware + observability + scheduled Agents |
Chapter 2: Claude Code Architecture Overview
In the previous chapter we established the theoretical framework. Beginning with this chapter, we validate that theory against a real, production-grade system. That system is Claude Code โ Anthropic's official AI coding assistant CLI, comprising over 500,000 lines of TypeScript. It represents the most complete production-grade Agent Harness reference implementation available today.
Why Claude Code? Because it is not an educational project โ it is a real product used daily by tens of thousands of developers. Every design decision is backed by real user pain points and genuine engineering trade-offs. By reverse-engineering its architecture, we can learn practical wisdom that "textbooks never cover."
2.1 Technology Stack
| Category | Technology |
|---|---|
| Runtime | Bun (native TypeScript, high performance) |
| Language | TypeScript (strict mode) |
| UI Framework | React + Ink (terminal components) |
| CLI Parser | Commander.js (@commander-js/extra-typings) |
| Schema Validation | Zod v4 |
| Search Engine | ripgrep (invoked via BashTool) |
| API Client | @anthropic-ai/sdk |
| Protocols | MCP SDK, LSP |
| State Management | Custom Zustand-like Store + React Context |
| Telemetry | OpenTelemetry + gRPC |
| Feature Flags | GrowthBook + Bun bun:bundle |
| Auth | OAuth 2.0, JWT, macOS Keychain |
Figure 2-1: Lines of Code Distribution by Directory in Claude Code โ tools/ and utils/ are the two largest directories, together accounting for approximately 32% of the codebase. This reflects the centrality of the tool system and infrastructure utilities to the Harness.
Figure 2-2: Module Counts by Category โ components (144) and commands (101) are the most numerous, reflecting Claude Code's nature as a terminal UI application.
2.2 Scale
- ~1,884 TypeScript/TSX files
- 512,664 lines of code
- 43+ tools
- 100+ slash commands
- 80+ React hooks
- 144+ UI components
- 22+ service modules
- 26+ hook events
2.3 Directory Structure
src/
โโโ main.tsx # ๅ
ฅๅฃ็น๏ผCLI ๅผๅฏผ๏ผ803 KB๏ผ
โโโ query.ts # ๆ ธๅฟ Agent ๅพช็ฏ๏ผ68 KB๏ผ
โโโ QueryEngine.ts # LLM ๆฅ่ฏขๅผๆ๏ผ46 KB๏ผ
โโโ Tool.ts # Tool ๅบ็กๆฅๅฃ๏ผ29 KB๏ผ
โโโ tools.ts # Tool ๆณจๅ่กจ๏ผ25 KB๏ผ
โโโ Task.ts # ไปปๅก็ฑปๅๅฎไน
โโโ commands.ts # ๅฝไปคๆณจๅ
โ
โโโ tools/ # 43 ไธชๅทฅๅ
ท็ฎๅฝ
โ โโโ BashTool/ # Shell ๅฝไปคๆง่ก
โ โโโ FileReadTool/ # ๆไปถ่ฏปๅ
โ โโโ FileWriteTool/ # ๆไปถๅๅปบ
โ โโโ FileEditTool/ # ้จๅๆไปถไฟฎๆน
โ โโโ GlobTool/ # ๆไปถๆจกๅผๅน้
โ โโโ GrepTool/ # ripgrep ๅ
ๅฎนๆ็ดข
โ โโโ AgentTool/ # ๅญ Agent ็ๆ
โ โโโ SkillTool/ # Skill ๆง่ก
โ โโโ MCPTool/ # MCP ๆๅกๅจ่ฐ็จ
โ โโโ WebFetchTool/ # URL ๅ
ๅฎนๆๅ
โ โโโ WebSearchTool/ # ็ฝ้กตๆ็ดข
โ โโโ ... # ๆดๅคๅทฅๅ
ท
โ
โโโ commands/ # ~101 ไธชๅฝไปค็ฎๅฝ
โ โโโ commit/ # Git ๆไบค
โ โโโ review/ # ไปฃ็ ๅฎกๆฅ
โ โโโ mcp/ # MCP ็ฎก็
โ โโโ skills/ # Skill ็ฎก็
โ โโโ ...
โ
โโโ components/ # 144+ React/Ink ็ป็ซฏ็ปไปถ
โโโ hooks/ # 80+ ่ชๅฎไน React Hooks
โโโ services/ # 22 ไธชๆๅกๅญ็ฎๅฝ
โ โโโ api/ # Anthropic API ๅฎขๆท็ซฏ
โ โโโ mcp/ # MCP ๅ่ฎฎ่ฟๆฅ
โ โโโ oauth/ # OAuth ่ฎค่ฏ
โ โโโ lsp/ # ่ฏญ่จๆๅกๅจๅ่ฎฎ
โ โโโ compact/ # ๅฏน่ฏๅ็ผฉ
โ โโโ plugins/ # ๆไปถๅ ่ฝฝ
โ โโโ ...
โ
โโโ utils/ # 33+ ๅญ็ฎๅฝ๏ผ100+ ๆไปถ
โ โโโ permissions/ # ๆ้้ป่พ
โ โโโ hooks.ts # Hook ๆง่กๅผๆ
โ โโโ hooks/ # Hook ้
็ฝฎ็ฎก็
โ โโโ sandbox/ # ๆฒ็้้
ๅจ
โ โโโ settings/ # ่ฎพ็ฝฎ็ฎก็
โ โโโ bash/ # Shell ๅทฅๅ
ท
โ โโโ memdir/ # ๆไน
่ฎฐๅฟ็ฎๅฝ
โ โโโ ...
โ
โโโ state/ # ๅบ็จ็ถๆ็ฎก็
โโโ entrypoints/ # CLI/MCP/SDK ๅ
ฅๅฃ
โโโ bridge/ # IDE ๅๅ้ไฟก
โโโ coordinator/ # ๅค Agent ็ผๆ
โโโ skills/ # Skill ็ณป็ป
โโโ plugins/ # ๆไปถ็ณป็ป
โโโ memdir/ # ่ฎฐๅฟ็ฎๅฝ็ณป็ป
โโโ schemas/ # Zod ้ช่ฏ Schema
โโโ types/ # ็ฑปๅๅฎไน
โโโ constants/ # ๅบ็จๅธธ้
2.4 Entry Point Flow
main.tsx โ ๅนถ่ก้ขๅ๏ผMDM่ฎพ็ฝฎ + Keychain + API้ข่ฟๆฅ๏ผ
โ
Commander.js CLI ่งฃๆๅจๅๅงๅ
โ
preAction Hook: init() โ ้ฅๆต โ ๆไปถ โ ่ฟ็งป โ ่ฟ็จ่ฎพ็ฝฎ
โ
React/Ink ๆธฒๆๅจๅฏๅจ
โ
ไบคไบๅผ REPL/ๅฏน่ฏๅพช็ฏ
Design Philosophy: The Claude Code entry point main.tsx (803 KB) employs a lazy loading strategy. Heavy modules (OpenTelemetry, gRPC, analytics) are loaded only when needed, while critical-path resources (MDM settings, Keychain) are prefetched in parallel to ensure fast startup times.
2.5 Core Data Flow Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code ๆฐๆฎๆตๅ
จๆฏ โ
โ โ
โ ็จๆท่พๅ
ฅ โโโ UserPromptSubmit Hook โโโ Slash Command ่งฃๆ โ
โ โ โ
โ v โ
โ QueryEngine.submitMessage() โ
โ โ โ
โ โโโ ็ณป็ปๆ็คบๆๅปบ: base + tools + CLAUDE.md + MCP + memory โ
โ โโโ ๆถๆฏ่ง่ๅ: normalizeMessagesForAPI() โ
โ โ โโ ้ๆๅบ attachment ๆถๆฏ โ
โ โ โโ ๅๅนถ่ฟ็ปญ user/assistant ๆถๆฏ โ
โ โ โโ ๅฅ็ฆป PDF/ๅพ็้่ฏฏ็้ๅคๅ
ๅฎน โ
โ โ โโ ่ง่ๅๅทฅๅ
ทๅ็งฐ๏ผๅซๅโๆญฃๅผๅ๏ผ โ
โ โ โโ ๅทฅๅ
ทๆ็ดขๅผ็จๅๅค็ โ
โ โ โ
โ v โ
โ queryLoop() [while(true)] โ
โ โ โ
โ โโโ ๅ็ผฉ็ฎก้: snip โ micro โ collapse โ auto โ
โ โโโ API ่ฐ็จ: deps.sample() [ๆตๅผ] โ
โ โ โ
โ โโโ ๅทฅๅ
ทๆง่ก: StreamingToolExecutor (ๅนถๅ) / runTools (้กบๅบ) โ
โ โ โ โ
โ โ โโโ ๅทฅๅ
ทๅๅบ: partitionToolCalls() โ
โ โ โ โโ isConcurrencySafe=true โ ๅนถๅๆง่ก โ
โ โ โ โโ isConcurrencySafe=false โ ไธฒ่กๆง่ก โ
โ โ โ โ
โ โ โโโ ๆฏไธชๅทฅๅ
ท: โ
โ โ โโ Zod schema ้ช่ฏ โ
โ โ โโ tool.validateInput() โ
โ โ โโ PreToolUse Hook โ
โ โ โโ ๆ้ๆฃๆฅ (rules โ mode โ classifier) โ
โ โ โโ Sandbox ๅ
่ฃ
(BashTool) โ
โ โ โโ tool.call() [ๅฎ้
ๆง่ก] โ
โ โ โโ PostToolUse Hook โ
โ โ โ
โ โโโ ้่ฏฏๆขๅค: 7 ไธช continue ็ซ็น โ
โ โโโ Stop Hook โ ็ปๆญขๆ็ปง็ปญ โ
โ โ
โ ็ปๆญข โ SessionEnd Hook โ ่ฝฌๅฝไฟๅญ โ ้ๅบ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2.6 Message Type System
Claude Code defines a rich message type system, with each type following a distinct processing path within the Agent Loop:
// src/types/message.ts โ ๆถๆฏ็ฑปๅๅฑๆฌก
type Message =
| UserMessage // ไบบ็ฑป่พๅ
ฅ๏ผๆๅทฅๅ
ท็ปๆ๏ผ
| AssistantMessage // ๆจกๅๅๅบ๏ผๆๆฌ + ๅทฅๅ
ท่ฐ็จ๏ผ
| AttachmentMessage // ่ฎฐๅฟ/่ตๆบ้ไปถ
| SystemMessage // ็ณป็ปๆถๆฏ
| SystemLocalCommandMessage // ๆฌๅฐๅทฅๅ
ท็ปๆ๏ผbash, read ็ญ๏ผ
| ToolUseSummaryMessage // ๅ็ผฉๅ็ๅทฅๅ
ทๅๅฒ
| TombstoneMessage // ๅทฒๅ ้คๆถๆฏๆ ่ฎฐ
| ProgressMessage // ๆตๅผ่ฟๅบฆๆดๆฐ
Message normalization (normalizeMessagesForAPI) is a complex pipeline that handles:
- Consecutive user message merging: Bedrock does not support multiple consecutive user messages, so the API layer merges them
- PDF/image error content stripping: If an uploaded PDF is too large and triggers an error, subsequent turns automatically strip that content to prevent redundant transmission
- Tool name normalization: Aliases (e.g., legacy names) are mapped to current canonical names
- Tool Reference handling: When Tool Search is enabled, reference blocks are preserved; when disabled, they are stripped
- Virtual message filtering: Display messages from REPL-internal tool calls are not sent to the API
Chapter 3: Agent Loop โ The Heart of the Harness
If the Harness is an automobile, the Agent Loop is its engine. No matter how luxurious the seats or how advanced the airbags, without an engine the car cannot move.
This is the most important chapter in the entire book. We will dissect Claude Code's core loop โ
queryLoop()โ line by line, examining how a singlewhile(true)drives the entire AI coding assistant. By the end of this chapter, you will have a thorough understanding of "how an Agent works," from the lowest level to the highest.
The Agent Loop is the most critical component of the entire Harness. In Claude Code, it is implemented as the queryLoop() function in src/query.ts.
3.1 Basic Architecture: Infinite Loop + Async Generator
Below is the actual signature and initialization of queryLoop from Claude Code's real source code (src/query.ts):
// src/query.ts โ ็ๅฎ็ๅฝๆฐ็ญพๅ
async function* queryLoop(
params: QueryParams,
consumedCommandUuids: string[],
): AsyncGenerator<
| StreamEvent
| RequestStartEvent
| Message
| TombstoneMessage
| ToolUseSummaryMessage,
Terminal
> {
// ===== ไธๅฏๅๅๆฐ โ ๅพช็ฏๆ้ดๆฐธไธ้ๆฐ่ตๅผ =====
const {
systemPrompt, userContext, systemContext,
canUseTool, fallbackModel, querySource,
maxTurns, skipCacheWrite,
} = params
const deps = params.deps ?? productionDeps()
// ===== ๅฏๅ่ทจ่ฟญไปฃ็ถๆ =====
// ๅพช็ฏไฝๅจๆฏๆฌก่ฟญไปฃๅผๅงๆถ่งฃๆๆญคๅฏน่ฑกไปฅไฟๆ่ฃธๅ่ฏปๅใ
// Continue ็ซ็นๅๅ
ฅ `state = { ... }` ่ไธๆฏ 9 ไธช็ฌ็ซ่ตๅผใ
let state: State = {
messages: params.messages,
toolUseContext: params.toolUseContext,
maxOutputTokensOverride: params.maxOutputTokensOverride,
autoCompactTracking: undefined,
stopHookActive: undefined,
maxOutputTokensRecoveryCount: 0,
hasAttemptedReactiveCompact: false,
turnCount: 1,
pendingToolUseSummary: undefined,
transition: undefined, // ไธบไปไนไธๆฌก่ฟญไปฃ continue ไบ
}
// ้ข็ฎ่ท่ธช่ทจๅ็ผฉ่พน็๏ผๅพช็ฏๅฑ้จ๏ผไธๅจ State ไธ๏ผ
let taskBudgetRemaining: number | undefined = undefined
// ๆฅ่ฏข้
็ฝฎๅฟซ็
ง๏ผไธๆฌกๆงๆ่ท็ฏๅข/statsig/ไผ่ฏ็ถๆ๏ผ
const config = buildQueryConfig()
// ่ฎฐๅฟ้ขๅ๏ผไฝฟ็จ `using` ็กฎไฟๅจ็ๆๅจ้ๅบๆถๆธ
็๏ผ
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
state.messages, state.toolUseContext,
)
while (true) {
// ... ๅพช็ฏไฝ๏ผไธๆ่ฏฆ่งฃ๏ผ
}
}
State Type Definition (this is the "skeleton" of the loop):
type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number
hasAttemptedReactiveCompact: boolean
maxOutputTokensOverride: number | undefined
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
stopHookActive: boolean | undefined
turnCount: number
transition: Continue | undefined // ไธๆฌก่ฟญไปฃไธบไฝ continue
}
The state diagram below illustrates the complete lifecycle of queryLoop โ each state corresponds to a phase within the loop, and each edge corresponds to a transition reason:
stateDiagram-v2
[*] --> Compaction: ่ฟๅ
ฅๅพช็ฏ
Compaction --> APICall: ๅ็ผฉๅฎๆ
APICall --> ToolExecution: ๆ tool_use ๅ
APICall --> StopHooks: ๆ tool_use ๅ
APICall --> CollapseRetry: 413 ้่ฏฏ
APICall --> ReactiveCompact: collapse ๅคฑ่ดฅ
APICall --> EscalateTokens: max_output_tokens
APICall --> MultiTurnRetry: ๅ็บงๅไปๆชๆญ
APICall --> FallbackModel: FallbackTriggeredError
CollapseRetry --> Compaction: continue site 1
ReactiveCompact --> Compaction: continue site 2
EscalateTokens --> Compaction: continue site 3
MultiTurnRetry --> Compaction: continue site 4
FallbackModel --> Compaction: continue site 6
ToolExecution --> Compaction: continue site 7\n๏ผๆญฃๅธธไธไธ่ฝฎ๏ผ
StopHooks --> [*]: ๆญฃๅธธๅฎๆ
StopHooks --> Compaction: blocking error\ncontinue site 5
StopHooks --> [*]: hook ้ปๆญข็ปง็ปญ
ReactiveCompact --> [*]: ๆขๅคๅคฑ่ดฅ
MultiTurnRetry --> [*]: ้่ฏ 3 ๆฌกๅ่ๅฐฝ
Simplified Logic Flow (for comprehension):
while (true) {
// 1. ่งฃๆ็ถๆ
const { messages, toolUseContext, ... } = state;
// 2. ๅ็ผฉ็ฎก้
// 3. ๆๅปบ็ณป็ปๆ็คบ + ่ง่ๅๆถๆฏ
// 4. ่ฐ็จ LLM API๏ผๆตๅผ๏ผ
// 5. ๆถ้ tool_use ๅ
// 6. ้่ฏฏๆขๅค๏ผ7 ไธช continue ็ซ็น๏ผ
// 7. ๅทฅๅ
ทๆง่ก
// 8. Stop Hook โ ็ปๆญขๆ็ปง็ปญ
// 9. ๆดๆฐ็ถๆ โ continue
}
Design Philosophy:
- Async Generator: Rather than returning a final result, the loop
yields every intermediate event (stream events, messages, tombstone messages). This allows the client to begin rendering before the API call completes. - Infinite loop + explicit exit: The loop exits only on
return Terminal. This is more flexible than a bounded loop, since many recovery paths require re-iteration. - Single State object: State is destructured at the start of each iteration and reassigned wholesale at continue sites, maintaining pseudo-immutable semantics.
3.2 The Seven Continue Sites
Claude Code's queryLoop has 7+ continue sites, each corresponding to a distinct recovery scenario:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ queryLoop() โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 1: Proactive Compaction โ โ
โ โ Trigger: tokens exceed threshold โ โ
โ โ Action: autocompact โ new messages โ โ โ
โ โ continue โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 2: Prompt Too Long โ โ
โ โ Trigger: API returns prompt-too-long โ โ
โ โ Action: context-collapse โ reactive โ โ
โ โ compact โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 3: Max Output Tokens โ โ
โ โ Trigger: model output truncated โ โ
โ โ Action: escalate 8kโ64k โ multi-turn โ โ
โ โ retry (up to 3 times) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 4: Fallback Model โ โ
โ โ Trigger: FallbackTriggeredError โ โ
โ โ Action: switch model โ retry request โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 5: Stop Hook Blocking โ โ
โ โ Trigger: user Hook requests additional โ โ
โ โ turns โ โ
โ โ Action: inject Hook message โ continue โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 6: Image/Media Errors โ โ
โ โ Trigger: ImageSizeError / โ โ
โ โ ImageResizeError โ โ
โ โ Action: reactive compact (remove images) โ โ
โ โ โ continue โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Continue Site 7: Tool Execution โ โ
โ โ Trigger: normal tool execution complete โ โ
โ โ Action: collect results โ update state โ โ โ
โ โ continue โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ return Terminal โ the only exit point โ โ
โ โ Condition: no tool calls + Stop Hook does โ โ
โ โ not block โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
3.3 The Compaction Pipeline
The context window is finite โ even a 1M token window can be filled during long conversations. Claude Code implements a four-level compaction pipeline, one of its most sophisticated subsystems.
flowchart TD
subgraph Pipeline["ๅ็ผฉ็ฎก้๏ผๆฏ่ฝฎ่ฟญไปฃๆง่ก๏ผ"]
direction TB
S["Level 1: Snip\nๅๅฒๆชๆญ\nๆๆฌ: ๆไฝ | ๅปถ่ฟ: ~0ms"]
MC["Level 2: Microcompact\n่ๅๅทฅๅ
ท็ปๆ็ผฉๅ\nๆๆฌ: ไฝ | ๅปถ่ฟ: ~1ms"]
CC["Level 3: Context-Collapse\n่ฏปๆถๆๅฐ๏ผไธไฟฎๆนๆฐ็ป๏ผ\nๆๆฌ: ไธญ | ๅปถ่ฟ: ~5ms"]
AC["Level 4: Autocompact\nLLM ๅ
จๅฏน่ฏๆ่ฆ\nๆๆฌ: ้ซ | ๅปถ่ฟ: ~2s"]
end
S -->|"้ๆพๅฐ้ token"| MC
MC -->|"่พน็ๆถๆฏๅปถ่ฟ"| CC
CC -->|"ๅฆๆไป่ถ
้ๅผ"| AC
CC -->|"ๅฆๆไฝไบ้ๅผ"| Skip["่ทณ่ฟ Autocompact\nไฟ็็ฒๅบฆไธไธๆ"]
classDef light fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef medium fill:#fef9c3,stroke:#ca8a04,color:#713f12
classDef heavy fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef skip fill:#f3f4f6,stroke:#6b7280,color:#374151
class S,MC light
class CC medium
class AC heavy
class Skip skip
Source Code Annotation Analysis: Regarding the execution order of Microcompact and Snip, the source code comments: "Apply snip before microcompact (both may run โ they are not mutually exclusive)... snipTokensFreed is plumbed to autocompact: snip's threshold check must reflect what snip removed." This reveals a subtle data-flow dependency: the number of tokens freed by Snip must be propagated to Autocompact's threshold check; otherwise Autocompact would underestimate the freed space, triggering unnecessary full-conversation summarization.
Regarding Context-Collapse, the comments state: "Nothing is yielded โ the collapsed view is a read-time projection... summary messages live in the collapse store, not the REPL array." This means Level 3 modifies no data structures โ it only changes how data is "read." This design allows collapse to persist across turns and remain fully reversible.
Each level operates independently, but a strict execution order constraint applies:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Compaction Pipeline โ
โ โ
โ Level 1: Snip Compact (every turn) โ
โ โโ Feature-gated history truncation โ
โ โโ Tracks freed token count โ
โ โโ Lightest weight, near-zero latency โ
โ โ
โ Level 2: Microcompact (every turn) โ
โ โโ Replaces tool results 3+ turns old with โ
โ โ "[Previous: used {tool}]" โ
โ โโ Caches compacted results โ
โ โโ Defers boundary messages until API response โ
โ (when cache_deleted_input is known) โ
โ โ
โ Level 3: Context-Collapse (read-time projection) โ
โ โโ Does not modify the message array; projects at โ
โ โ read time instead โ
โ โโ Progressively drains collapsible context by โ
โ โ granularity โ
โ โโ Low cost, incremental โ
โ โ
โ Level 4: Autocompact (triggered at >50k tokens) โ
โ โโ Saves full transcript to disk โ
โ โโ LLM summarizes all messages โ
โ โโ Replaces all messages with summary โ
โ โโ Heaviest weight, but frees the most space โ
โ โ
โ Execution order: snip โ micro โ context-collapse โ โ
โ auto โ
โ Levels are not mutually exclusive and can run in โ
โ combination โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Design Philosophy:
- Progressive: Attempts lightweight operations first, escalating to heavy operations only when necessary
- Boundary deferral: Microcompact's boundary messages are deferred until after the API response, since cache hit information is available only at that point
- Budget tracking across compaction boundaries:
taskBudgetRemainingcaptures the final window before compaction, accumulating across multiple compaction events
3.4 Tool Execution Orchestration
Claude Code features two tool execution modes, both of which coexist in production:
Mode 1: StreamingToolExecutor (Default โ Execute While Streaming)
// src/services/tools/toolOrchestration.ts โ ็ๅฎไปฃ็
export class StreamingToolExecutor {
private tools: TrackedTool[] = []
private toolUseContext: ToolUseContext
private hasErrored = false
// ๅญ AbortController๏ผๅฝไธไธช Bash ๅทฅๅ
ทๅบ้ๆถ๏ผ
// ๅ
ๅผๅญ่ฟ็จ็ซๅณๆญปไบก๏ผไฝไธไธญๆญข็ถ็บงๆฅ่ฏข
private siblingAbortController: AbortController
private discarded = false
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
const toolDefinition = findToolByName(this.toolDefinitions, block.name)
if (!toolDefinition) {
// ๅทฅๅ
ทไธๅญๅจ โ ็ซๅณๅๅปบ้่ฏฏ็ปๆ
this.tools.push({
id: block.id, block, assistantMessage,
status: 'completed',
isConcurrencySafe: true,
pendingProgress: [],
results: [createUserMessage({
content: [{
type: 'tool_result',
content: `<tool_use_error>Error: No such tool: ${block.name}</tool_use_error>`,
is_error: true,
tool_use_id: block.id,
}],
})],
})
return
}
// ่งฃๆ่พๅ
ฅๅนถๅคๆญๆฏๅฆๅฏๅนถๅ
const parsedInput = toolDefinition.inputSchema.safeParse(block.input)
const isConcurrencySafe = parsedInput?.success
? Boolean(toolDefinition.isConcurrencySafe(parsedInput.data))
: false
this.tools.push({
id: block.id, block, assistantMessage,
status: 'queued', isConcurrencySafe,
pendingProgress: [],
})
void this.processQueue() // ็ซๅณๅผๅงๅค็
}
// ๅฝๆตๅผๅ้ๅ็ๆถ๏ผไธขๅผๆๆๅพ
ๅค็ๅ่ฟ่กไธญ็ๅทฅๅ
ท
discard(): void { this.discarded = true }
}
Key design: addTool() is called during the model's streaming generation. Whenever a complete tool_use JSON block is identified, the tool is immediately queued for execution. While the model is still generating the second tool call, the first is already running.
Mode 2: runTools() (Fallback โ Execute After Partitioning)
// src/services/tools/toolOrchestration.ts โ ็ๅฎไปฃ็
export async function* runTools(
toolUseMessages: ToolUseBlock[],
assistantMessages: AssistantMessage[],
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
let currentContext = toolUseContext
// ๆ ธๅฟ่ฎพ่ฎก๏ผๅทฅๅ
ทๅๅบ
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
toolUseMessages, currentContext,
)) {
if (isConcurrencySafe) {
// ===== ๅช่ฏปๆนๆฌก๏ผๅนถๅๆง่ก =====
const queuedContextModifiers: Record<string, ((ctx) => ctx)[]> = {}
for await (const update of runToolsConcurrently(blocks, ...)) {
if (update.contextModifier) {
// ๆถ้ไธไธๆไฟฎๆนๅจ๏ผๅปถ่ฟๅบ็จ
queuedContextModifiers[update.contextModifier.toolUseID]
?.push(update.contextModifier.modifyContext)
}
yield { message: update.message, newContext: currentContext }
}
// ๆนๆฌกๅฎๆๅ๏ผๆ้กบๅบๅบ็จๆๆไธไธๆไฟฎๆน
for (const block of blocks) {
for (const modifier of queuedContextModifiers[block.id] ?? []) {
currentContext = modifier(currentContext)
}
}
} else {
// ===== ๅๅ
ฅๆนๆฌก๏ผไธฒ่กๆง่ก =====
for await (const update of runToolsSerially(blocks, ...)) {
if (update.newContext) currentContext = update.newContext
yield { message: update.message, newContext: currentContext }
}
}
}
}
Tool Partitioning Algorithm (partitionToolCalls):
Input: [Read("a.ts"), Read("b.ts"), Write("c.ts"), Read("d.ts")]
Partition result:
Batch 1: { isConcurrencySafe: true, blocks: [Read("a.ts"), Read("b.ts")] }
Batch 2: { isConcurrencySafe: false, blocks: [Write("c.ts")] }
Batch 3: { isConcurrencySafe: true, blocks: [Read("d.ts")] }
Execution order: Batch 1 concurrent โ Batch 2 serial โ Batch 3 concurrent
Design Philosophy: Read-only tools (Read, Glob, Grep) are inherently safe for concurrent execution โ they do not modify state. Write tools (Write, Edit, Bash) must execute serially because they may depend on the side effects of preceding tools. The partitioning algorithm groups consecutive tools of the same type, achieving an optimal balance between safety and performance. Context modifiers (contextModifier) are collected and applied lazily, ensuring context consistency during concurrent execution.
Why does this matter? Suppose the Agent needs to read 10 files to answer an architecture question. Without tool partitioning, these 10 Read operations would execute sequentially โ each potentially taking tens of milliseconds. With partitioning, they execute concurrently, and total time approximates that of the slowest individual read. In practice, this reduces "codebase reading" operations from seconds to milliseconds. This optimization produces a "perceptual fluency" for the user โ you need not understand the mechanism, but you notice the speed.
3.5 Error Recovery Cascade (with Real Source Code)
Claude Code implements a cascading recovery strategy for recoverable errors. The following is real code from src/query.ts:
Prompt Too Long (413) Recovery
// src/query.ts โ ็ๅฎ็ 413 ๆขๅคไปฃ็
if (isWithheld413) {
// ็ฌฌ 1 ๆญฅ: ๆ็ฉบ context-collapse ้ๅ
// ๅชๆๅจไธๆฌก transition ไธๆฏ collapse_drain_retry ๆถๆๅฐ่ฏ
// ๏ผๅฆๆๅทฒ็ปๆ็ฉบ่ฟไฝไป็ถ 413๏ผ่ทณ่ฟ็ดๆฅ่ฟๅ
ฅ reactive compact๏ผ
if (feature('CONTEXT_COLLAPSE') && contextCollapse
&& state.transition?.reason !== 'collapse_drain_retry') {
const drained = contextCollapse.recoverFromOverflow(messagesForQuery, querySource)
if (drained.committed > 0) {
state = { ...state,
messages: drained.messages,
transition: { reason: 'collapse_drain_retry', committed: drained.committed },
}
continue // โ Continue Site 1
}
}
}
// ็ฌฌ 2 ๆญฅ: Reactive Compact๏ผๅฎๆดๆ่ฆ๏ผ
if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
const compacted = await reactiveCompact.tryReactiveCompact({
hasAttempted: hasAttemptedReactiveCompact, // ้ฒๆญขๆ ้ๅพช็ฏ
querySource,
aborted: toolUseContext.abortController.signal.aborted,
messages: messagesForQuery,
cacheSafeParams: { systemPrompt, userContext, systemContext, ... },
})
if (compacted) {
// ้ข็ฎ่ท่ธช๏ผๆ่ทๅ็ผฉๅ็ๆ็ปไธไธๆ็ชๅฃ
if (params.taskBudget) {
const preCompactContext = finalContextTokensFromLastResponse(messagesForQuery)
taskBudgetRemaining = Math.max(0,
(taskBudgetRemaining ?? params.taskBudget.total) - preCompactContext)
}
const postCompactMessages = buildPostCompactMessages(compacted)
for (const msg of postCompactMessages) { yield msg }
state = { ...state,
messages: postCompactMessages,
hasAttemptedReactiveCompact: true,
transition: { reason: 'reactive_compact_retry' },
}
continue // โ Continue Site 2
}
// ็ฌฌ 3 ๆญฅ: ๆๆๆขๅคๅคฑ่ดฅ โ ๅ็จๆทๆฅๅ
// ๅ
ณ้ฎ๏ผไธ่ฆ่ฟๅ
ฅ Stop Hooks๏ผๆจกๅไปๆชไบง็ๆๆๅๅบ๏ผ
// Stop Hooks ๆ ๆณๆๆไนๅฐ่ฏไผฐใ่ฟ่ก Stop Hooks ไผ้ ๆๆญปไบก่บๆ๏ผ
// error โ hook blocking โ retry โ error โ ...
yield lastMessage
void executeStopFailureHooks(lastMessage, toolUseContext)
return { reason: 'prompt_too_long' }
}
Max Output Tokens Recovery
// src/query.ts โ ็ๅฎ็ max_output_tokens ๆขๅคไปฃ็
if (isWithheldMaxOutputTokens(lastMessage)) {
// ๅ็บง้่ฏ๏ผๅฆๆไฝฟ็จไบ้ป่ฎค็ 8k ไธ้๏ผๅ็บงๅฐ 64k ้่ฏ **ๅไธ่ฏทๆฑ**
// ๆ meta ๆถๆฏ๏ผๆ ๅค่ฝฎไบคไบ
const capEnabled = getFeatureValue_CACHED_MAY_BE_STALE('tengu_otk_slot_v1', false)
if (capEnabled && maxOutputTokensOverride === undefined
&& !process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
logEvent('tengu_max_tokens_escalate', { escalatedTo: ESCALATED_MAX_TOKENS })
state = { ...state,
maxOutputTokensOverride: ESCALATED_MAX_TOKENS, // 64k
transition: { reason: 'max_output_tokens_escalate' },
}
continue // โ Continue Site 3
}
// ๅค่ฝฎๆขๅค๏ผๆณจๅ
ฅๆขๅคๆถๆฏ๏ผ่ฆๆฑๆจกๅไปๆญ็น็ปง็ปญ
if (maxOutputTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) { // ้ๅถ 3 ๆฌก
const recoveryMessage = createUserMessage({
content: `Output token limit hit. Resume directly โ no apology, no recap. ` +
`Pick up mid-thought if that is where the cut happened. ` +
`Break remaining work into smaller pieces.`,
isMeta: true,
})
state = { ...state,
messages: [...messagesForQuery, ...assistantMessages, recoveryMessage],
maxOutputTokensRecoveryCount: maxOutputTokensRecoveryCount + 1,
transition: {
reason: 'max_output_tokens_recovery',
attempt: maxOutputTokensRecoveryCount + 1,
},
}
continue // โ Continue Site 4
}
// ๆขๅค่ๅฐฝ โ ๅฑ็คบ่ขซๆชๆญ็้่ฏฏ
yield lastMessage
}
Stop Hook Recovery
// src/query.ts โ Stop Hook ้ปๆญขๅ็ๆขๅค
const stopHookResult = yield* handleStopHooks(
messagesForQuery, assistantMessages, systemPrompt,
userContext, systemContext, toolUseContext, querySource, stopHookActive,
)
if (stopHookResult.preventContinuation) {
return { reason: 'stop_hook_prevented' }
}
if (stopHookResult.blockingErrors.length > 0) {
state = { ...state,
messages: [...messagesForQuery, ...assistantMessages, ...stopHookResult.blockingErrors],
maxOutputTokensRecoveryCount: 0,
// ๅ
ณ้ฎ๏ผไฟ็ hasAttemptedReactiveCompact ๆ ๅฟ๏ผ
// ๅฆๆ compact ๅทฒ็ป่ฟ่กไฝๆ ๆณๆขๅค prompt-too-long๏ผ
// ้็ฝฎๆญคๆ ๅฟไผๅฏผ่ดๆ ้ๅพช็ฏ๏ผ
// compact โ ไป็ถๅคช้ฟ โ error โ stop hook โ compact โ ...
hasAttemptedReactiveCompact,
stopHookActive: true,
transition: { reason: 'stop_hook_blocking' },
}
continue // โ Continue Site 5
}
All Termination Reasons
// query.ts ไธญ็ 10 ็ง็ปๆญขๅๅ
return { reason: 'completed' } // ๆญฃๅธธๅฎๆ๏ผๆ ๅทฅๅ
ท่ฐ็จ + Stop Hook ไธ้ปๆญข๏ผ
return { reason: 'blocking_limit' } // ็กฌๆง token ้ๅถ
return { reason: 'stop_hook_prevented' } // Stop Hook ้ปๆญข็ปง็ปญ
return { reason: 'aborted_streaming' } // ็จๆทไธญๆญ๏ผๆจกๅๅๅบไธญ๏ผ
return { reason: 'aborted_tools' } // ็จๆทไธญๆญ๏ผๅทฅๅ
ทๆง่กไธญ๏ผ
return { reason: 'hook_stopped' } // Hook ้ไปถๅๆญข็ปง็ปญ
return { reason: 'max_turns', turnCount }// ่พพๅฐๆๅคง่ฝฎๆฌก้ๅถ
return { reason: 'prompt_too_long' } // 413 ๆขๅค่ๅฐฝ
return { reason: 'image_error' } // ๅพ็/PDF ๅคชๅคง
return { reason: 'model_error', error } // ๆๅคๅผๅธธ
In-Depth Source Code Annotation Analysis:
The following insights are drawn from internal developer comments within the Claude Code source code, revealing real-world engineering challenges encountered in production:
1. Error Withholding Strategy
The source code comments: "yielding early would leak intermediate errors to consumers like cowork/desktop that terminate on any error field, even though recovery is still running."
This means that queryLoop does not yield immediately upon discovering an error. Instead, it withholds the error, waiting until the recovery flow completes before deciding whether to expose it. This prevents downstream consumers (IDE extensions, desktop applications) from seeing intermediate errors and terminating prematurely.
2. Budget Tracking Across Compaction Boundaries
The source code comments: "remaining is undefined until first compact fires โ before compact the server sees full history and counts down from {total} itself (see api/api/sampling/prompt/renderer.py:292); after compact, server only sees summary and would under-count spend."
This reveals an elegant server-client coordination design: before compaction, the server can see the full history and compute budget consumption itself; after compaction, the server can only see the summary, so the client must inform the server "how much the part you can no longer see has consumed."
3. Why hasAttemptedReactiveCompact Is Not Reset
Note that in the Stop Hook recovery path, the hasAttemptedReactiveCompact flag is preserved rather than reset. The source code explains: if compact has already run but failed to recover from prompt-too-long, resetting this flag would cause an infinite loop: compact -> still too long -> error -> stop hook -> compact -> ... This is a fix for a real production bug.
4. The using Semantics of Memory Prefetch
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(...) employs the TC39 Explicit Resource Management proposal (using keyword). The source code comments: "Fired once per user turn โ the prompt is invariant across loop iterations, so per-iteration firing would ask sideQuery the same question N times." using ensures that prefetch resources are automatically cleaned up when the generator exits (whether normally or abnormally).
Pedagogical Takeaway: These details reveal a core principle โ the complexity of a production-grade Agent Loop lies not in "the loop itself" but in "how to recover gracefully when the loop fails." A 30-line while(true) suffices for a basic Agent Loop, but handling all edge cases properly requires 1,800+ lines. The gap between the two represents the entire value of Harness Engineering.
3.6 Pause and Reflect
Having studied the Agent Loop, attempt to answer the following:
- Why does queryLoop use
while(true)instead of recursion? (Hint: consider memory and stack depth)- Why does the compaction pipeline have 4 levels instead of 1? (Hint: consider the trade-off between cost and latency)
- If you were to add a new recovery path (e.g., "API key expired"), which parts of the code would you need to modify?
These questions have no canonical answers, but reflecting on them will deepen your understanding of the "why" rather than merely the "how."
Figure 3-2: (Left) Complexity comparison between the minimal implementation and the production implementation (logarithmic scale) โ a 60x increase in lines of code, where each dimension of growth corresponds to a real production requirement. (Right) Token release efficiency of the four-level compaction pipeline โ from 180K progressively reduced to 45K.
Figure 3-3: Estimated trigger frequency of the 7 continue sites โ "Next Turn" (normal progression) accounts for 95%. Error recovery sites collectively account for approximately 5%, yet it is precisely this 5% of the code (approximately 500 lines) that prevents session interruptions and cost overruns.
Figure 3-1: The queryLoop() state machine โ showing the complete flow of 7 continue sites, the 4-level compaction pipeline, StreamingToolExecutor parallel execution, and 10 termination reasons.
3.7 Quantitative Analysis: Complexity Metrics of the Agent Loop
To quantify the gap between a "minimal Agent Loop" and a "production Agent Loop," we conducted a code metrics analysis of src/query.ts:
| Metric | Minimal Implementation (s01) | Claude Code Production Implementation |
|---|---|---|
| Lines of Code | 30 lines | 1,800+ lines |
| Continue Sites | 1 | 7 |
| Termination Reasons | 1 (completed) | 10 |
| Error Recovery Paths | 0 | 5 cascading recoveries |
| Compaction Strategies | 0 levels | 4-level pipeline |
| Concurrency Modes | Serial | 2 (Streaming + Sequential) |
| State Fields | 1 (messages) | 10 (State type) |
| Analytics Instrumentation Points | 0 | 15+ |
| Feature Gates | 0 | 8+ |
Complexity Growth Analysis: The growth from 30 lines to 1,800 lines represents a 60x increase. But this is not "over-engineering" โ every line corresponds to a real production problem. For example: - The
hasAttemptedReactiveCompactflag is only 1 line, yet it prevents an infinite-loop bug that could consume thousands of dollars in API costs. - ThetaskBudgetRemainingtracking logic is approximately 20 lines, yet it is the only mechanism capable of correctly computing token consumption across compaction boundaries. - The StreamingToolExecutor is approximately 200 lines, yet it reduces multi-tool execution latency from O(n) to O(1) (the time of the slowest tool).
3.8 Summary of Agent Loop Design Philosophy
- Resilience over rigidity: 7+ continue sites enable recovery from nearly any error
- Progressive degradation: Each error type first attempts the lightest recovery, escalating gradually
- Streaming-first: The Async Generator makes every intermediate state observable
- Explicit state: A single State object with no implicit global state
- Built-in observability: Every recovery point includes analytics and profiling
Chapter 4: Tool System โ The Agent's Hands
The Agent Loop is the engine, while the tool system is the steering wheel and throttle. No matter how powerful the engine, the vehicle cannot reach its destination without the ability to steer and modulate speed.
In Claude Code, the model (LLM) itself cannot read files, run commands, or search code. Its sole capability is generating text. Through the tool system, however, these text outputs are translated into real operations โ reading a file, editing a line of code, running a test.
In this chapter we examine how Claude Code designs a system of 43+ tools, each independently self-contained yet uniformly managed. These design patterns can be directly reused in your own Agent projects.
The tool system is the sole channel through which the Agent interacts with the external world in the Harness. Claude Code implements a system of 43+ tools, each a self-contained module.
4.1 Tool Interface Definition
Located in src/Tool.ts, this is the base type for all tools:
type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
// ===== ๆ ธๅฟๆ ่ฏ =====
name: string; // ๅทฅๅ
ทๅ็งฐ๏ผไธปๆ ่ฏ็ฌฆ๏ผ
aliases?: string[]; // ๅซๅ๏ผๅๅๅ
ผๅฎน๏ผ
userFacingName(): string; // ๆพ็คบๅ็งฐ
// ===== Schema & ้ช่ฏ =====
inputSchema: ZodType<Input>; // Zod ่พๅ
ฅ้ช่ฏ
inputJSONSchema?: JSONSchema; // ๅฏ้ JSON Schema๏ผMCP ๅทฅๅ
ท๏ผ
outputSchema?: ZodType<Output>; // ๅฏ้่พๅบ็ฑปๅ
validateInput(input): Promise<ValidationResult>;
// ===== ๆง่ก =====
call(
args: Input,
context: ToolUseContext,
canUseTool: CanUseTool,
parentMessage: AssistantMessage,
progressCallback?: ProgressCallback<P>,
): Promise<ToolResult<Output>>;
// ===== ๆ้ & ๅฎๅ
จ =====
checkPermissions(args, context): Promise<PermissionDecision>;
isConcurrencySafe(args): boolean; // ่ฝๅฆๅนถ่กๆง่ก
isDestructive(args): boolean; // ไธๅฏ้ๆไฝ๏ผ
isReadOnly(): boolean; // ๅช่ฏปๆไฝ๏ผ
preparePermissionMatcher(args): string; // Hook ๆจกๅผๅน้
// ===== ่กไธบ =====
isEnabled(): boolean; // ็นๆง้จๆงๆฃๆฅ
interruptBehavior(): 'cancel' | 'block';
requiresUserInteraction(): boolean;
// ===== ๆธฒๆ =====
renderToolUseMessage(args): ReactElement;
renderToolResultMessage(result): ReactElement;
renderToolUseProgressMessage(progress): ReactElement;
// ===== ๆ็ดข & ๆๅ =====
searchHint: string; // ToolSearch ็ 3-10 ่ฏๅ
ณ้ฎ่ฏ
shouldDefer: boolean; // ๅปถ่ฟๅ ่ฝฝ
alwaysLoad: boolean; // ๆฐธไธๅปถ่ฟ
// ===== ๆ่ฟฐ๏ผๅจๆ็ๆ๏ผ=====
description(isNonInteractive?: boolean): string;
prompt(context): string; // ็ณป็ปๆ็คบ็ๆฎต
};
Design Philosophy:
- Self-describing: Each tool carries its own schema, description, permission logic, and rendering functions
- Dual schema support: Zod (internal validation) + JSON Schema (MCP interoperability)
- Dynamic descriptions:
description()accepts an interaction mode parameter; UI-related explanations can be omitted in non-interactive mode - Lazy loading:
shouldDeferensures infrequently used tools are not loaded into the model context on the first turn, conserving tokens
Common Beginner Misconception: Many people focus exclusively on the
call()method when designing tool systems โ "what can the tool do." In production, however, permission checking, input validation, and progress rendering account for 80% of tool code. A well-designed tool interface is not merely about "execution" but about "executing safely, observably, and interruptibly."
4.2 Tool Registry
Located in src/tools.ts:
// ๅฏไธ็ๅทฅๅ
ท็ๅฎๆฅๆบ
function getAllBaseTools(): Tool[] {
return [
// === ๅง็ปๅ ่ฝฝ ===
AgentTool,
TaskOutputTool,
BashTool,
FileReadTool,
FileEditTool,
FileWriteTool,
WebFetchTool,
WebSearchTool,
AskUserQuestionTool,
SkillTool,
// ...ๆดๅคๅง็ปๅฏ็จ็ๅทฅๅ
ท
// === ็นๆง้จๆง ===
...(feature('PROACTIVE') ? [SleepTool] : []),
...(feature('AGENT_TRIGGERS') ? [ScheduleCronTool] : []),
...(feature('COORDINATOR_MODE') ? [TeamCreateTool, TeamDeleteTool] : []),
...(isReplModeEnabled() ? [REPLTool] : []),
// ...ๆดๅคๆกไปถๅทฅๅ
ท
];
}
Dead Code Elimination:
// Bun ็ bun:bundle ๅจ็ผ่ฏๆถ่ฏไผฐ feature() ่ฐ็จ
// ๅฆๆ feature('PROACTIVE') ็ผ่ฏไธบ false:
...(false ? [SleepTool] : [])
// โ SleepTool ็ๅ
จ้จไปฃ็ ่ขซ tree-shake ็งป้ค
// ๅ
ๆฌๅ
ถๅผ็จ็ๆๆๅญ็ฌฆไธฒๅไพ่ต
This is a key design pattern in Claude Code: compile-time feature gating. External distributions can remove entire subsystems by setting feature flags, without the need to manually delete code.
4.3 Tool Pool Assembly
function assembleToolPool(builtInTools: Tool[], mcpTools: Tool[]): Tool[] {
// 1. ่ฟๆปค่ขซ deny ่งๅ็ฆๆญข็ MCP ๅทฅๅ
ท
const filteredMcp = mcpTools.filter(t => !getDenyRuleForTool(t));
// 2. ๅๅซๆๅบ๏ผไฟๆ prompt cache ็จณๅฎๆง๏ผ
const sortedBuiltIn = sortBy(builtInTools, t => t.name);
const sortedMcp = sortBy(filteredMcp, t => t.name);
// 3. ่ฟๆฅ๏ผๅ
็ฝฎๅทฅๅ
ทๅจๅ๏ผไฝไธบ็ผๅญๅ็ผ๏ผ
const combined = [...sortedBuiltIn, ...sortedMcp];
// 4. ๅป้๏ผๅ
็ฝฎไผๅ
๏ผ
return uniqBy(combined, t => t.name);
}
Cache Stability Design:
Built-in tools, sorted by name, form a stable cache prefix. When MCP tools are added or removed, the prefix remains unchanged, and the Anthropic API's prompt cache is not invalidated. This is a subtle but important performance optimization.
4.4 Tool Execution Lifecycle
Figure 4-1: The 7-step tool execution pipeline โ from Zod Schema validation to the PostToolUse Hook, each step can alter the tool's behavior or block its execution.
The sequence diagram below illustrates the complete path of a tool from request to execution โ note how Hooks intervene at critical junctures:
sequenceDiagram
participant M as Model
participant V as Validator
participant PH as PreToolUse Hook
participant P as Permission Engine
participant S as Sandbox
participant T as Tool.call()
participant AH as PostToolUse Hook
M->>V: tool_use block
V->>V: Zod Schema ้ช่ฏ
alt ้ช่ฏๅคฑ่ดฅ
V-->>M: ๆ ผๅผ้่ฏฏๆถๆฏ
end
V->>V: tool.validateInput()
alt ้ช่ฏๅคฑ่ดฅ
V-->>M: ไธๅก้ป่พ้่ฏฏ
end
V->>PH: ่พๅ
ฅ JSON
PH->>PH: ๆกไปถๅน้
(if ๅญๆฎต)
alt Hook ้ปๆญข
PH-->>M: blocking error
else Hook ไฟฎๆน่พๅ
ฅ
PH->>P: updatedInput
else Hook ๆนๅ
PH->>P: allow (ไฝไธ็ป่ฟ deny ่งๅ)
end
P->>P: deny่งๅ โ ask่งๅ โ ๆจกๅผๆฃๆฅ
alt ๆ้ๆ็ป
P-->>M: ๆ็ปๆถๆฏ + ๅปบ่ฎฎ
end
P->>S: ๅฝไปคๅ
่ฃ
(ไป
BashTool)
S->>S: wrapWithSandbox()
S->>T: ๆฒ็ๅๅฝไปค
T->>T: ๅฎ้
ๆง่ก
T->>AH: ๆง่ก็ปๆ
AH->>AH: ๅฎก่ฎกๆฅๅฟ / ่พๅบไฟฎๆน
AH-->>M: ๆ็ป tool_result
The same process illustrated as a traditional flowchart:
็จๆท/ๆจกๅ่ฏทๆฑๅทฅๅ
ท่ฐ็จ
โ
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. validateInput() โ ็ปๆ้ช่ฏ๏ผๅฟ
ๅกซๅญๆฎตใ่ๅด๏ผ
โ ไฝฟ็จ Zod Schema โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ ้่ฟ
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. checkPermissions() โ ๅทฅๅ
ท็นๅฎ็ๆ้้ป่พ
โ ่ฟๅ allow/ask/deny โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ ๆช่ขซๆ็ป
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. Rule-Based Perms โ ่ฎพ็ฝฎไธญ็ allow/deny/ask ่งๅ
โ checkRuleBasedPerms() โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ ๆช่ขซๆ็ป
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. PreToolUse Hooks โ ็จๆทๅฎไน็ Hook
โ ๅฏๆนๅๆ้ปๆญข โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ ๆช่ขซ้ปๆญข
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 5. User Prompt/Classifierโ ๆ็ปๅฎกๆน๏ผๆ่ชๅจๅ็ฑปๅจ๏ผ
โ auto ๆจกๅผ๏ผYOLO ๅ็ฑปๅจโ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ ๆนๅ
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 6. call() โ ๅฎ้
ๆง่กๅทฅๅ
ท
โ ่ฟๅ ToolResult โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ
v
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 7. PostToolUse Hooks โ ๆง่กๅๅ่ฐ
โ ๅฎก่ฎกๆฅๅฟใ้็ฅ็ญ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
4.5 Tool Execution Pipeline (with Real Code Including Hook Integration)
Tool execution involves far more than simply calling tool.call() โ it is a multi-step pipeline where each step can alter behavior:
// src/services/tools/toolExecution.ts โ ็ๅฎ็ๆง่ก็ฎก้
async function checkPermissionsAndCallTool(
tool: Tool,
toolUseID: string,
input: Record<string, unknown>,
toolUseContext: ToolUseContext,
canUseTool: CanUseToolFn,
assistantMessage: AssistantMessage,
onToolProgress: (progress: ToolProgress) => void,
): Promise<MessageUpdate[]> {
// ===== ๆญฅ้ชค 1: Zod Schema ้ช่ฏ =====
const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
return [{ message: createUserMessage({
content: formatZodValidationError(tool.name, parsedInput.error),
}) }]
}
// ===== ๆญฅ้ชค 2: ๅทฅๅ
ท็นๅฎ้ช่ฏ =====
const isValidCall = await tool.validateInput?.(parsedInput.data, toolUseContext)
if (isValidCall?.result === false) {
return [{ message: createUserMessage({
content: isValidCall.message,
}) }]
}
// ===== ๆญฅ้ชค 3: PreToolUse Hook =====
// Hook ๅฏไปฅ๏ผๆนๅใ้ปๆญขใไฟฎๆน่พๅ
ฅใๆณจๅ
ฅไธไธๆ
let processedInput = parsedInput.data
let hookPermissionResult: PermissionResult | undefined
for await (const result of runPreToolUseHooks(...)) {
switch (result.type) {
case 'hookPermissionResult':
hookPermissionResult = result.hookPermissionResult
break
case 'hookUpdatedInput':
processedInput = result.updatedInput // Hook ไฟฎๆนไบ่พๅ
ฅ๏ผ
break
}
}
// ===== ๆญฅ้ชค 4: ๆ้่งฃๆ๏ผHook + Rules ไบคไบ๏ผ=====
// ๅ
ณ้ฎ่ฎพ่ฎก๏ผHook ็ 'allow' ไธ็ป่ฟ settings.json ็ deny/ask ่งๅ
const { decision, input: callInput } = await resolveHookPermissionDecision(
hookPermissionResult, tool, processedInput,
toolUseContext, canUseTool, assistantMessage, toolUseID,
)
if (decision.behavior !== 'allow') {
return [/* ๆ้่ขซๆ็ป็ๆถๆฏ */]
}
// ===== ๆญฅ้ชค 5: ๅฎ้
ๆง่กๅทฅๅ
ท =====
let toolResult = await tool.call(
callInput, toolUseContext, canUseTool,
assistantMessage, onToolProgress,
)
// ===== ๆญฅ้ชค 6: PostToolUse Hook =====
// Hook ๅฏไปฅ๏ผไฟฎๆน MCP ๅทฅๅ
ท่พๅบใๆณจๅ
ฅ้ขๅคไธไธๆ
for await (const result of runPostToolUseHooks(...)) {
if (result.updatedMCPToolOutput) {
toolResult = { ...toolResult, data: result.updatedMCPToolOutput }
}
}
// ===== ๆญฅ้ชค 7: ่ฝฌๆขไธบ API ๆ ผๅผๅนถ่ฟๅ =====
return resultingMessages
}
Hook Permission Decision Resolution (this is the most nuanced part):
// src/services/tools/toolHooks.ts โ ็ๅฎไปฃ็
export async function resolveHookPermissionDecision(
hookPermissionResult, tool, input, toolUseContext,
canUseTool, assistantMessage, toolUseID,
) {
if (hookPermissionResult?.behavior === 'allow') {
// Hook ่ฏด"ๅ
่ฎธ"โโไฝ่ฟไธๆฏๆ็ปๅคๅณ๏ผ
// deny/ask ่งๅไป็ถ้็จ๏ผๅฎๅ
จไธๅฏๅ้๏ผ
// ๅฆๆๅทฅๅ
ท้่ฆ็จๆทไบคไบ๏ผไธ Hook ๆไพไบ updatedInput๏ผ
// ้ฃไน Hook ๅฐฑๆฏ"็จๆทไบคไบ"๏ผๅฆ headless ๅ
่ฃ
ๅจ๏ผ
const interactionSatisfied =
tool.requiresUserInteraction?.() &&
hookPermissionResult.updatedInput !== undefined
// ๅณไฝฟ Hook ๅ
่ฎธ๏ผไปๆฃๆฅ่งๅ
const ruleCheck = await checkRuleBasedPermissions(tool, input, toolUseContext)
if (ruleCheck?.behavior === 'deny') {
// Deny ่งๅ่ฆ็ Hook ็ allow๏ผ
return { decision: ruleCheck, input }
}
if (ruleCheck?.behavior === 'ask') {
// Ask ่งๅไป้่ฆๅฏน่ฏๆก
return { decision: await canUseTool(...), input }
}
// ๆ ่งๅ้ปๆญข โ Hook ็ allow ็ๆ
return { decision: hookPermissionResult, input }
}
// ... deny ๅ ask ๅค็
}
Core Security Invariant: deny > settings rules > hook allow. Even if a Hook approves an operation, deny rules in settings.json still block it. This prevents malicious Hooks from circumventing security policies.
Why can't a Hook allow override deny rules? This is a real security consideration. Imagine you install a third-party MCP server that provides a PreToolUse Hook returning
allowfor all operations. If Hook allows could override deny rules, this third-party code would gain permissions exceeding your security policy โ it could enable the Agent to perform operations you have explicitly forbidden. Claude Code's design guarantees: deny rules you write in settings.json constitute an inviolable baseline, regardless of any Hook intervention.
4.6 FileEditTool String Replacement Algorithm
The FileEditTool's core algorithm merits separate analysis โ it handles smart quote matching, a genuine engineering challenge:
// src/tools/FileEditTool/utils.ts โ ็ๅฎไปฃ็
// ้ฎ้ข๏ผๆจกๅๆๆถ็ๆ "curly quotes"๏ผๆบ่ฝๅผๅท๏ผ
// ไฝๆไปถไธญๆฏ "straight quotes"๏ผ็ดๅผๅท๏ผ๏ผๆๅไน
const LEFT_DOUBLE_CURLY_QUOTE = '\u201C' // "
const RIGHT_DOUBLE_CURLY_QUOTE = '\u201D' // "
function normalizeQuotes(str: string): string {
return str
.replaceAll('\u2018', "'") // ' โ '
.replaceAll('\u2019', "'") // ' โ '
.replaceAll('\u201C', '"') // " โ "
.replaceAll('\u201D', '"') // " โ "
}
// ไธ้ถๆฎตๆฅๆพ็ฎๆณ
function findActualString(fileContent: string, searchString: string): string | null {
// ้ถๆฎต 1: ็ฒพ็กฎๅน้
if (fileContent.includes(searchString)) return searchString
// ้ถๆฎต 2: ๅผๅท่ง่ๅๅน้
const normalizedSearch = normalizeQuotes(searchString)
const normalizedFile = normalizeQuotes(fileContent)
const searchIndex = normalizedFile.indexOf(normalizedSearch)
if (searchIndex !== -1) {
// ่ฟๅๆไปถไธญ็ **ๅๅง** ๅญ็ฌฆไธฒ๏ผไฟ็ๅๅงๅผๅท้ฃๆ ผ๏ผ
return fileContent.substring(searchIndex, searchIndex + searchString.length)
}
return null
}
// ๆฟๆข็ฎๆณ
function applyEditToFile(
originalContent: string,
oldString: string,
newString: string,
replaceAll: boolean = false,
): string {
const f = replaceAll
? (content, search, replace) => content.replaceAll(search, () => replace)
: (content, search, replace) => content.replace(search, () => replace)
if (newString !== '') return f(originalContent, oldString, newString)
// ่พน็ๆ
ๅต๏ผๅ ้คๆไฝ
// ๅฆๆ oldString ไธไปฅๆข่ก็ปๅฐพ๏ผไฝๆไปถไธญ oldString ๅ้ข็ดง่ทๆข่ก๏ผ
// ๅๆถๅ ้ค้ฃไธชๆข่ก๏ผ้ฒๆญข็ไธ็ฉบ่ก๏ผ
const stripTrailingNewline =
!oldString.endsWith('\n') && originalContent.includes(oldString + '\n')
return stripTrailingNewline
? f(originalContent, oldString + '\n', newString)
: f(originalContent, oldString, newString)
}
Design Wisdom: The use of () => replace rather than passing replace directly prevents special patterns such as $1 and $& in the replacement string from being misinterpreted by JavaScript's regex replacement engine. A subtle but critical safeguard.
4.7 Quantitative Analysis: Tool Execution Performance Characteristics
| Execution Mode | Scenario | Latency Model | Actual Performance |
|---|---|---|---|
| StreamingToolExecutor concurrent | 10 Read tools | O(max(t_i)) | ~50ms (slowest file read time) |
| StreamingToolExecutor serial | 1 Write then 1 Read | O(sum of t_i) | ~80ms (write+read sequential) |
| runTools concurrent batch | 5 Read + 1 Write + 3 Read | O(max(5)) + O(1) + O(max(3)) | ~130ms |
| Internal callback Hook fast path | PostToolUse (all internal Hooks) | O(n) but very fast | ~1.8us (after optimization) |
| External Hook execution | PreToolUse command Hook | O(hook_timeout) | 5-30s (depends on script) |
Source Code Annotation: Regarding the internal Hook fast path, the source code comments: "Fast-path: all hooks are internal callbacks (sessionFileAccessHooks, attributionHooks). These return {} and don't use the abort signal... Measured: 6.01us -> ~1.8us per PostToolUse hit (-70%)." This 70% performance improvement comes from skipping span/progress/abortSignal/JSON parsing โ for PostToolUse Hooks triggered on every tool call, such micro-optimizations produce significant cumulative effects.
Figure 4-2: Category distribution of 43+ tools โ Core I/O (6 tools) has the highest usage frequency, while Advanced (6 tools) are loaded on demand via feature gates.
Figure 4-3: Core tool capability radar chart โ showing dimensions such as concurrency safety, read-only status, and destructiveness. FileReadTool and GrepTool are the "safest" tools (concurrency safe + read-only), while BashTool is the "most dangerous" (potentially destructive + non-read-only + not concurrency safe).
Figure 4-4: Tool execution latency distribution (logarithmic scale) โ ranging from 2us for internal Hooks to 15 seconds for Agent Explore, latencies span 7 orders of magnitude. This explains why StreamingToolExecutor's concurrency optimization is so important โ in multi-tool scenarios, it reduces total latency from the sum of all tools to the time of the slowest tool.
4.8 Tool Classification
| Category | Tools | Characteristics |
|---|---|---|
| Core I/O | BashTool, FileReadTool, FileWriteTool, FileEditTool, GlobTool, GrepTool | Always loaded |
| Agent | AgentTool, SendMessageTool, TeamCreateTool, TeamDeleteTool | Sub-Agent creation and management |
| Workflow | WebFetchTool, WebSearchTool, NotebookEditTool | External resource access |
| Task | TaskCreateTool, TaskUpdateTool, TaskListTool, TaskOutputTool, TaskStopTool | Task management |
| Planning | EnterPlanModeTool, ExitPlanModeTool, TodoWriteTool | Plan mode |
| Advanced | ScheduleCronTool, SleepTool, MonitorTool, REPLTool | Feature-gated |
| Worktree | EnterWorktreeTool, ExitWorktreeTool | Git worktree isolation |
| MCP | MCPTool, ListMcpResourcesTool, ReadMcpResourceTool | MCP protocol |
| Search | ToolSearchTool | Deferred tool discovery |
4.6 Tool Deferral (Lazy Loading)
// ๅนถ้ๆๆๅทฅๅ
ท้ฝๅจ็ฌฌไธ่ฝฎๅ ่ฝฝ
// shouldDefer = true ็ๅทฅๅ
ทไธๅ้็ปๆจกๅ
// ็ดๅฐ ToolSearchTool ่ขซ่ฐ็จๅ็ฐๅฎไปฌ
// ไพ๏ผNotebookEditTool
{
name: 'NotebookEdit',
shouldDefer: true, // ็ฌฌไธ่ฝฎไธๅ ่ฝฝ
searchHint: 'jupyter notebook cell edit insert',
alwaysLoad: false,
}
// ไพ๏ผBashTool
{
name: 'Bash',
shouldDefer: false, // ๅง็ปๅ ่ฝฝ
alwaysLoad: true, // ๆฐธไธๅปถ่ฟ
}
Design Philosophy:
Model tool definitions consume from the token budget. Loading all 43+ tools simultaneously would consume a substantial portion of the context window. Through lazy loading, only core tools (~15) are loaded on the first turn, with the remainder discovered on demand via ToolSearch. This is a canonical application of context engineering.
Chapter 5: Permission Model โ Constraint Architecture
Recall the "taming a horse" metaphor from Chapter 1. By now, our horse (Agent) has an engine (Loop) and a control system (Tools). But what if it can gallop freely and trample the crops? We need fences โ this is the permission model.
This represents the most essential "constraint" pillar of Harness Engineering. A well-designed permission model does not "restrict" the Agent โ it "reduces the Agent's probability of making mistakes." Claude Code's permission system is among the most mature implementations in the industry, revealing a counterintuitive truth: the more precise the constraints, the freer the Agent becomes. Because when you can precisely control risk, you dare to let the Agent do more.
The permission model is the Harness's "safety valve." It determines what the Agent can do, cannot do, and must ask about.
5.1 Five Permission Modes
type PermissionMode =
| 'default' // ๆๆๆไฝๅง็ป่ฏข้ฎ
| 'acceptEdits' // ่ชๅจๆนๅๆไปถ็ผ่พ๏ผๅ
ถไป่ฏข้ฎ
| 'bypassPermissions' // ่ชๅจๆนๅไธๅ๏ผๅฑ้ฉ๏ผ
| 'dontAsk' // ่ชๅจๆ็ป้่ฆ่ฏข้ฎ็ๆไฝ
| 'plan' // ่ฎกๅๆจกๅผ้ๅถ๏ผๅช่ฏป + ่ฎกๅๆไปถ๏ผ
| 'auto' // AI ๅ็ฑปๅจ่ชๅจๅฎกๆน๏ผๅฎ้ชๆง๏ผ
| 'bubble'; // ๅๆณกๅฐ็ถ Agent๏ผๅญ Agent ็จ๏ผ
Design Philosophy:
Modes are not binary (allow/deny) but form a spectrum. default is the safest, suitable for new users; bypassPermissions is appropriate for trusted automation environments; auto is the most interesting โ it uses a two-stage AI classifier to determine whether an operation is safe.
Analogy: Permission modes are akin to driving assistance systems.
defaultis like novice mode โ every lane change requires confirmation.acceptEditsis like adaptive cruise control โ driving straight is automatic, turns are manual.bypassPermissionsis like full self-driving โ you place complete trust in the system.autois the most interesting โ an AI driver is behind the wheel, but it has its own "safety supervisor" (the YOLO classifier) monitoring it.
5.2 Three-Level Rule System
type PermissionRule = {
source: PermissionRuleSource; // ่งๅๆฅๆบ
ruleBehavior: 'allow' | 'deny' | 'ask';
ruleValue: {
toolName: string; // ไพ: "Bash", "Write", "mcp__server"
ruleContent?: string; // ไพ: "git *", "*.ts", "prefix:npm *"
};
};
Rule syntax examples:
# ๅ
่ฎธๆๆ git ๅฝไปค
Bash(git *)
# ๅ
่ฎธๅๅ
ฅ TypeScript ๆไปถ
Write(*.ts)
# ๆ็ปๆๆ MCP ๆๅกๅจๅทฅๅ
ท
mcp__*
# ๅ
่ฎธ่ฏปๅไปปไฝๆไปถ
Read
# ๆ็ป rm -rf ๅฝไปค
Bash(rm -rf *)
# ๅ
่ฎธ็นๅฎ MCP ๆๅกๅจ็ๆๆๅทฅๅ
ท
mcp__my-server(*)
5.3 Defense-in-Depth Model
Figure 5-1: The six-layer defense-in-depth security model โ from soft constraints (CLAUDE.md, ~95% compliance rate) to hard constraints (hardcoded denials, 100% unbypassable). The cumulative stacking of layers drives the overall bypass probability toward zero.
Before delving into specific permission rules, it is important to first understand Claude Code's six-layer security architecture from a macro perspective. This is one of the most important design patterns in the entire Harness:
flowchart TB
subgraph Layer1["็ฌฌ 1 ๅฑ: CLAUDE.md๏ผๆๅฏผๆง็บฆๆ๏ผ"]
direction LR
L1["ๅ่ฏ Agent 'ไธ่ฆไฟฎๆน migrations/ ็ฎๅฝ'"]
end
subgraph Layer2["็ฌฌ 2 ๅฑ: Permission Rules๏ผๅฃฐๆๆง็บฆๆ๏ผ"]
direction LR
L2["settings.json ไธญ็ allow/deny/ask ่งๅ"]
end
subgraph Layer3["็ฌฌ 3 ๅฑ: Hooks๏ผๅฏ็ผ็จ็บฆๆ๏ผ"]
direction LR
L3["PreToolUse ่ๆฌๆฃๆฅๆไฝๅๆณๆง"]
end
subgraph Layer4["็ฌฌ 4 ๅฑ: YOLO Classifier๏ผAI ็บฆๆ๏ผ"]
direction LR
L4["็ฌ็ซ AI ๆจกๅๅฎกๆฅๆไฝๅฎๅ
จๆง"]
end
subgraph Layer5["็ฌฌ 5 ๅฑ: Sandbox๏ผ็ณป็ป็บง็บฆๆ๏ผ"]
direction LR
L5["ๆไฝ็ณป็ป็บงๆไปถ/็ฝ็ป้็ฆป"]
end
subgraph Layer6["็ฌฌ 6 ๅฑ: Hardcoded Denials๏ผไธๅฏ่ฆ็็บฆๆ๏ผ"]
direction LR
L6["settings.json ๅง็ปไธๅฏๅ๏ผๆ ๆณ้่ฟ้
็ฝฎ็ฆ็จ"]
end
Layer1 --> Layer2 --> Layer3 --> Layer4 --> Layer5 --> Layer6
classDef soft fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
classDef medium fill:#fef9c3,stroke:#ca8a04,color:#713f12
classDef hard fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class Layer1 soft
class Layer2,Layer3 medium
class Layer4 medium
class Layer5,Layer6 hard
Analysis: Note the color gradient โ from blue (soft constraints, can be ignored) to yellow (medium constraints, can be overridden via configuration) to red (hard constraints, unbypassable). In engineering practice, Layer 1 (CLAUDE.md) has a compliance rate of approximately 95% โ the model occasionally "forgets." But Layer 6's compliance rate is 100%, since it is hardcoded. This gradient design means: you do not need every layer to be perfect; you only need the cumulative bypass probability to be sufficiently low. If each layer has a 5% bypass rate, six layers stacked yield a bypass probability of 0.05^6, which is approximately 0.000000002%.
5.4 Settings Hierarchy (7 Priority Levels)
Rules originate from multiple sources, ordered from highest to lowest priority:
Highest priority
โ
1. CLI arguments (cliArg) โ Command-line overrides
2. Session commands (command) โ /permissions command
3. Flag settings (flagSettings) โ CLAUDE_CODE_FLAG_SETTINGS
4. Policy settings (policySettings) โ Organization policy
5. Local settings (localSettings) โ .claude/settings.json.local
6. Project settings (projectSettings) โ .claude/settings.json
7. User settings (userSettings) โ ~/.claude/settings.json
โ
Lowest priority
With enterprise managed settings:
Managed Settings (MDM/Enterprise):
โโโ /managed/managed-settings.json โ Base managed settings
โโโ /managed/managed-settings.d/*.json โ Drop-in overrides
โโโ macOS plutil / Windows Registry โ OS-level MDM
Design Philosophy:
The hierarchical settings system allows organization-level policy enforcement without modifying user settings. Enterprise administrators can lock down certain permissions via MDM (Mobile Device Management), project maintainers can define sensible defaults in project settings, and individual users can fine-tune on top of these.
flowchart BT
U["็จๆท่ฎพ็ฝฎ\n~/.claude/settings.json\nไผๅ
็บงๆไฝ"] --> P["้กน็ฎ่ฎพ็ฝฎ\n.claude/settings.json"]
P --> L["ๆฌๅฐ่ฎพ็ฝฎ\n.claude/settings.json.local\nไธๆไบค git"]
L --> Po["็ญ็ฅ่ฎพ็ฝฎ\n็ป็ป็ญ็ฅ"]
Po --> M["็ฎก็่ฎพ็ฝฎ\nMDM/ไผไธ\nๅฏ้ๅฎ"]
M --> F["Flag ่ฎพ็ฝฎ\n็ฏๅขๅ้"]
F --> C["CLI ๅๆฐ\nไผๅ
็บงๆ้ซ"]
classDef low fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mid fill:#fef9c3,stroke:#ca8a04,color:#713f12
classDef high fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class U,P low
class L,Po mid
class M,F,C high
Analysis: Trust Hierarchy and Override Direction
Note that the arrows point from bottom to top โ lowest priority at the bottom, highest priority at the top. This is not accidental: the closer a setting is to "runtime," the higher its priority. User settings are the "most distant" (edited once, used long-term), while CLI arguments are the "most immediate" (can differ on each run). This design lets you temporarily override any setting via CLI arguments without modifying files.
Another key design:
lockedByPolicy: trueallows administrators to lock sandbox settings so that users cannot disable them. The source code comments note this was "Added to unblock NVIDIA enterprise rollout" โ a feature driven by a real enterprise customer requirement.
5.4 Permission Decision Pipeline (Real Source Code)
The following is the real permission pipeline from src/utils/permissions/permissions.ts, with comments revealing the rationale behind each decision:
// src/utils/permissions/permissions.ts โ ็ๅฎไปฃ็
async function hasPermissionsToUseToolInner(
tool: Tool, input: Record<string, unknown>, context: ToolUseContext,
): Promise<PermissionDecision> {
if (context.abortController.signal.aborted) throw new AbortError()
let appState = context.getAppState()
// ===== 1a. ๆดไธชๅทฅๅ
ท่ขซ Deny =====
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) {
return { behavior: 'deny', decisionReason: { type: 'rule', rule: denyRule },
message: `Permission to use ${tool.name} has been denied.` }
}
// ===== 1b. ๆดไธชๅทฅๅ
ท่ขซ Ask =====
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
if (askRule) {
// ็นๆฎๆ
ๅต๏ผๆฒ็่ชๅจๅ
่ฎธ
// ๅฝ autoAllowBashIfSandboxed ๅผๅฏๆถ๏ผๆฒ็ๅ็ๅฝไปค่ทณ่ฟ ask ่งๅ
// ไธไผๆฒ็ๅ็ๅฝไปค๏ผๆ้คๅฝไปคใdangerouslyDisableSandbox๏ผไป้ตๅฎ ask
const canSandboxAutoAllow =
tool.name === BASH_TOOL_NAME &&
SandboxManager.isSandboxingEnabled() &&
SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&
shouldUseSandbox(input)
if (!canSandboxAutoAllow) {
return { behavior: 'ask', decisionReason: { type: 'rule', rule: askRule } }
}
// ่ฝๅ
ฅไธๆน่ฎฉ Bash ็ checkPermissions ๅค็ๅฝไปค็บง่งๅ
}
// ===== 1c. ๅทฅๅ
ท็นๅฎๆ้ๆฃๆฅ =====
let toolPermissionResult: PermissionResult = { behavior: 'passthrough' }
try {
const parsedInput = tool.inputSchema.parse(input)
toolPermissionResult = await tool.checkPermissions(parsedInput, context)
} catch (e) {
if (e instanceof AbortError) throw e
logError(e)
}
// ===== 1d. ๅทฅๅ
ทๅฎ็ฐๆ็ป =====
if (toolPermissionResult?.behavior === 'deny') return toolPermissionResult
// ===== 1e. ้่ฆ็จๆทไบคไบ็ๅทฅๅ
ท =====
if (tool.requiresUserInteraction?.() && toolPermissionResult?.behavior === 'ask') {
return toolPermissionResult
}
// ===== 1f. ๅ
ๅฎน็บง ask ่งๅ๏ผ้่ฆ๏ผ๏ผ=====
// ๅฝ็จๆท้
็ฝฎไบๅ
ๅฎน็บง ask ่งๅๅฆ Bash(npm publish:*)๏ผ
// tool.checkPermissions ่ฟๅ {behavior:'ask', decisionReason:{type:'rule', ruleBehavior:'ask'}}
// ่ฟๅฟ
้กป่ขซๅฐ้๏ผๅณไฝฟๅจ bypassPermissions ๆจกๅผไธ๏ผ
if (toolPermissionResult?.behavior === 'ask' &&
toolPermissionResult.decisionReason?.type === 'rule' &&
toolPermissionResult.decisionReason.rule.ruleBehavior === 'ask') {
return toolPermissionResult
}
// ===== 1g. ๅฎๅ
จๆฃๆฅ๏ผไธๅฏ็ป่ฟ๏ผ=====
// .git/, .claude/, .vscode/, shell ้
็ฝฎ็ญ่ทฏๅพ
// ๅณไฝฟ bypassPermissions ๆจกๅผไนๅฟ
้กปๆ็คบ
if (toolPermissionResult?.behavior === 'ask' &&
toolPermissionResult.decisionReason?.type === 'safetyCheck') {
return toolPermissionResult
}
// ===== 2a. ๆจกๅผๆฃๆฅ =====
appState = context.getAppState() // ้ๆฐ่ทๅๆๆฐ็ถๆ
const shouldBypassPermissions =
appState.toolPermissionContext.mode === 'bypassPermissions' ||
(appState.toolPermissionContext.mode === 'plan' &&
appState.toolPermissionContext.isBypassPermissionsModeAvailable)
if (shouldBypassPermissions) {
return { behavior: 'allow', decisionReason: { type: 'mode', mode: '...' } }
}
// ===== 2b. ๆดไธชๅทฅๅ
ท่ขซ Allow =====
const allowRule = toolAlwaysAllowedRule(appState.toolPermissionContext, tool)
if (allowRule) {
return { behavior: 'allow', decisionReason: { type: 'rule', rule: allowRule } }
}
// ===== 3. passthrough โ ask =====
return toolPermissionResult.behavior === 'passthrough'
? { ...toolPermissionResult, behavior: 'ask' }
: toolPermissionResult
}
// ===== ๅคๅฑๅ
่ฃ
๏ผๆจกๅผ่ฝฌๆข =====
export const hasPermissionsToUseTool: CanUseToolFn = async (...) => {
const result = await hasPermissionsToUseToolInner(...)
// ๅ
่ฎธ โ ้็ฝฎ่ฟ็ปญๆ็ป่ฎกๆฐๅจ
if (result.behavior === 'allow') {
if (feature('TRANSCRIPT_CLASSIFIER') && context.mode === 'auto') {
persistDenialState(context, recordSuccess(currentDenialState))
}
return result
}
// ask โ ๆจกๅผ่ฝฌๆข
if (result.behavior === 'ask') {
if (appState.toolPermissionContext.mode === 'dontAsk') {
return { behavior: 'deny', decisionReason: { type: 'mode', mode: 'dontAsk' } }
}
// auto ๆจกๅผ โ AI ๅ็ฑปๅจ๏ผ่ง 5.5 ่๏ผ
}
return result
}
Permission Decision Order Diagram:
ๅทฅๅ
ท่ฐ็จ่ฏทๆฑ
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโ
โ โ โ
Deny ่งๅ? Ask ่งๅ? Allow ่งๅ?
โ ๆฏ โ ๆฏ โ ๆฏ
v โ v
ๆ็ป ๆฒ็ๅฏไปฅ ๅ
่ฎธ
่ชๅจๅ
่ฎธ?
โ ๅฆ
v
tool.checkPermissions()
โ
โโโโโโโโโโผโโโโโโโโโ
โ โ โ
deny ask allow/
โ โ passthrough
v โ โ
ๆ็ป ๅ
ๅฎน็บง่งๅ? โ
โ ๆฏ โ
v โ
ๆ็ป/่ฏข้ฎ โ
โ
ๅฎๅ
จๆฃๆฅ?โโโค
โ ๆฏ โ
v โ
่ฏข้ฎ โ
โ
bypassPermissions?
โ ๆฏ โ ๅฆ
v v
ๅ
่ฎธ Allow ่งๅ?
โ ๆฏ โ ๅฆ
v v
ๅ
่ฎธ ask โ ๆจกๅผ่ฝฌๆข
โโ dontAsk โ deny
โโ auto โ AI ๅ็ฑปๅจ
โโ default โ ็จๆทๆ็คบ
5.5 Permission Rule Parser (Real Source Code)
Parsing rule strings is more complex than it appears โ escaped parentheses must be handled:
// src/utils/permissions/permissionRuleParser.ts โ ็ๅฎไปฃ็
// ่พๅ
ฅ: "Bash(python -c \"print\\(1\\)\")"
// ่พๅบ: { toolName: "Bash", ruleContent: "python -c \"print(1)\"" }
export function permissionRuleValueFromString(ruleString: string): PermissionRuleValue {
// ๆพๅฐ็ฌฌไธไธช **ๆช่ฝฌไน** ็ๅทฆๆฌๅท
const openParenIndex = findFirstUnescapedChar(ruleString, '(')
if (openParenIndex === -1) {
return { toolName: normalizeLegacyToolName(ruleString) }
}
// ๆพๅฐๆๅไธไธช **ๆช่ฝฌไน** ็ๅณๆฌๅท
const closeParenIndex = findLastUnescapedChar(ruleString, ')')
if (closeParenIndex === -1 || closeParenIndex <= openParenIndex) {
return { toolName: normalizeLegacyToolName(ruleString) }
}
// ๅณๆฌๅทๅฟ
้กปๅจๆซๅฐพ
if (closeParenIndex !== ruleString.length - 1) {
return { toolName: normalizeLegacyToolName(ruleString) }
}
const toolName = ruleString.substring(0, openParenIndex)
const rawContent = ruleString.substring(openParenIndex + 1, closeParenIndex)
// ็ฉบๅ
ๅฎน "Bash()" ๆ้้
็ฌฆ "Bash(*)" โ ๅทฅๅ
ท็บง่งๅ
if (rawContent === '' || rawContent === '*') {
return { toolName: normalizeLegacyToolName(toolName) }
}
// ๅ่ฝฌไน: \\( โ (, \\) โ ), \\\\ โ \\
const ruleContent = unescapeRuleContent(rawContent)
return { toolName: normalizeLegacyToolName(toolName), ruleContent }
}
// ๅคๆญๅญ็ฌฆๆฏๅฆ่ขซ่ฝฌไน๏ผๅ้ขๆๅฅๆฐไธชๅๆๆ ๏ผ
function findFirstUnescapedChar(str: string, char: string): number {
for (let i = 0; i < str.length; i++) {
if (str[i] === char) {
let backslashCount = 0
let j = i - 1
while (j >= 0 && str[j] === '\\') { backslashCount++; j-- }
if (backslashCount % 2 === 0) return i // ๅถๆฐไธชๅๆๆ = ๆช่ฝฌไน
}
}
return -1
}
MCP Server-Level Rule Matching:
// ่งๅ "mcp__server1" ๅน้
ๅทฅๅ
ท "mcp__server1__tool1"
// ่งๅ "mcp__server1__*" ๅน้
server1 ็ๆๆๅทฅๅ
ท
function toolMatchesRule(tool, rule): boolean {
if (rule.ruleValue.ruleContent !== undefined) return false // ๅ
ๅฎน่งๅไธๅน้
ๆดไธชๅทฅๅ
ท
const nameForRuleMatch = getToolNameForPermissionCheck(tool)
if (rule.ruleValue.toolName === nameForRuleMatch) return true
// MCP ๆๅกๅจ็บงๅน้
const ruleInfo = mcpInfoFromString(rule.ruleValue.toolName)
const toolInfo = mcpInfoFromString(nameForRuleMatch)
return ruleInfo !== null && toolInfo !== null &&
(ruleInfo.toolName === undefined || ruleInfo.toolName === '*') &&
ruleInfo.serverName === toolInfo.serverName
}
Figure 5-2: The permission decision funnel โ 100% of tool calls pass through successive filter layers. Ultimately, only approximately 11% require an expensive classifier or user confirmation. The first 4 steps filter out 89% of calls, all via zero-cost rule matching.
Figure 5-3: Layer-by-layer bypass probability in the defense-in-depth model (blue bars: per-layer probability; red line: cumulative probability, logarithmic scale) โ After stacking 6 layers, the cumulative bypass probability approaches zero. Note that Layer 6 (hardcoded denials) has a per-layer probability of 0, reducing the cumulative probability to zero.
Figure 5-4: Permission decision matrix heatmap โ 5 permission modes x 6 tool types. Green=ALLOW, orange=ASK, red=DENY. Note that under auto mode, Bash(danger) remains ASK โ the classifier adopts a conservative strategy for high-risk operations.
5.6 Quantitative Analysis: Distribution of Permission Decisions
Based on analytics instrumentation points and comments in the Claude Code source code, we can infer the typical distribution of permission decisions:
| Decision Path | Estimated Share | Latency | Cost |
|---|---|---|---|
| Rule-based Allow (entire tool) | ~40% | <1ms | 0 |
| Mode-based Allow (bypassPermissions) | ~20% | <1ms | 0 |
| Safe-tool Allowlist (auto mode) | ~15% | <1ms | 0 |
| Tool checkPermissions Allow | ~10% | 1-5ms | 0 |
| YOLO Classifier Allow (fast stage) | ~8% | 50-200ms | ~$0.001 |
| YOLO Classifier Allow (thinking stage) | ~3% | 500ms-2s | ~$0.01 |
| User Prompt (interactive) | ~3% | 1-30s | 0 (waiting for user) |
| Deny (rule or classifier) | ~1% | varies | varies |
Source Code Annotation: Regarding classifier optimization, the source code comments: "Before running the auto mode classifier, check if acceptEdits mode would allow this action. This avoids expensive classifier API calls for safe operations like file edits." This fast-path check is estimated to skip approximately 35% of classifier calls. Another comment notes: "Allowlisted tools are safe and don't need YOLO classification." This skips an additional ~15%. Combined, only about 11% of tool calls actually require a classifier API call.
Performance Implications: The average latency for permission checking is approximately 5-10ms (weighted average), but variance is extremely high. Rule-based paths incur virtually no latency, while classifier paths may require 2 seconds. This is why Claude Code implements speculative prefetching in
toolExecution.tsโ it starts the Bash classifier check in parallel with PreToolUse Hook execution. The source code comments: "Speculatively start the bash allow classifier check early so it runs in parallel with pre-tool hooks."
5.7 YOLO Classifier (Auto Mode)
The auto mode uses a two-stage AI classifier to automatically approve tool calls:
Stage 1: Fast Classifier
โโ Uses a smaller/faster model
โโ Checks: Is this operation safe?
โโ Returns confidence: high/medium/low
โโ If high confidence + shouldBlock=false โ approve directly
โโ If uncertain โ proceed to Stage 2
Stage 2: Thinking Classifier
โโ Uses a larger, more deliberative model
โโ Analyzes full context (conversation history + tool input)
โโ Returns final judgment
โโ If still uncertain โ fall back to user prompt
Safe Tool Fast Path:
โโ Read, Glob, Grep and other read-only tools
โโ Skips API call (saving latency and cost)
โโ Returns allow directly
Consecutive Denial Tracking:
โโ If the classifier denies multiple times consecutively
โโ Falls back to user prompt
โโ Prevents the classifier from getting stuck when being overly conservative
Design Philosophy:
The YOLO classifier embodies a core tenet of Harness engineering โ do not trust the model to judge whether its own operations are safe. Even if the primary model considers an operation correct, an independent "gatekeeper" model performs a secondary review. The two-stage design strikes a balance between speed and safety.
5.6 Permission Decision Reason Tracking
Every permission decision includes a detailed reason, used for auditing and debugging:
type PermissionDecisionReason =
| { type: 'rule'; rule: PermissionRule }
| { type: 'mode'; mode: PermissionMode }
| { type: 'classifier'; classifier: string; reason: string }
| { type: 'hook'; hookName: string; reason?: string }
| { type: 'safetyCheck'; reason: string; classifierApprovable: boolean }
// ... ๆดๅคๅไฝ
Chapter 6: Hooks System โ Lifecycle Extensibility
The permission model tells the Agent "whether it can act," while Hooks let you inject your own logic "before" and "after" the Agent acts.
Think of Hooks as airport security checkpoints. Passengers (tool calls) pass through security screening (PreToolUse Hook), may be tagged after passing (PostToolUse Hook), and are stopped if their luggage is problematic (blocking error). Security personnel can be human (command Hook), AI-powered (agent Hook), or even remote (http Hook).
Claude Code defines 26 Hook events and 4 Hook types โ the most complete Agent lifecycle extension system visible in open source to date. Understanding it gives you the key to making a Harness "customizable."
Hooks are the Harness's extension points. They allow users to inject custom logic at critical moments in the Agent lifecycle.
6.1 The 26 Hook Events
// src/types/hooks.ts โ ๅฎๆด็ Hook ไบไปถๅ่กจ
type HookEvent =
// ๅทฅๅ
ท็ธๅ
ณ
| 'PreToolUse' // ๅทฅๅ
ทๆง่กๅ
| 'PostToolUse' // ๅทฅๅ
ทๆง่กๅ
| 'PostToolUseFailure'// ๅทฅๅ
ทๆง่กๅคฑ่ดฅๅ
// ๆ้
| 'PermissionRequest' // ๆ้่ฏทๆฑ
| 'PermissionDenied' // ๆ้่ขซๆ็ป
// ไผ่ฏ
| 'SessionStart' // ไผ่ฏๅผๅง
| 'SessionEnd' // ไผ่ฏ็ปๆ
| 'Stop' // ๆจกๅๅๆญข
| 'StopFailure' // ๅๆญขๅคฑ่ดฅ
// ็จๆท่พๅ
ฅ
| 'UserPromptSubmit' // ็จๆทๆไบคๆ็คบ
// Agent
| 'SubagentStart' // ๅญ Agent ๅฏๅจ
| 'SubagentStop' // ๅญ Agent ๅๆญข
| 'TeammateIdle' // ้ๅ็ฉบ้ฒ
// ไปปๅก
| 'TaskCreated' // ไปปๅกๅๅปบ
| 'TaskCompleted' // ไปปๅกๅฎๆ
// ๅ็ผฉ
| 'PreCompact' // ๅ็ผฉๅ
| 'PostCompact' // ๅ็ผฉๅ
// ๅ
ถไป
| 'Setup' // ๅๅง่ฎพ็ฝฎ
| 'Notification' // ้็ฅ
| 'Elicitation' // ไฟกๆฏ่ฏทๆฑ
| 'ElicitationResult' // ไฟกๆฏ่ฏทๆฑ็ปๆ
| 'ConfigChange' // ้
็ฝฎๅๆด
| 'CwdChanged' // ๅทฅไฝ็ฎๅฝๅๆด
| 'FileChanged' // ๆไปถๅๆด
| 'WorktreeCreate' // Worktree ๅๅปบ
| 'WorktreeRemove' // Worktree ็งป้ค
| 'InstructionsLoaded'; // ๆไปคๅ ่ฝฝๅฎๆ
Figure 6-1: Estimated trigger frequency of the 26 Hook events โ PreToolUse and PostToolUse are the most frequent events (triggered on every tool call), while SessionStart/End fire only at session boundaries. This explains why PostToolUse's internal Hooks have a dedicated fast-path optimization (-70% latency).
Figure 6-2: Cost-intelligence scatter plot of Hook types โ Command Hook in the lower left (cheap but simple), Agent Hook in the upper right (expensive but intelligent). The dashed line divides four quadrants: upper left is the "ideal zone" (intelligent and cheap), lower right is the "avoid zone" (unintelligent and expensive).
6.2 Hook Lifecycle and Type Selection
Choosing the correct Hook type is a critical decision in Harness customization. The following chart aids selection based on the scenario:
quadrantChart
title Hook ็ฑปๅ้ๆฉ็ฉ้ต
x-axis "ไฝๆๆฌ" --> "้ซๆๆฌ"
y-axis "ไฝๆบ่ฝ" --> "้ซๆบ่ฝ"
quadrant-1 "Agent Hook"
quadrant-2 "Prompt Hook"
quadrant-3 "Command Hook"
quadrant-4 "HTTP Hook"
"่งๅๆฃๆฅ": [0.15, 0.2]
"Lint ่ฟ่ก": [0.25, 0.15]
"ๅฎๅ
จๅฎกๆฅ": [0.6, 0.85]
"ๆต่ฏ้ช่ฏ": [0.75, 0.9]
"Slack ้็ฅ": [0.7, 0.1]
"ๅฎก่ฎกๆฅๅฟ": [0.65, 0.15]
"ไปฃ็ ่ดจ้่ฏๅ": [0.45, 0.7]
"ๅ่งๆฃๆฅ": [0.5, 0.6]
Interpretation: The lower-left corner is Command Hook territory โ simple, cheap, and deterministic (e.g., lint, grep checks). The upper-right corner is Agent Hook territory โ the smartest but most expensive (requiring a full Claude call to understand code semantics). HTTP Hooks occupy the lower right โ moderate cost (network latency) but low intelligence (merely POSTing data). Prompt Hooks sit in the upper middle โ a single LLM judgment, cheaper than an Agent but smarter than a script.
Four Hook Types
Type 1: Command Hook (Shell Command)
{
"type": "command",
"command": "npm test -- --bail",
"if": "Bash(npm *)",
"shell": "bash",
"timeout": 30,
"statusMessage": "Running tests...",
"once": false,
"async": false,
"asyncRewake": false
}
Execution flow:
1. Path conversion (Windows: C:\Users\foo -> /c/Users/foo)
2. Variable substitution (${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT})
3. Shell selection (Bash or PowerShell)
4. JSON input written to stdin
5. Line-by-line stdout parsing (detecting async signals and prompt requests)
6. Exit code determines result
Exit code semantics:
- 0: Success; stdout content optionally displayed
- 2: Blocking error; stderr shown to both the model and the user
- Other: Non-blocking error; stderr shown only to the user
Type 2: Prompt Hook (LLM Evaluation)
{
"type": "prompt",
"prompt": "Review this code change for security vulnerabilities: $ARGUMENTS",
"model": "claude-sonnet-4-6",
"timeout": 60
}
Uses an independent LLM call to evaluate Hook input. Suitable for checks requiring semantic understanding (e.g., code security review).
Type 3: HTTP Hook (External Service)
{
"type": "http",
"url": "https://hooks.slack.com/triggers/...",
"headers": {
"Authorization": "Bearer $SLACK_TOKEN"
},
"allowedEnvVars": ["SLACK_TOKEN"],
"timeout": 10
}
POSTs JSON to an external URL. The $VAR_NAME syntax in headers interpolates from whitelisted environment variables. allowedEnvVars restricts accessible environment variables, preventing accidental leakage.
Type 4: Agent Hook (Agent Validator)
{
"type": "agent",
"prompt": "Verify that the test suite passes and no regressions were introduced",
"model": "claude-sonnet-4-6",
"timeout": 120
}
Uses a full Claude Agent (with tool access) to validate operations. The most expensive but most powerful โ the Agent can read files, run tests, and check results.
6.3 Hook Configuration Structure
// ~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "eslint --stdin --stdin-filename=$TOOL_INPUT_FILE_PATH",
"if": "Write(*.ts)"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo '{\"decision\": \"block\", \"reason\": \"sudo is not allowed\"}' ",
"if": "Bash(sudo *)"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "https://audit.company.com/log",
"headers": { "Authorization": "Bearer $AUDIT_TOKEN" },
"allowedEnvVars": ["AUDIT_TOKEN"]
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "git stash",
"timeout": 10
}
]
}
]
}
}
6.4 Hook Input/Output Protocol
Input (JSON passed via stdin):
// ๆๆ Hook ็ๅบ็กๅญๆฎต
{
session_id: string,
transcript_path: string,
cwd: string,
permission_mode?: string,
agent_id?: string,
agent_type?: string,
}
// PreToolUse ็นๆๅญๆฎต
{
tool_name: "Write",
tool_input: { file_path: "/src/index.ts", content: "..." },
tool_use_id: "toolu_xxx",
}
// PostToolUse ็นๆๅญๆฎต
{
tool_name: "Bash",
tool_input: { command: "npm test" },
tool_response: { stdout: "...", exit_code: 0 },
tool_use_id: "toolu_xxx",
}
// UserPromptSubmit
{
prompt_text: "Fix the bug in login.ts",
}
// SessionStart
{
source: "startup" | "resume" | "clear" | "compact",
}
Output (JSON returned via stdout):
type HookJSONOutput = {
// ๅ
จๅฑๆงๅถ
continue?: boolean; // false = ๅๆญขๅฏน่ฏ
stopReason?: string;
decision?: 'approve' | 'block';
reason?: string;
systemMessage?: string; // ๆณจๅ
ฅ็ณป็ปๆถๆฏ
suppressOutput?: boolean;
// ๆ้็ธๅ
ณ
permissionDecision?: 'allow' | 'deny' | 'ask';
// ไบไปถ็นๅฎ
hookSpecificOutput?: {
updatedInput?: object; // ไฟฎๆนๅทฅๅ
ท่พๅ
ฅ๏ผPreToolUse๏ผ
additionalContext?: string; // ๆณจๅ
ฅ้ขๅคไธไธๆ
watchPaths?: string[]; // ๆณจๅๆไปถ็่งๅจ
updatedMCPToolOutput?: unknown; // ไฟฎๆน MCP ๅทฅๅ
ท่พๅบ
action?: 'accept' | 'decline' | 'cancel';
}
};
6.5 In-Depth Analysis of the Hook Execution Engine
The following is the real implementation of the Hook execution engine from src/utils/hooks.ts:
// src/utils/hooks.ts โ ็ๅฎไปฃ็ ๏ผ็ฎๅไฝไฟ็ๅ
ณ้ฎ้ป่พ๏ผ
async function* executeHooks({
hookInput, toolUseID, matchQuery, signal, timeoutMs,
toolUseContext, messages, forceSyncExecution, requestPrompt,
}) {
// ๅฎๅ
จๆฃๆฅ 1: ๅ
จๅฑ็ฆ็จ
if (shouldDisableAllHooksIncludingManaged()) return
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) return
// ๅฎๅ
จๆฃๆฅ 2: ๅทฅไฝ็ฉบ้ดไฟกไปป
// ๆๆ Hook ้ฝ้่ฆๅทฅไฝ็ฉบ้ดไฟกไปป๏ผ้ฒๆญข RCE ๆผๆด๏ผ
if (shouldSkipHookDueToTrust()) return
// ๆฅๆพๅน้
็ Hook
const matchingHooks = await getMatchingHooks(
appState, sessionId, hookEvent, hookInput, tools,
)
if (matchingHooks.length === 0) return
// ===== ๅฟซ้่ทฏๅพไผๅ =====
// ๅฆๆๆๆ Hook ้ฝๆฏๅ
้จๅ่ฐ๏ผๅฆ sessionFileAccessHooksใattributionHooks๏ผ
// ่ทณ่ฟ span/progress/abortSignal/JSONๅค็ โ ๆง่ฝๆๅ 70%
const userHooks = matchingHooks.filter(h => !isInternalHook(h))
if (userHooks.length === 0) {
// 6.01ยตs โ ~1.8ยตs per PostToolUse hit (-70%)
for (const { hook } of matchingHooks) {
if (hook.type === 'callback') {
await hook.callback(hookInput, toolUseID, signal, context)
}
}
return
}
// ===== ๅนถ่กๆง่กๆๆ Hook =====
// ๆฏไธช Hook ๆ็ฌ็ซ็่ถ
ๆถ
// ... ่ๅ็ปๆ ...
}
Shell Execution of Command Hooks
// src/utils/hooks.ts โ execCommandHook ็ๅ
ณ้ฎ็ป่
async function execCommandHook(hook, hookEvent, hookName, jsonInput, signal, ...) {
// 1. Shell ้ๆฉ
// PowerShell: pwsh -NoProfile -NonInteractive -Command
// Bash: spawn(command, [], { shell: gitBashPath | true })
// 2. ๅ้ๆฟๆข
// ${CLAUDE_PROJECT_DIR} โ ้กน็ฎ็ฎๅฝ
// ${CLAUDE_PLUGIN_ROOT} โ ๆไปถ็ฎๅฝ
// ${CLAUDE_PLUGIN_DATA} โ ๆไปถๆฐๆฎ็ฎๅฝ
// ${user_config.X} โ ๆไปถ้
็ฝฎๅผ
// 3. stdin ๅๅ
ฅ๏ผUTF-8 ็ผ็ ๏ผ
child.stdin.write(jsonInput + '\n', 'utf8')
// 4. stdout ้่ก่งฃๆ
child.stdout.on('data', data => {
stdout += data
// ===== Prompt Request ๅ่ฎฎ =====
// Hook ๅฏไปฅ่ฏทๆฑ็จๆท่พๅ
ฅ๏ผ
// ่พๅบ่กๆ ผๅผ: {"prompt": {"type": "text", "message": "Enter value:"}}
if (requestPrompt) {
for (const line of lines) {
const parsed = jsonParse(line.trim())
const validation = promptRequestSchema().safeParse(parsed)
if (validation.success) {
// ๅบๅๅๅผๆญฅ prompt ๅค็
promptChain = promptChain.then(async () => {
const response = await requestPrompt(validation.data)
child.stdin.write(jsonStringify(response) + '\n', 'utf8')
})
continue
}
}
}
// ===== ๅผๆญฅๆฃๆต =====
// ็ฌฌไธ่ก่พๅบๅฆๆๆฏ {"async": true, ...}
// โ ๅฐ่ฟ็จ่ฝฌๅ
ฅๅๅฐ๏ผไธป็บฟ็จ็ปง็ปญ
if (!initialResponseChecked) {
const firstLine = firstLineOf(stdout).trim()
const parsed = jsonParse(firstLine)
if (isAsyncHookJSONOutput(parsed) && !forceSyncExecution) {
executeInBackground({ processId, hookId, ... })
shellCommandTransferred = true
resolve({ stdout, stderr, output, status: 0 })
}
}
})
// 5. ็ญๅพ
ๅฎๆ
// ๅ
ณ้ฎ๏ผ็ญๅพ
stdout ๅ stderr ๆต็ปๆๅๅ่ฎคไธบ่พๅบๅฎๆ
// ้ฒๆญข 'close' ไบไปถๅจๆๆ 'data' ไบไปถๅค็ๅ่งฆๅ็็ซๆๆกไปถ
await Promise.all([stdoutEndPromise, stderrEndPromise])
// 6. ๅฅ็ฆปๅทฒๅค็็ prompt ่ฏทๆฑ่ก
// ไฝฟ็จๅ
ๅฎนๅน้
่้็ดขๅผ๏ผ้ฒๆญข็ดขๅผๆผ็งปๅฏผ่ด็ prompt JSON ๆณ้ฒ
const finalStdout = processedPromptLines.size === 0 ? stdout :
stdout.split('\n').filter(line => !processedPromptLines.has(line.trim())).join('\n')
return { stdout: finalStdout, stderr, output, status: exitCode }
}
Hook JSON Output Processing:
// Hook ่พๅบ่ขซ่งฃๆไธบ JSON๏ผๆๅๅณ็ญๅๅฏไฝ็จ
function processHookJSONOutput({ json, ... }) {
const result = {}
// ๅ
จๅฑๆงๅถ
if (json.continue === false) result.preventContinuation = true
// ๅณ็ญ
if (json.decision === 'approve') result.permissionBehavior = 'allow'
if (json.decision === 'block') {
result.permissionBehavior = 'deny'
result.blockingError = { blockingError: json.reason || 'Blocked by hook' }
}
// ไบไปถ็นๅฎๅญๆฎต
if (json.hookSpecificOutput?.hookEventName === 'PreToolUse') {
if (json.hookSpecificOutput.updatedInput) {
result.updatedInput = json.hookSpecificOutput.updatedInput // ไฟฎๆนๅทฅๅ
ท่พๅ
ฅ
}
result.additionalContext = json.hookSpecificOutput.additionalContext
}
if (json.hookSpecificOutput?.hookEventName === 'PostToolUse') {
if (json.hookSpecificOutput.updatedMCPToolOutput) {
result.updatedMCPToolOutput = json.hookSpecificOutput.updatedMCPToolOutput
}
}
return result
}
6.6 Async Hook Protocol
// Hook ๅฏไปฅ้่ฟ็ฌฌไธ่ก JSON ไฟกๅทๅผๆญฅๆง่ก๏ผ
// stdout ็ฌฌไธ่ก: {"async": true, "asyncTimeout": 60000}
// ๆญคๅ Hook ๅจๅๅฐ่ฟ่ก
// ๆ้่ฟ้
็ฝฎ๏ผ
{ type: 'command', command: '...', async: true, asyncRewake: true }
// asyncRewake: ๅฆๆ้ๅบ็ ไธบ 2๏ผๆๅ
ฅ้็ฅ้ๅๅค้ๆจกๅ
// ็จไพ๏ผๅๅฐ่ฟ่กๆต่ฏๅฅไปถ๏ผๅคฑ่ดฅๆถ้็ฅ Agent
6.6 Conditional Execution (the if Field)
The if field of Hooks uses the same syntax as permission rules:
// ๅชๅฏน git ๅฝไปค่ฟ่ก
{ "if": "Bash(git *)" }
// ๅชๅฏน TypeScript ๆไปถๅๅ
ฅ่ฟ่ก
{ "if": "Write(*.ts)" }
// ๅชๅฏน npm ๅฝไปค่ฟ่ก
{ "if": "Bash(prefix:npm)" }
// ๅน้
ๆๆ Bash ่ฐ็จ
{ "if": "Bash" }
Design Philosophy:
The Hook system follows the principle of "data-driven extensibility." Rather than requiring users to modify source code, all customization is accomplished through declarative configuration in settings.json. The four Hook types cover the full spectrum from simple shell scripts to full Agent validation, with unified and straightforward exit code semantics.
Practical Advice: If you are using Hooks for the first time, start with the simplest approach โ a PreToolUse command hook that prints the tool name and input. Once you are comfortable with the input/output protocol, try conditional execution (the
iffield) and HTTP callbacks. Agent hooks are the most powerful but also the most expensive (each invocation requires a complete Agent execution); reserve them for scenarios where you genuinely need to "understand code semantics."Common Pitfall: Avoid heavy operations in Stop hooks. If your Stop hook injects a large number of tokens (such as an entire test report), it may trigger a prompt-too-long error. The prompt-too-long recovery skips Stop hooks (to prevent a death spiral), causing your logic to be silently bypassed. Keep Stop hooks lightweight โ if you need to convey large amounts of information, write it to a file and let the Agent read it.
Chapter 7: Sandbox & Security โ The Safety Net
If the permission model is the fence and Hooks are the security checkpoints, then the sandbox is the physical isolation barrier. The first two operate at the logical level โ they depend on software executing correctly. But software can have bugs, and logic can be circumvented. The sandbox enforces isolation at the operating system level: even if the Agent's code has a vulnerability, it cannot access files or networks it should not touch.
This is the classic "Defense in Depth" principle of security engineering: never stake security on a single mechanism; instead, layer defenses. Claude Code has six defensive layers, with the sandbox being the second-to-last (the final layer is hardcoded denials).
The sandbox is the Harness's last line of defense. Even if the permission model and Hooks are both bypassed, the sandbox still limits what the Agent can do.
7.1 Sandbox Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code โ
โ โ
โ BashTool.call() โ
โ โ โ
โ v โ
โ shouldUseSandbox() โ
โ โ โ
โ โโ ๆฒ็ๆฏๅฆๅฏ็จ๏ผ โ
โ โโ dangerouslyDisableSandbox ๆฏๅฆๅ
่ฎธ๏ผ โ
โ โโ ๅฝไปคๆฏๅฆๅจๆ้คๅ่กจไธญ๏ผ โ
โ โ โ
โ v โ
โ SandboxManager.wrapWithSandbox(command) โ
โ โ โ
โ v โ
โ @anthropic-ai/sandbox-runtime โ
โ โ โ
โ โโ ๆไปถ็ณป็ป้ๅถ โ
โ โโ ็ฝ็ป้ๅถ โ
โ โโ ่ฟ็จ้ๅถ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Figure 7-1: Sandbox coverage โ Settings Files, Skills Dir, and Git Config are 100% restricted (hardcoded DENY) and cannot be opened via configuration. Filesystem Write restriction is at 85% (most paths are restricted; only the project directory is permitted). Process Spawn has the lowest restriction rate (50%), since certain commands (such as Docker) need to bypass the sandbox.
7.2 Three Restriction Dimensions
Filesystem Restrictions
interface FsReadRestrictionConfig {
allowRead: string[]; // ๅ
่ฎธ่ฏปๅ็่ทฏๅพๆจกๅผ
denyRead: string[]; // ็ฆๆญข่ฏปๅ็่ทฏๅพๆจกๅผ
}
interface FsWriteRestrictionConfig {
allowWrite: string[]; // ๅ
่ฎธๅๅ
ฅ็่ทฏๅพๆจกๅผ
denyWrite: string[]; // ็ฆๆญขๅๅ
ฅ็่ทฏๅพๆจกๅผ
}
// ่ทฏๅพ่ฏญๆณ:
// "/path" โ ็ธๅฏนไบ่ฎพ็ฝฎๆ น่ทฏๅพ
// "//path" โ ็ปๅฏน่ทฏๅพ๏ผไปๆ น็ฎๅฝๅผๅง๏ผ
// "~/path" โ ็จๆทไธป็ฎๅฝ
// "./path" โ ๅฝๅๅทฅไฝ็ฎๅฝ
// "*.ext" โ ้้
็ฌฆๆจกๅผ
Security Hardcoding:
- .claude/settings*.json is always deny write (preventing sandbox escape)
- .claude/skills, .claude/commands are always deny write (preventing malicious code injection)
- Detects bare repository files (HEAD, objects, refs) and clears them after command execution
Network Restrictions
interface NetworkRestrictionConfig {
allowedDomains: string[]; // ๅ
่ฎธ็ๅๅๆจกๅผ
deniedDomains: string[]; // ็ฆๆญข็ๅๅๆจกๅผ
allowUnixSockets?: boolean; // Unix socket ่ฎฟ้ฎ
allowLocalBinding?: boolean; // ๆฌๅฐ็ซฏๅฃ็ปๅฎ
}
Command Exclusions
// settings.sandbox.excludedCommands
// ๅน้
ๆจกๅผ:
// "cmd:" โ ๅ็ผๅน้
// "cmd" โ ็ฒพ็กฎๅน้
// "cmd*" โ ้้
็ฌฆ
// ๆณจๆ: ่ฟ **ไธๆฏ** ๅฎๅ
จ่พน็
// ไป
ไธบไพฟๅฉๅ่ฝ๏ผ่ทณ่ฟๆฒ็ไปฅ้ฟๅ
ๅ
ผๅฎนๆง้ฎ้ข๏ผ
7.3 Path Resolution (Claude Code-Specific Conventions)
Claude Code uses special path prefix conventions distinct from sandbox-runtime's standard paths:
// src/utils/sandbox/sandbox-adapter.ts โ ็ๅฎไปฃ็
export function resolvePathPatternForSandbox(
pattern: string, source: SettingSource,
): string {
// "//" ๅ็ผ โ ไปๆไปถ็ณป็ปๆ น็ฎๅฝๅผๅง็็ปๅฏน่ทฏๅพ
// "//.aws/**" โ "/.aws/**"
if (pattern.startsWith('//')) {
return pattern.slice(1)
}
// "/" ๅ็ผ โ ็ธๅฏนไบ่ฎพ็ฝฎๆไปถ็ฎๅฝ
// ๆ้่งๅไธญ "/foo/**" โ "${settings_root}/foo/**"
if (pattern.startsWith('/') && !pattern.startsWith('//')) {
const root = getSettingsRootPathForSource(source)
return resolve(root, pattern.slice(1))
}
// ๅ
ถไปๆจกๅผ๏ผ~/path, ./path, path๏ผ็ดๆฅไผ ้
// sandbox-runtime ็ normalizePathForSandbox ไผๅค็
return pattern
}
Sandbox Initialization Flow:
// ๆฒ็ๅๅงๅๆฏๅผๆญฅ็๏ผไฝๅฟ
้กปๅจ็ฌฌไธไธชๅฝไปคๆง่กๅๅฎๆ
async function initialize(sandboxAskCallback?) {
if (initializationPromise) return initializationPromise
if (!isSandboxingEnabled()) return
// ๅ
่ฃ
ๅ่ฐไปฅๅผบๅถๆง่ก allowManagedDomainsOnly ็ญ็ฅ
const wrappedCallback = sandboxAskCallback ? async (hostPattern) => {
if (shouldAllowManagedSandboxDomainsOnly()) {
logForDebugging(`Blocked: ${hostPattern.host} (allowManagedDomainsOnly)`)
return false
}
return sandboxAskCallback(hostPattern)
} : undefined
// ๅๅปบ Promise๏ผๅจไปปไฝ await ไนๅๅๆญฅๅๅปบ๏ผ้ฒๆญข็ซๆๆกไปถ๏ผ
initializationPromise = (async () => {
// ๆฃๆต worktree ไธปไปๅบ่ทฏๅพ๏ผไผ่ฏๆ้ดไธๅ๏ผ็ผๅญ๏ผ
if (worktreeMainRepoPath === undefined) {
worktreeMainRepoPath = await detectWorktreeMainRepoPath(getCwdState())
}
const settings = getSettings_DEPRECATED()
const runtimeConfig = convertToSandboxRuntimeConfig(settings)
await BaseSandboxManager.initialize(runtimeConfig, wrappedCallback)
// ่ฎข้
่ฎพ็ฝฎๅๆด๏ผๅจๆๆดๆฐๆฒ็้
็ฝฎ
settingsSubscriptionCleanup = settingsChangeDetector.subscribe(() => {
const newConfig = convertToSandboxRuntimeConfig(getSettings_DEPRECATED())
BaseSandboxManager.updateConfig(newConfig)
})
})()
return initializationPromise
}
7.4 Converting Permission Rules to Sandbox Configuration
Claude Code converts its permission rule syntax into the sandbox-runtime configuration format:
// ๆ้่งๅ โ ๆฒ็่ทฏๅพ่ฝฌๆข
// Edit(/path/to/dir/*) โ sandbox.filesystem.allowWrite
// Read(/patterns) โ sandbox.filesystem.allowRead
// WebFetch(domain:example.com) โ sandbox.network.allowedDomains
function convertToSandboxConfig(
permissionRules: PermissionRule[],
sandboxSettings: SandboxSettings,
): SandboxRuntimeConfig {
// ๅๅนถๆ้่งๅๅๆฒ็่ฎพ็ฝฎ
// ๆ้่งๅไธญ็ allow ่งๅ โ sandbox ็ allow ๅ่กจ
// ๆฒ็่ฎพ็ฝฎ็ดๆฅๆ ๅฐ
// deny ่งๅๅง็ปๅ
ๅซๅฎๅ
จ็กฌ็ผ็
}
7.4 Sandbox Enablement Check
function isSandboxingEnabled(): boolean {
return isSupportedPlatform() // macOS, Linux, WSL2+
&& checkDependencies().errors.length === 0 // bubblewrap, socat
&& isPlatformInEnabledList() // sandbox.enabledPlatforms
&& getSandboxEnabledSetting(); // ็จๆท่ฎพ็ฝฎ
}
// ไพ่ตๆฃๆฅ๏ผๅธฆ็ผๅญ๏ผ
function checkDependencies(): { errors: string[], warnings: string[] } {
// ๆฃๆฅ: bubblewrap ๅฏๆง่กๆไปถ
// ๆฃๆฅ: socat ๅฏๆง่กๆไปถ
// ๆฃๆฅ: cap_setfcap capability
}
7.5 dangerouslyDisableSandbox
// BashTool ็่พๅ
ฅๅๆฐ
{
command: "docker build .",
dangerouslyDisableSandbox: true // ่ทณ่ฟๆฒ็
}
// ไป
ๅฝ่ฎพ็ฝฎๅ
่ฎธๆถ็ๆ:
// settings.sandbox.allowUnsandboxedCommands = true
// ่ฎพ่ฎกๆๅพ: ๆไบๅฝไปค๏ผๅฆ Docker๏ผไธๅ
ผๅฎนๆฒ็
// ไฝๅฟ
้กปๆพๅผ่ฏทๆฑๅนถ่ฎฐๅฝๅจๆก
Design Philosophy:
The sandbox design follows the Defense in Depth principle. Even if the permission model allows an operation, the sandbox still limits that operation's "blast radius." Critical security files (settings, skills) are hardcoded as non-writable โ a security guarantee that does not depend on configuration.
Why are settings files hardcoded as non-writable? Imagine this scenario: the Agent discovers that the sandbox is restricting its operations, so it "cleverly" modifies
settings.jsonto disable the sandbox, then proceeds. This constitutes a sandbox escape attack. Claude Code prevents this by hardcoding deny write for.claude/settings*.jsonโ even if all other security layers are bypassed, this rule remains in effect. This embodies the security engineering principle that "untrusted code must not be able to modify trust boundaries."
Chapter 8: Context Engineering โ The Art of Information Management
The preceding five chapters addressed "what the Agent does" and "what the Agent cannot do." This chapter shifts perspective โ "what the Agent knows."
Imagine you are a new employee on your first day. If nobody tells you the company's coding standards, architectural decisions, and known bugs, the code you write will most likely fail to meet expectations. Agents face the same challenge โ their performance is directly determined by the information they can "see."
Context Engineering is the discipline of managing the Agent's "field of vision." It answers three questions: 1. What should the Agent know? (CLAUDE.md, memory system) 2. When should it be told? (On-demand loading, prefetching) 3. What happens when there is too much information? (Four-level compaction pipeline)
Claude Code's design in this area is particularly sophisticated โ a 200-line memory index, a four-level compaction pipeline, and a parallel prefetch mechanism together constitute the most refined Agent context management system in the industry.
Context Engineering is the first pillar of Harness Engineering. It manages what information enters the model's context window, when, and in what form.
Figure 8-1: The context engineering pipeline โ Information flows from multiple sources (CLAUDE.md, memory files, MCP instructions, environmental context) into the model's context window, passing through parallel prefetching, relevance filtering, and token budget allocation. The right side shows the composition of the context window and the four compaction zones.
8.1 Quantitative Analysis: Context Window Budget Allocation
The typical budget allocation for Claude Code's context window (assuming 200K tokens):
| Component | Estimated Token Share | Size | Compacted? |
|---|---|---|---|
| System prompt (base) | ~5-8% | 10-16K | No (cache prefix) |
| Tool definitions (15 core + N MCP) | ~8-15% | 16-30K | No (cache prefix) |
| CLAUDE.md content | ~2-5% | 4-10K | No |
| MCP server instructions | ~1-3% | 2-6K | No |
| Memory attachments | ~1-2% | 2-4K | No (attached on demand) |
| Conversation history | ~60-80% | 120-160K | Yes (four-level pipeline) |
| Reserved space (model output) | ~5-10% | 10-20K | N/A |
Key Insight: Conversation history occupies 60-80% of the context space, which is precisely why the compaction pipeline is so important. System prompt and tool definitions occupy 13-23% โ this is also why tool lazy loading (ToolSearch) is valuable: loading 15 out of 43 tools saves approximately 8% of context space, equivalent to an additional 16K tokens of conversation history in long conversations.
Cache Economics: The system prompt and tool definitions serve as the cache prefix (~30-46K tokens). Anthropic API's prompt cache does not charge input fees for the matched prefix portion. At $3/M input tokens, this saves approximately $0.0001-0.00014 per API call. A typical session involves 20-50 API calls, yielding total savings of approximately $0.002-0.007/session. At scale (millions of daily active users), this becomes a significant cost optimization.
Figure 8-2: Space allocation in the 200K context window โ conversation history occupies 70% (140K tokens) and is the primary target of the compaction pipeline. System prompt + tool definitions total approximately 18.5%, which is the primary beneficiary area of the prompt cache.
Figure 8-3: Token growth curve of the four-level compaction pipeline (over 50 turns) โ The red dashed line shows that without compaction, the 200K limit is breached at turn 45. The purple line (full pipeline) shows that tokens drop to 45K after Autocompact triggers at turn 15, then grow slowly. This enables Claude Code to handle 100+ turn conversations without interruption.
8.2 CLAUDE.md โ Project-Level Persistent Context
CLAUDE.md is Claude Code's core context mechanism. It is a Markdown file that provides project-level persistent context:
# CLAUDE.md
## ้กน็ฎๆฆ่ฟฐ
่ฟๆฏไธไธช Next.js 14 ๅบ็จ๏ผไฝฟ็จ TypeScript + Tailwind CSSใ
## ๆถๆ็บฆๆ
- ็ปไปถๆพๅจ src/components/
- API ่ทฏ็ฑๆพๅจ src/app/api/
- ไธ่ฆไฝฟ็จ class ็ปไปถ
- ๆๆ API ่ฐ็จๅฟ
้กปไฝฟ็จ fetch๏ผไธ่ฆ็จ axios
## ๅฝๅ่ง่
- ็ปไปถ๏ผPascalCase
- ๅทฅๅ
ทๅฝๆฐ๏ผcamelCase
- ๅธธ้๏ผUPPER_SNAKE_CASE
## ๆต่ฏ
- ่ฟ่กๆต่ฏ: npm test
- ๆต่ฏๆกๆถ: Jest + React Testing Library
- ่ฆ็็่ฆๆฑ: >80%
## ๅทฒ็ฅ้ฎ้ข
- #123: ็ปๅฝ้กต้ขๅจ Safari ไธๆๅธๅฑ้ฎ้ข
- ไธ่ฆไฟฎๆน legacy/ ็ฎๅฝไธ็ๆไปถ
Loading hierarchy:
~/.claude/CLAUDE.md # Global (all projects)
.claude/CLAUDE.md # Project-level
.claude/CLAUDE.md.local # Local override (not committed to git)
subdirectory/CLAUDE.md # Directory-level (loaded when entering)
8.2 System Prompt Construction Pipeline
// src/QueryEngine.ts โ ็ณป็ปๆ็คบๆๅปบ
function buildSystemPrompt(): SystemPrompt {
const parts = [];
// 1. ๅบ็ก็ณป็ปๆ็คบ๏ผๅทฅๅ
ทๆ่ฟฐใ่กไธบๆๅ๏ผ
parts.push(getBaseSystemPrompt());
// 2. ๅทฅๅ
ทๅฎไน
for (const tool of tools) {
parts.push(tool.prompt(context));
}
// 3. CLAUDE.md ๅ
ๅฎน
parts.push(loadClaudeMd());
// 4. MCP ๆๅกๅจๆไปค
for (const mcp of mcpClients) {
parts.push(mcp.instructions);
}
// 5. ่ชๅฎไน็ณป็ปๆ็คบ๏ผ็จๆท่ฆ็๏ผ
if (customPrompt) parts.push(customPrompt);
// 6. ่ฎฐๅฟๆบๅถๆ็คบ๏ผๅฆๆ่ฎฐๅฟ็ณป็ป๏ผ
if (memoryEnabled) parts.push(memoryMechanicsPrompt);
return asSystemPrompt(parts.join('\n'));
}
8.3 Memory System
Claude Code implements a file-based persistent memory system, located in src/memdir/.
Four Memory Types
type MemoryType =
| 'user' // ็จๆท่ง่ฒใๅๅฅฝใ็ฅ่ฏๆฐดๅนณ
| 'feedback' // ๅทฅไฝๆนๆณๆๅฏผ๏ผไปไนๅฏๅ/้ฟๅ
๏ผ
| 'project' // ๆญฃๅจ่ฟ่ก็ๅทฅไฝใ็ฎๆ ใๅ่ฎกๆถ
| 'reference'; // ๅค้จ็ณป็ปๆ้
Memory File Format
---
name: user-prefers-terse-responses
description: ็จๆทไธๅๆฌขๅ้ฟ็ๆป็ป๏ผๅธๆ็ฎๆด็ดๆฅ็ๅๅค
type: feedback
---
ไธ่ฆๅจๆฏๆฌกๅๅคๆซๅฐพๆป็ปๅๅ็ไบๆ
โโ็จๆทๅฏไปฅ่ชๅทฑ่ฏป diffใ
**Why:** ็จๆทๆ็กฎ่กจ็คบไธๅๆฌขๅฐพ้จๆป็ปใ
**How to apply:** ๆๆๅๅคไฟๆ็ฎๆด๏ผไธๅ ๅฐพ้จๆป็ปๆฎต่ฝใ
Memory Index (MEMORY.md)
- [User Role](user_role.md) โ ้ซ็บง Go ๅทฅ็จๅธ๏ผReact ๆฐๆ
- [Terse Responses](feedback_terse.md) โ ไธ่ฆๅฐพ้จๆป็ป
- [Auth Rewrite](project_auth.md) โ ๅ่ง้ฉฑๅจ็่ฎค่ฏไธญ้ดไปถ้ๅ
- [Bug Tracker](reference_linear.md) โ ็ฎก้ bug ๅจ Linear INGEST ้กน็ฎ
Memory Scanning and Attachment
// src/memdir/memoryScan.ts
function scanMemories(memoryDir: string): MemoryHeader[] {
// ๆซๆ ~/.claude/memory/ ็ฎๅฝ
// ่ฏปๅๆฏไธช .md ๆไปถ็ frontmatter
// ๆไฟฎๆนๆถ้ดๆๅบ๏ผๆๆฐไผๅ
๏ผ
// ไธ้: MAX_MEMORY_FILES = 200
return headers;
}
// ไธๆฅ่ฏขๅพช็ฏ้ๆ
// 1. ๅจๆตๅผๅๅบๆ้ดๅผๅง่ฎฐๅฟๆซๆ๏ผ้ขๅ๏ผ
startRelevantMemoryPrefetch();
// 2. ่ฟๆปค็ธๅ
ณ่ฎฐๅฟๅนถๅๅปบ้ไปถๆถๆฏ
const attachments = getAttachmentMessages(memories, userMessage);
// 3. ้ๅ ๅฐ็จๆทๆถๆฏ
messages.push(...attachments);
Real Implementation of Memory Scanning
// src/memdir/memoryScan.ts โ ็ๅฎไปฃ็
// ๅๆฌก้ๅ๏ผstat + read ๅๅนถ๏ผๅๅฐ็ณป็ป่ฐ็จ๏ผ
// ๅฏนไบๅธธ่งๆ
ๅต๏ผN โค 200๏ผ๏ผ็ธๆฏๅ
stat ๆๅบๅ read๏ผ็ณป็ป่ฐ็จๅๅ
export async function scanMemoryFiles(
memoryDir: string, signal: AbortSignal,
): Promise<MemoryHeader[]> {
try {
const entries = await readdir(memoryDir, { recursive: true })
const mdFiles = entries.filter(
f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
)
const headerResults = await Promise.allSettled(
mdFiles.map(async (relativePath): Promise<MemoryHeader> => {
const filePath = join(memoryDir, relativePath)
// ๅช่ฏปๅๅ FRONTMATTER_MAX_LINES ่ก๏ผไผๅๅคงๆไปถ๏ผ
const { content, mtimeMs } = await readFileInRange(
filePath, 0, FRONTMATTER_MAX_LINES, undefined, signal,
)
const { frontmatter } = parseFrontmatter(content, filePath)
return {
filename: relativePath,
filePath,
mtimeMs,
description: frontmatter.description || null,
type: parseMemoryType(frontmatter.type),
}
}),
)
return headerResults
.filter((r): r is PromiseFulfilledResult<MemoryHeader> =>
r.status === 'fulfilled')
.map(r => r.value)
.sort((a, b) => b.mtimeMs - a.mtimeMs) // ๆๆฐไผๅ
.slice(0, MAX_MEMORY_FILES) // ไธ้ 200
} catch {
return [] // ็ฎๅฝไธๅญๅจๆถไผ้
้็บง
}
}
Memory Manifest Formatting:
// ็จไบ่ฎฐๅฟ้ๆฉๆ็คบๅๆๅ Agent ๆ็คบ
export function formatMemoryManifest(memories: MemoryHeader[]): string {
return memories.map(m => {
const tag = m.type ? `[${m.type}] ` : ''
const ts = new Date(m.mtimeMs).toISOString()
return m.description
? `- ${tag}${m.filename} (${ts}): ${m.description}`
: `- ${tag}${m.filename} (${ts})`
}).join('\n')
}
Design Philosophy:
The memory system follows the explicit over implicit principle. Memories are structured Markdown files (not a database) with explicit types and metadata. The MEMORY.md index is capped at 200 entries to prevent memory bloat. Memory scanning runs in parallel with API calls (prefetching), adding no latency.
Promise.allSettled (rather than Promise.all) ensures that a single corrupted memory file does not cause the entire scan to fail โ this is defensive programming as applied within the Harness.
8.4 Context Compaction Strategies
(See Section 3.3 for the four-level compaction pipeline)
Key additions:
Full Transcript Save Strategy:
โโ Saves the complete transcript to disk before every autocompact
โโ Path: ~/.claude/history/<session_id>/
โโ Purpose: --resume recovery, auditing, debugging
โโ Does not participate in compaction decisions (backup only)
Budget Tracking Across Compaction:
โโ taskBudgetRemaining is captured before compaction
โโ Accumulated across multiple compaction events
โโ Ensures total spend does not exceed budget
8.5 Dynamic Context
// src/context.ts โ ็ฏๅขไธไธๆๆถ้
function collectContext(): UserContext {
return {
currentDate: new Date(),
platform: process.platform,
shell: process.env.SHELL,
osVersion: getOSVersion(),
modelInfo: getModelInfo(),
cwd: process.cwd(),
gitState: getGitState(), // ๅๆฏใ็ถๆใ่ฟ็จ
terminalSize: getTerminalSize(),
// ...ๆดๅค็ฏๅขไฟกๆฏ
};
}
Chapter 9: Settings & Configuration โ Harness Tunability
So far we have seen the loop, tools, permissions, Hooks, sandbox, and context โ all of these components have tunable parameters. But where are those parameters stored? Whose settings take priority? Can enterprise administrators lock down certain settings?
The settings system is the Harness's "control panel." A well-designed control panel must let newcomers work out of the box, allow advanced users to fine-tune precisely, and enable enterprise administrators to enforce policy. Claude Code solves this perfectly with a 7-level hierarchical settings system.
The settings system determines how the Harness's behavior is adjusted and customized.
9.1 settings.json Structure
{
// ===== ๆ้ =====
"permissions": {
"allow": ["Read", "Glob", "Grep", "Bash(git *)"],
"deny": ["Bash(sudo *)", "Bash(rm -rf *)"],
"ask": ["Write(*.env)", "Bash(npm publish)"],
"defaultMode": "default",
"additionalDirectories": ["/shared/libs"],
"disableBypassPermissionsMode": "disable",
"disableAutoMode": "disable"
},
// ===== Hooks =====
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "check-safety.sh",
"if": "Bash(rm *)"
}
]
}
]
},
// ===== ๆฒ็ =====
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"fsRead": { "allow": ["**"], "deny": ["/etc/shadow"] },
"fsWrite": { "allow": ["./**"], "deny": [".env", "*.key"] },
"network": {
"allowedDomains": ["*.github.com", "registry.npmjs.org"],
"deniedDomains": ["*.malware.com"]
},
"excludedCommands": ["docker", "podman"],
"lockedByPolicy": false
},
// ===== ๅ
ถไป =====
"model": "claude-sonnet-4-6",
"env": {
"NODE_ENV": "development"
},
"attribution": "Co-Authored-By: Claude",
"cleanupPeriodDays": 30,
"defaultShell": "bash",
"allowedMcpServers": ["@anthropic/mcp-*"],
"deniedMcpServers": ["*-untrusted"]
}
9.2 Hierarchical Loading
// src/utils/settings/settings.ts
function loadSettings(): MergedSettings {
// ๆไผๅ
็บงไปไฝๅฐ้ซๅๅนถ
const layers = [
loadUserSettings(), // ~/.claude/settings.json
loadProjectSettings(), // .claude/settings.json
loadLocalSettings(), // .claude/settings.json.local
loadPolicySettings(), // ็ป็ป็ญ็ฅ
loadManagedSettings(), // MDM/ไผไธ
loadFlagSettings(), // ็ฏๅขๅ้
loadCliArgSettings(), // ๅฝไปค่กๅๆฐ
];
return deepMerge(layers); // ๅ่
่ฆ็ๅ่
}
Enterprise Managed Settings:
/managed/managed-settings.json
โโ Base managed settings
โโ Distributed by MDM (Jamf, Intune, etc.)
โโ Can lock sandbox: { "sandbox": { "lockedByPolicy": true } }
/managed/managed-settings.d/
โโ security-policy.json # Security policy drop-in
โโ compliance-rules.json # Compliance rules drop-in
โโ (loaded in alphabetical order; later files override earlier ones)
9.3 Schema Validation
// src/schemas/ โ Zod ้ช่ฏ
const SettingsSchema = z.object({
permissions: PermissionsSchema.optional(),
hooks: HooksSchema.optional(),
sandbox: SandboxSchema.optional(),
model: z.string().optional(),
env: z.record(z.string()).optional(),
// ...
}).passthrough(); // ไฟ็ๆช็ฅๅญๆฎต๏ผๅๅๅ
ผๅฎน๏ผ
.passthrough() is a critical design decision: older settings files may contain fields unrecognized by newer versions. passthrough() preserves these fields without raising errors, ensuring backward compatibility.
9.4 In-Depth Analysis: Settings Merge Algorithm
Settings merging appears simple but involves carefully crafted custom logic:
// src/utils/settings/settings.ts โ ็ๅฎไปฃ็
// ๆฐ็ปๅๅนถ็ญ็ฅ๏ผ่ฟๆฅ + ๅป้๏ผ่้ๆฟๆข๏ผ
function settingsMergeCustomizer(objValue, srcValue) {
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
return mergeArrays(objValue, srcValue) // concat + uniq
}
// ้ๆฐ็ป๏ผๆ ้่ฆ็๏ผๅฏน่ฑก้ๅฝๅๅนถ
}
Why concatenate arrays rather than replace? Consider permission rules: an enterprise policy defines
deny: ["Bash(sudo *)"], and the project settings definedeny: ["Bash(rm -rf *)"]. With replacement, the project's deny would overwrite the enterprise deny. Through concatenation and deduplication, the final deny list contains both rules โ which is the correct security behavior.
9.5 Managed Settings Drop-in Pattern
// ็ฑปไผผ systemd ็ drop-in ็ฎๅฝๆจกๅผ๏ผ
// managed-settings.json โ ๅบ็ก๏ผๆไฝไผๅ
็บง๏ผ
// managed-settings.d/
// 10-otel.json โ ๅฏ่งๆตๆงๅข้็้
็ฝฎ
// 20-security.json โ ๅฎๅ
จๅข้็้
็ฝฎ
// 30-compliance.json โ ๅ่งๅข้็้
็ฝฎ
// ๆๅญๆฏๅบๆๅบๅๅนถ๏ผๅ่
่ฆ็ๅ่
Why drop-in rather than a single file? In large enterprises, different teams manage different configuration dimensions. The security team is responsible for deny rules, the platform team for MCP whitelists, and the compliance team for data retention policies. The drop-in pattern lets each team independently manage their own configuration fragments without coordinating edits on a single file โ consistent with Linux system administration best practices.
9.6 Defensive Cache Cloning
// ็ๅฎไปฃ็ ๏ผ็ผๅญ่ฏปๅๆถ่ฟๅๅ
้ๅฏๆฌ
// ๅๅ ๏ผlodash ็ mergeWith() ไผไฟฎๆน็ฌฌไธไธชๅๆฐ
// ๅฆๆ็ดๆฅ่ฟๅ็ผๅญๅฏน่ฑก๏ผ่ฐ็จ่
็ๅๅนถๆไฝไผๆฑกๆ็ผๅญ
// ไธไธไธช่ฏปๅ่
ไผ็ๅฐ่ขซไฟฎๆน็ๆฐๆฎโโไธไธชๆ้พ่ฐ่ฏ็ bug
const cached = getCachedParsedFile(path)
return cached ? structuredClone(cached) : loadAndCache(path)
Design Pattern Analysis: Postel's Law (Be Liberal in What You Accept)
This exemplifies the classic internet engineering principle โ "be conservative in what you send, be liberal in what you accept." Settings files are edited by users and may contain typos, outdated fields, or experimental configuration.
passthrough()ensures Claude Code does not crash when encountering such "imperfect" input, silently ignoring unrecognized fields. This is particularly important in Harness Engineering because settings files persist across versions โ old settings should not be rejected by new versions when a user upgrades Claude Code.
Chapter 10: MCP Integration โ Extending the Harness Boundary
Claude Code ships with 43+ built-in tools, but real-world needs are infinite โ someone needs to query a database, someone else needs to operate Kubernetes, and another needs to send Slack messages. Anthropic cannot anticipate every need.
MCP (Model Context Protocol) is the solution: a standard protocol that allows anyone to write a "tool server," which Claude Code automatically discovers and uses. This is analogous to the USB protocol โ you do not need to redesign the computer for each peripheral; you need only a unified interface.
In this chapter we examine how Claude Code connects to MCP servers via 6 transport protocols and how it seamlessly integrates external tools into its permission and Hook systems.
MCP (Model Context Protocol) allows Claude Code to connect to external tool servers, vastly extending the Harness's capabilities.
10.1 Six Transport Protocols
flowchart LR
CC[Claude Code] --> stdio["Stdio\nๅญ่ฟ็จ stdin/stdout"]
CC --> sse["SSE\nServer-Sent Events"]
CC --> http["Streamable HTTP\nHTTP ๆต"]
CC --> ws["WebSocket\nTLS/ไปฃ็ๆฏๆ"]
CC --> inproc["InProcess\nๅ
ๅญ TS ๆจกๅ"]
CC --> sdk["SdkControl\nSDK daemon"]
stdio --> local["ๆฌๅฐ MCP ๆๅกๅจ\n(filesystem, git)"]
sse --> remote["่ฟ็จ MCP ๆๅกๅจ\n(Slack, Linear)"]
http --> remote
ws --> remote
inproc --> chrome["Chrome/Computer Use\n้ฟๅ
325MB ๅญ่ฟ็จ"]
sdk --> daemon["SDK Daemon\nๆงๅถ้ข"]
classDef transport fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
classDef server fill:#dcfce7,stroke:#16a34a,color:#14532d
class stdio,sse,http,ws,inproc,sdk transport
class local,remote,chrome,daemon server
Source Code Annotation Analysis: A key comment in the MCP client implementation reads: "Run Chrome MCP server in-process to avoid spawning ~325MB subprocess." This reveals the true reason InProcess transport exists: certain MCP servers (such as Chrome browser control) incur excessive memory overhead when started as independent processes. Through InProcess transport, they run within the Claude Code process, sharing memory space.
// src/services/mcp/client.ts โ ๅ
ญ็งไผ ่พ็ฑปๅ
type MCPTransport =
| StdioClientTransport // ๅญ่ฟ็จ๏ผstdin/stdout๏ผ
| SSEClientTransport // Server-Sent Events
| StreamableHTTPTransport // HTTP ๆต
| WebSocketTransport // WebSocket๏ผๆฏๆ TLS/ไปฃ็๏ผ
| InProcessTransport // ๅ
ๅญ๏ผTypeScript ๆจกๅ๏ผ
| SdkControlTransport; // SDK daemon ๆงๅถ
10.2 MCP Configuration
// ~/.claude/mcp.json๏ผๅ
จๅฑ๏ผ
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
"env": {}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "ghp_..." }
}
}
}
// .claude/mcp.json๏ผ้กน็ฎ็บง๏ผไธ mcp.json ๅๅนถ๏ผ
{
"mcpServers": {
"database": {
"command": "python",
"args": ["mcp_db_server.py"],
"env": { "DB_URL": "postgresql://..." }
}
}
}
10.3 MCP Tool Execution
// MCP ๅทฅๅ
ทๆง่กๆต็จ
async function callMcpTool(
server: MCPClient,
toolName: string,
input: object,
): Promise<ToolResult> {
// 1. ่ฐ็จ MCP ๆๅกๅจ
const result = await server.callTool(toolName, input);
// 2. ๅค็ๆตๅผ่ฟๅบฆ
if (result.isStreaming) {
for await (const progress of result.stream) {
yield progress;
}
}
// 3. ็ปๆๆชๆญๅ้ช่ฏ
const truncated = truncateIfNeeded(result);
// 4. OAuth token ๅทๆฐ๏ผๅฆ้่ฆ๏ผ
if (result.error?.type === 'auth_error') {
await refreshOAuthToken(server);
return callMcpTool(server, toolName, input); // ้่ฏ
}
// 5. ไบ่ฟๅถๅ
ๅฎนๆไน
ๅ๏ผๅคง่พๅบ๏ผ
if (isBinaryContent(result)) {
await persistBinaryContent(result);
}
return truncated;
}
10.4 In-Depth Analysis: MCP Connection Lifecycle
// src/services/mcp/client.ts โ ็ๅฎ็่ฟๆฅ็ผๆ
// getMcpToolsCommandsAndResources() ็ๆ ธๅฟ้ป่พ๏ผ
// 1. ๆไผ ่พ็ฑปๅๅๅบ๏ผไธๅๅนถๅๅบฆ
// ๆฌๅฐๆๅกๅจ๏ผstdio๏ผ: ไฝๅนถๅ๏ผbatch ~3๏ผ๏ผ้ฟๅ
่ฟ็จ็ๆไบ็จ
// ่ฟ็จๆๅกๅจ๏ผsse/http/ws๏ผ: ้ซๅนถๅ๏ผbatch ~20๏ผ๏ผไป
็ฝ็ป่ฟๆฅ
// 2. ไธ็บง่ฟๆปค
// Level 1: ็ฆ็จๆฃๆฅ๏ผsettings ไธญๆ ่ฎฐไธบ disabled๏ผ
// Level 2: ่ฎค่ฏ็ผๅญๆฃๆฅ๏ผ15 ๅ้ TTL ็ๅคฑ่ดฅ็ผๅญ๏ผ
// Level 3: ๅฎ้
่ฟๆฅๅฐ่ฏ๏ผmemoized by name + config hash๏ผ
// 3. ๆตๅผ็ปๆๅ่ฐ๏ผไธ็ญๆๆๆๅกๅจ่ฟๆฅๅฎๆฏ๏ผ
// ๆฏไธชๆๅกๅจ่ฟๆฅๅฎๆๅ็ซๅณๅ่ฐ onConnectionAttempt()
// UI ๅฏไปฅๅข้ๆธฒๆๅทฒ่ฟๆฅ็ๆๅกๅจ
Race condition guard for auth cache: The source code comments: "Serialize cache writes through a promise chain to prevent concurrent read-modify-write races when multiple servers return 401 in the same batch." This is a classic concurrency problem โ 10 MCP servers returning 401 simultaneously would corrupt the cache file if writes were not serialized. The solution: serialize all write operations through a Promise chain, with read operations sharing the same memoized Promise.
10.5 MCP Configuration Deduplication Strategy
Configuration source priority:
Manual config > Plugin auto-discovery > Claude.ai browser connector
Deduplication rules ("same server" determination):
stdio servers: command array exactly identical
remote servers: URL exactly identical
Name conflicts: later overrides earlier
Enterprise policies:
allowlist + denylist with three matching modes:
Name match: exact server name
Command match: stdio server's command array
URL match: wildcard patterns (https://*/api/* โ regex)
denylist merged from all sources (always in effect)
allowlist controlled by shouldAllowManagedMcpServersOnly() policy
10.6 MCP Tool Integration with Built-in Tools
// MCP ๅทฅๅ
ทไธๅ
็ฝฎๅทฅๅ
ทๅ
ฑไบซๅไธไธชๅทฅๅ
ทๆฑ
// assembleToolPool() ๅฐไธค่
ๅๅนถ
// ๅป้่งๅ: ๅ
็ฝฎๅทฅๅ
ทไผๅ
// ๅฆๆ MCP ๆๅกๅจๆไพไบๅๅๅทฅๅ
ท๏ผๅ
็ฝฎ็ๆฌ่ขซไฟ็
// Deny ่งๅๅๆ ท้็จไบ MCP ๅทฅๅ
ท
// ๅฏไปฅๅจ settings.json ไธญ:
{
"permissions": {
"deny": ["mcp__untrusted-server"] // ็ฆๆญขๆดไธช MCP ๆๅกๅจ
},
"allowedMcpServers": ["@official/*"],
"deniedMcpServers": ["*-untrusted"]
}
10.5 MCP Skills Discovery
// ๅฏ้็นๆง: MCP_SKILLS
// ไป MCP ๆๅกๅจๅ็ฐๅนถๆณจๅ Skills
// skills/mcpSkills.ts
// MCP ๆๅกๅจๅฏไปฅๆด้ฒ Skills๏ผไธไป
ๆฏ Tools๏ผ
// Skills ๆฏๆด้ซ็บง็ๅทฅไฝๆตๆฝ่ฑก
// ้่ฟ skills builder ๆจกๅผๆณจๅ
Chapter 11: Sub-Agent System โ Multi-Agent Orchestration
Up to this point, our Harness has only one Agent. But for complex tasks, a single Agent is often insufficient โ it may need to search different parts of the codebase simultaneously, or have a dedicated "reviewer" check the code it wrote.
This is analogous to a company: the CEO cannot do everything personally and needs to delegate tasks to team members. But delegation is not simply "go do this" โ it requires clear permission scopes (which resources can you access), information isolation (do not let noise from one subtask pollute another), and result reporting (give me only the summary, not the raw data).
Claude Code's sub-Agent system is designed precisely according to this approach. Each sub-Agent has its own message history, tool set, permission mode, and token budget โ fully isolated, returning only a summary upon completion.
Figure 11-1: Multi-Agent orchestration architecture โ The parent Agent spawns isolated sub-Agents (Explore/Plan/General/Custom/Fork) via AgentTool, each with independent message history, token budget, and permission mode. The bottom shows the Coordinator/Swarm system and Worktree isolation.
11.1 Agent Tool
Agent Tool is Claude Code's sub-Agent spawning mechanism, located in src/tools/AgentTool/.
// Agent ๅฎไน็ปๆ
interface AgentDefinition {
agentType: string; // ไพ: "Explore", "Plan", "general-purpose"
description: string; // ็จ้ๆ่ฟฐ
whenToUse: string; // ไฝๆถไฝฟ็จ
tools: string[] | '*'; // ๅฏ็จๅทฅๅ
ท๏ผ'*' = ๆๆ๏ผ
maxTurns?: number; // ๆๅคง่ฝฎๆฌก้ๅถ
model?: string | 'inherit'; // ๆจกๅ้ๆฉ
permissionMode?: PermissionMode; // ๆ้ๆจกๅผ
getSystemPrompt(): string; // ็ณป็ปๆ็คบ็ๆ
}
11.2 Agent Types
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Agent Types โ
โ โ
โ general-purpose (General-Purpose Agent) โ
โ โโ Tools: all (*) โ
โ โโ Use case: complex multi-step tasks โ
โ โโ Model: inherits from parent โ
โ โ
โ Explore (Exploration Agent) โ
โ โโ Tools: read-only (Read, Glob, Grep, โ
โ โ WebFetch, ...) โ
โ โโ Use case: codebase exploration, search โ
โ โโ Cannot: edit, write, run commands โ
โ โโ Three depth levels: quick, medium, โ
โ very thorough โ
โ โ
โ Plan (Planning Agent) โ
โ โโ Tools: read-only + Plan file writing โ
โ โโ Use case: designing implementation plans โ
โ โโ Cannot: execute actual code changes โ
โ โ
โ custom (Custom Agent) โ
โ โโ Definition: ~/.claude/agents/<name>.md โ
โ โโ Frontmatter: tools, model, maxTurns โ
โ โโ System prompt: Markdown body โ
โ โ
โ Fork (Implicit Fork Agent) โ
โ โโ Experimental feature โ
โ โโ Automatically forks from parent context โ
โ โโ Inherits parent's tools and permissions โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
11.3 Sub-Agent Spawning Flow
sequenceDiagram
participant P as ็ถ Agent
participant AT as AgentTool
participant MCP as MCP ๆๅกๅจ
participant C as ๅญ Agent
participant T as ๅญ Agent ๅทฅๅ
ท้
P->>AT: Agent(type:"Explore", prompt:"...")
AT->>AT: ๅ ่ฝฝ Agent ๅฎไน
AT->>MCP: ๆฃๆฅ MCP ๆๅกๅจๅฐฑ็ปช
alt MCP ๆชๅฐฑ็ปช
loop ๆฏ 500ms ่ฝฎ่ฏข (ๆๅค 30s)
AT->>MCP: ่ฟๆฅ็ถๆ?
end
end
AT->>C: ๅๅปบ้็ฆปไธไธๆ
Note over C: messages = []<br/>็ฌ็ซ Token ้ข็ฎ<br/>็ฌ็ซๅ็ผฉ็ฎก้
AT->>T: ็ป่ฃ
ๅทฅๅ
ท้ (ๅฎไนๆๅฎๆ็ปงๆฟ)
C->>C: ็ฌ็ซ queryLoop()
loop Agent ๆง่ก
C->>T: ๅทฅๅ
ท่ฐ็จ
T-->>C: ๅทฅๅ
ท็ปๆ
end
C-->>AT: ่ฟๅๆ่ฆ
AT-->>P: ๆ่ฆ (ไธๅซๅญ Agent ๅฎๆดๅๅฒ)
Source Code Annotation Analysis: A key comment in the AgentTool source code reads: "Fork children keep the Agent tool in their pool for cache-identical tool defs, so reject fork attempts at call time." This means Fork sub-Agents retain the Agent tool in their tool pool (to maintain cache-identical tool definitions with the parent) but reject recursive Fork attempts at runtime. The primary checking mechanism is
querySource(unaffected by compaction), with message scanning as a fallback. This prevents the runaway scenario of "Agents infinitely spawning Agents."
็ถ Agent ่ฏทๆฑ: Agent(type: "Explore", prompt: "...")
โ
v
AgentTool.call()
โ
โโ 1. ๅ ่ฝฝ Agent ๅฎไน๏ผๅ
็ฝฎๆ่ชๅฎไน๏ผ
โโ 2. ๆๅปบ้็ฆป็ๆถๆฏๆฐ็ป๏ผmessages = []๏ผ
โโ 3. ้ๆฉๅทฅๅ
ท้๏ผๅฎไนไธญๆๅฎๆ็ปงๆฟ๏ผ
โโ 4. ่ฎพ็ฝฎๆ้ๆจกๅผ๏ผbubble / default / ็ปงๆฟ๏ผ
โโ 5. ๅฏๅจ็ฌ็ซ็ๆฅ่ฏขๅพช็ฏ
โ โโ ๅญ Agent ๆ่ชๅทฑ็:
โ โโ ๆถๆฏๅๅฒ
โ โโ ๅทฅๅ
ทไธไธๆ
โ โโ ๅ็ผฉ็ฎก้
โ โโ Token ้ข็ฎ
โโ 6. ๆถ้่พๅบ
โโ 7. ่ฟๅๆ่ฆ็ป็ถ Agent
Design Philosophy:
The core design of sub-Agents is context isolation. Each sub-Agent begins with a blank message list and returns only a summary upon completion. This prevents: - Subtask noise from polluting the parent context - Token budgets from being exhausted by exploratory queries - Permission leakage (the sub-Agent's tool set can be more restricted)
11.4 Custom Agents
<!-- ~/.claude/agents/code-reviewer.md -->
---
name: code-reviewer
description: Specialized agent for code review
tools: [Read, Grep, Glob]
model: claude-sonnet-4-6
maxTurns: 50
---
You are a code reviewer. Your job is to:
1. Read the changed files
2. Check for bugs, security issues, and style violations
3. Provide actionable feedback
You are READ-ONLY. You cannot modify any files.
Focus on:
- Security vulnerabilities (injection, XSS, etc.)
- Performance issues (N+1 queries, memory leaks)
- Code quality (naming, SRP, test coverage)
11.5 Coordinator / Swarm System
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Coordinator System โ
โ โ
โ src/coordinator/ โ
โ โโ Multi-Agent orchestration โ
โ โโ Team creation/deletion โ
โ โโ Task assignment โ
โ โโ State synchronization โ
โ โ
โ utils/swarm/ โ
โ โโ Coordination logic โ
โ โโ Teammate tools โ
โ โโ Communication protocol โ
โ โ
โ Tools: โ
โ โโ TeamCreateTool โ Create team Agents โ
โ โโ TeamDeleteTool โ Delete team Agents โ
โ โโ SendMessageTool โ Inter-Agent messaging โ
โ โโ TaskStopTool โ Stop tasks โ
โ โ
โ Services: โ
โ โโ teamMemorySync/ โ Multi-Agent memory โ
โ โ sync โ
โ โโ AgentSummary/ โ Agent state summaries โ
โ โโ swarm/ โ Swarm permission โ
โ polling โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
11.6 Quantitative Analysis: Sub-Agent Isolation Overhead and Benefits
| Metric | Without Sub-Agents (Single Agent) | With Sub-Agents (Isolated) |
|---|---|---|
| Context pollution risk | High (all exploration noise remains in history) | Low (sub-Agent history discarded) |
| Token consumption | O(exploration tokens + work tokens) | O(summary tokens + work tokens) |
| Exploring 10 files scenario | ~50K tokens (all retained in context) | ~2K tokens (only summary returned) |
| Token savings rate | โ | ~96% (in the above example) |
| Startup overhead | 0 | ~5-10K tokens (system prompt duplication) |
| MCP readiness wait | 0 | 0-30s (initial connection) |
Source Code Annotation: Regarding MCP readiness waiting, the source code comments: "Avoids a race condition where the agent is invoked before MCP servers finish connecting. Early exit if any required server has already failed โ no point waiting if the check will fail anyway." The 30-second timeout and 500ms polling interval are empirical values balancing wait cost and connection reliability.
Regarding Fork Agent cache optimization, the comments note: "Fork path: child inherits the PARENT's system prompt (not FORK_AGENT's) for CACHE-IDENTICAL API request prefixes." This means Fork sub-Agents reuse the parent's prompt cache, and the first API call does not need to rebuild the cache โ saving approximately 30-46K tokens of cache creation cost.
Figure 11-2: Token savings from sub-Agent context isolation โ In the "explore 10 files" scenario, operating without sub-Agents requires 50K tokens (all retained in context), while using sub-Agents requires only 2K tokens (summary only), a 96% saving. These savings compound significantly over long conversations.
11.7 Task System
// src/Task.ts โ ไปปๅก็ฑปๅ
type TaskType =
| 'local_bash' // Bash ๅฝไปคๆง่ก
| 'local_agent' // ๆฌๅฐ Agent
| 'remote_agent' // ่ฟ็จ Agent๏ผCCR๏ผ
| 'in_process_teammate' // ่ฟ็จๅ
้ๅ๏ผassistant ๆจกๅผ๏ผ
| 'local_workflow' // ๅทฅไฝๆต่ๆฌ
| 'monitor_mcp' // MCP ็ๆง
| 'dream'; // Dream ๆจกๅผไปปๅก
interface TaskState {
id: string; // ๅ็ผ + 8 ไฝ้ๆบๅญ็ฌฆ๏ผbase36๏ผ
type: TaskType;
status: 'pending' | 'running' | 'completed' | 'failed' | 'killed';
description: string;
toolUseId?: string;
startTime: number;
endTime?: number;
totalPausedMs: number;
outputFile: string; // ็ฃ็ไธ็่พๅบๆไปถ
outputOffset: number;
notified: boolean;
}
11.7 Worktree Isolation
Git Worktree Isolation Mode:
โโ Each sub-Agent works in an independent git worktree
โโ Prevents file conflicts (multiple Agents editing the same file simultaneously)
โโ Tools: EnterWorktreeTool / ExitWorktreeTool
โโ Upon completion:
โ โโ If changes exist: retain worktree, return path and branch
โ โโ If no changes: automatically clean up worktree
โโ Sandbox integration: worktree path added to sandbox allowWrite
Chapter 12: Skills & Plugins โ The Extension Ecosystem
Tools are atomic operations (read a file, run a command), while Skills are workflows ("do a code review for me," "deploy to staging"). If Tools are LEGO bricks, Skills are pre-built models โ you can use them as-is or disassemble and recombine them.
Claude Code's Skills system is what transforms it from a "coding tool" into a "workflow platform." A single
.mdfile can define a new workflow โ no TypeScript required, no recompilation needed.
12.1 Skills System
Skills are reusable workflow definitions, analogous to "advanced macros."
Built-in Skills
// src/skills/bundled/index.ts
function initBundledSkills(): void {
registerUpdateConfigSkill(); // /update-config
registerKeybindingsSkill(); // /keybindings-help
registerDebugSkill(); // /debug
registerSimplifySkill(); // /simplify
registerBatchSkill(); // /batch
// ็นๆง้จๆง Skills
if (feature('AGENT_TRIGGERS_REMOTE')) {
registerScheduleSkill(); // /schedule
}
if (feature('AGENT_TRIGGERS')) {
registerLoopSkill(); // /loop
}
if (feature('BUILDING_CLAUDE_APPS')) {
registerClaudeApiSkill(); // /claude-api
}
}
Custom Skills
<!-- ~/.claude/skills/my-deploy.md -->
---
name: deploy
description: Deploy the application to staging
args: environment
---
# Deploy Skill
When invoked with /deploy <environment>:
1. Run the test suite: `npm test`
2. Build the application: `npm run build`
3. Deploy to the specified environment:
- staging: `aws deploy --env staging`
- production: `aws deploy --env production` (requires confirmation)
4. Verify deployment health check
5. Report results
Skill Loading
// src/skills/loadSkillsDir.ts
function loadSkillsDir(dir: string): SkillDefinition[] {
// 1. ๆซๆ ~/.claude/skills/ ็ฎๅฝ
// 2. ่ฏปๅๆฏไธช .md ๆไปถ
// 3. ่งฃๆ frontmatter๏ผname, description, args๏ผ
// 4. ๅๅปบ SkillDefinition
// 5. ๆณจๅไธบๅฏ็จๅฝไปค
}
12.2 In-Depth Analysis: Skill Loading Pipeline
// src/skills/loadSkillsDir.ts โ Skill ๅ็ฝฎๆฐๆฎ่งฃๆ
// 25+ ไธช frontmatter ๅญๆฎต๏ผ
// ่บซไปฝ: name, description, version, when_to_use
// ๆง่ก: model, disable-model-invocation, user-invocable, context('fork')
// ๅทฅๅ
ท: allowed-tools, disallowed-tools
// ๅๆฐ: arguments (ๆฐ็ป/้ๅทๅ้), argument-hint
// Hooks: ้่ฟ HooksSchema() ้ช่ฏ
// ่ทฏๅพ: parseSkillPaths() (ไธ CLAUDE.md ่งๅ็ธๅๆ ผๅผ)
// ไธค็ง็ฎๅฝๆ ผๅผ๏ผ
// ๆฐๆ ผๅผ: /skills/skill-name/SKILL.md (ๆฏไธช skill ไธไธช็ฎๅฝ)
// ๆงๆ ผๅผ: /commands/namespace/file.md (ๆๅนณ markdown)
// ๆงๆ ผๅผ็ๅฝๅ็ฉบ้ด: /commands/auth/login/SKILL.md โ "auth:login"
// ๅฎๅ
จ้ๅถ๏ผ
// MCP skills (่ฟ็จ/ไธๅฏไฟก) ็ฆๆญขๆง่กๅ
่ Shell ๅฝไปค
// ๆฌๅฐ skills ๅฏไปฅ็จ `!command` ่ฏญๆณๆง่ก Shell
Skill's
context: 'fork'mode: When a skill specifiescontext: 'fork', it executes in a forked sub-Agent with its own independent message history and context window. This means the Skill's execution does not pollute the main conversation's context โ the skill returns only result text upon completion. This is Context Engineering as applied within the Skill system.
12.3 Built-in Skills Registry
Always loaded (11):
updateConfig, keybindings, verify, debug, loremIpsum,
skillify, remember, simplify, batch, stuck
Feature-gated (7):
dream โ KAIROS/KAIROS_DREAM (background memory consolidation)
hunter โ REVIEW_ARTIFACT
loop โ AGENT_TRIGGERS (loop execution)
schedule โ AGENT_TRIGGERS_REMOTE (remote scheduling)
claudeApi โ BUILDING_CLAUDE_APPS
claudeInChromeโ auto-detected
skillGeneratorโ RUN_SKILL_GENERATOR
12.4 Plugin System
src/plugins/ โ Plugin system core
src/services/plugins/ โ Plugin loading, version management, marketplace
src/utils/plugins/ โ Plugin utilities, caching
Plugin features:
โโ Version management (SemVer)
โโ Cache system (reduces redundant loading)
โโ Marketplace integration
โโ Plugin-level Hook injection
โโ Independent data directory (${CLAUDE_PLUGIN_DATA})
โโ Configuration isolation (${user_config.X})
Chapter 13: Building Your Own Harness โ A Practical Guide
The preceding 12 chapters have dissected Claude Code as a "reference implementation." Now it is time to get hands-on.
Do not attempt to build a 500,000-line Harness in one go โ that is the result of dozens of Anthropic engineers working over several years. A good Harness grows organically; it is not designed top-down. Start with the minimum viable configuration, adding constraints and capabilities incrementally as you encounter real problems.
The three levels below are not "pick one" โ they form a progressive path. Spend 1 hour building a Level 1, use it for a few weeks. Then, based on real pain points, upgrade to Level 2 and use it for several months. Only consider Level 3 when your organization requires it.
Figure 13-1: Three-tier Harness maturity radar chart โ Level 1 (green) has basic capabilities in Context Management and Permission Control but lacks Multi-Agent, MCP, and enterprise MDM. Level 3 (purple) approaches full marks across all 8 dimensions. Note that Level 2 (yellow) is the "sweet spot" for most teams โ moderate investment with broad coverage.
13.1 Level 1: Personal Harness (1-2 Hours)
Step 1: Create CLAUDE.md
# CLAUDE.md
## ้กน็ฎๆถๆ
- src/: ๆบไปฃ็
- tests/: ๆต่ฏๆไปถ
- docs/: ๆๆกฃ
## ๅผๅ่ง่
- ่ฏญ่จ: TypeScript strict mode
- ๆต่ฏ: vitest
- ๆ ผๅผๅ: prettier
- ๆไบค: conventional commits
## ้่ฆ็บฆๆ
- ไธ่ฆไฟฎๆน migrations/ ็ฎๅฝ๏ผๅทฒ้จ็ฝฒ็่ฟ็งปไธๅฏๅ๏ผ
- ๆๆ API ็ซฏ็นๅฟ
้กปๆ่ฎค่ฏไธญ้ดไปถ
- ไธ่ฆไฝฟ็จ any ็ฑปๅ
Step 2: Configure Basic Permissions
// .claude/settings.json
{
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"Bash(npm test *)",
"Bash(npm run *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)"
],
"deny": [
"Bash(sudo *)",
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)"
]
}
}
Step 3: Add Basic Hooks
// .claude/settings.json๏ผhooks ้จๅ๏ผ
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo 'Writing file: '$(echo $HOOK_INPUT | jq -r '.tool_input.file_path')",
"if": "Write(*.ts)"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "npm run lint -- --quiet 2>/dev/null || true",
"if": "Bash(npm *)",
"async": true
}
]
}
]
}
}
13.2 Level 2: Team Harness (1-2 Days)
Step 1: Shared Settings
// .claude/settings.json๏ผๆไบคๅฐ git๏ผ
{
"permissions": {
"allow": [
"Bash(npm test *)",
"Bash(npm run lint *)",
"Bash(git *)"
],
"deny": [
"Bash(sudo *)",
"Bash(rm -rf /)",
"Bash(npm publish *)",
"Write(*.env*)",
"Write(*.key)",
"Write(*.pem)"
],
"defaultMode": "default"
},
"sandbox": {
"enabled": true,
"fsWrite": {
"deny": [".env", ".env.*", "*.key", "*.pem", "secrets/"]
},
"network": {
"allowedDomains": [
"*.github.com",
"registry.npmjs.org",
"api.anthropic.com"
]
}
}
}
Step 2: Define Team Agents
<!-- .claude/agents/architect.md -->
---
name: architect
description: Reviews architecture decisions and suggests improvements
tools: [Read, Grep, Glob]
model: claude-opus-4-6
maxTurns: 30
---
You are an architecture reviewer. Analyze the codebase and provide:
1. Dependency analysis
2. Coupling/cohesion assessment
3. SOLID principle compliance
4. Suggestions for improvement
Focus on high-level design, not line-by-line code review.
<!-- .claude/agents/test-writer.md -->
---
name: test-writer
description: Writes comprehensive tests for existing code
tools: [Read, Write, Edit, Bash, Glob, Grep]
model: claude-sonnet-4-6
maxTurns: 100
---
You are a test engineer. For any given code:
1. Analyze the code and identify test cases
2. Write comprehensive tests (unit + integration)
3. Run the tests to verify they pass
4. Ensure >80% coverage for touched files
Step 3: Configure MCP Servers
// .claude/mcp.json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
},
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": { "DATABASE_URL": "${DATABASE_URL}" }
}
}
}
Step 4: Create Team Skills
<!-- .claude/skills/review-pr.md -->
---
name: review-pr
description: Comprehensive PR review workflow
args: pr_number
---
# PR Review Workflow
1. Fetch the PR diff: `gh pr diff $ARGUMENTS`
2. Read all changed files
3. For each file:
- Check for security issues
- Check for performance concerns
- Check for test coverage
- Check for style consistency
4. Generate a structured review comment
5. Post the review: `gh pr review $ARGUMENTS --comment --body "..."`
13.3 Level 3: Organization-Level Harness (1-2 Weeks)
Step 1: Enterprise MDM Settings
// /managed/managed-settings.json
{
"permissions": {
"deny": [
"Bash(sudo *)",
"Bash(curl * | bash)",
"Bash(wget * | bash)",
"Write(*.env*)",
"Write(*.pem)",
"Write(*.key)"
],
"disableBypassPermissionsMode": "disable"
},
"sandbox": {
"enabled": true,
"lockedByPolicy": true,
"network": {
"deniedDomains": ["*.malware.com", "*.phishing.net"]
}
},
"deniedMcpServers": ["*-untrusted", "*-experimental"]
}
Step 2: Audit Hooks
// /managed/managed-settings.d/audit.json
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "https://audit.company.com/api/agent-actions",
"headers": {
"Authorization": "Bearer $AUDIT_API_KEY",
"Content-Type": "application/json"
},
"allowedEnvVars": ["AUDIT_API_KEY"],
"async": true
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo '{\"session_started\": true}' | curl -s -X POST -d @- https://audit.company.com/api/sessions",
"async": true
}
]
}
]
}
}
Step 3: Implement the Plan-Work-Review Cycle
Reference the 13 guard rules from the claude-code-harness project:
R01: Block sudo commands
R02: Forbid writing to .git/, .env, SSH keys
R03: Forbid Shell writing to protected files
R04: Writing outside project root requires confirmation
R05: rm -rf requires confirmation
R06: Forbid git push --force
R07-R09: Mode-specific guards (work/codex/breezing)
R10: Forbid --no-verify, --no-gpg-sign
R11: Forbid git reset --hard main/master
R12: Warn on direct push to main/master
R13: Warn on editing protected files
Implemented as a PreToolUse Hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node guard-rules.js",
"timeout": 5
}
]
}
]
}
}
Chapter 14: Advanced Patterns and Design Philosophy
This is the final content chapter and arguably the most important. The preceding chapters covered "how Claude Code does things." This chapter addresses "why it does things this way."
Tools and frameworks become obsolete, but design philosophies do not. If you retain only one chapter from this book, it should be this one โ these principles apply to any Agent Harness, not just Claude Code.
14.1 Ten Design Philosophies of Claude Code
1. Async Generator Streaming Architecture
ไธๆฏ: function query() โ Promise<FinalResult>
่ๆฏ: async function* query() โ AsyncGenerator<StreamEvent>
ไธบไปไน: ๅฎขๆท็ซฏๅฏไปฅๅจๆจกๅๆ่ๆถๅฐฑๅผๅงๆธฒๆ
็จๆทๅฏไปฅ้ๆถไธญๆญ
่ฟๅบฆๅฏน็จๆทๅฏ่ง
2. State Machine via Continue Sites
ไธๆฏ: ๆพๅผ็ถๆๆไธพ + switch/case
่ๆฏ: while(true) + 7 ไธช continue ็ซ็น
ไธบไปไน: ๆขๅค่ทฏๅพๆฏ่ช็ถ็๏ผcontinue = ้่ฏ๏ผ
็ถๆ่ฝฌๆขๆฏๅฑ้จ็๏ผๅช้ๆดๆฐ State ๅฏน่ฑก๏ผ
ๆฐๆขๅค่ทฏๅพๅฏไปฅๆทปๅ ่ไธ้ๆ
3. Compile-Time Feature Gating
ไธๆฏ: if (config.feature_x) { ... }
่ๆฏ: if (feature('FEATURE_X')) { ... } // bun:bundle ็ผ่ฏๆถๆฑๅผ
ไธบไปไน: ๅค้จๅ่ก็ไธๅ
ๅซไปปไฝๅ
้จ็นๆงไปฃ็ ๏ผ่ฟๅญ็ฌฆไธฒ้ฝๆฒกๆ๏ผ
ไธๅญๅจ่ฟ่กๆถๅๆฏ้ขๆตๅผ้
ไปฃ็ ๅคงๅฐๆๅฐๅ
4. Cache Prefix Stability
ไธๆฏ: ๅฐๅ
็ฝฎๅทฅๅ
ทๅ MCP ๅทฅๅ
ทๆททๅๆๅบ
่ๆฏ: ๅ
็ฝฎๅทฅๅ
ทๆๅบๅไฝไธบ็จณๅฎๅ็ผ๏ผMCP ๅทฅๅ
ทๆๅบๅ่ฟฝๅ
ไธบไปไน: Anthropic API ็ prompt cache ๅบไบๅ็ผๅน้
MCP ๅทฅๅ
ทๅๅๆถ๏ผๅ
็ฝฎๅทฅๅ
ทๅ็ผไธๅ โ ็ผๅญไธๅคฑๆ
ๅคงๅน
้ไฝ API ๆๆฌ
5. Defense in Depth
Layer 1: CLAUDE.md๏ผๆๅฏผๆง็บฆๆ๏ผ
Layer 2: Permission Rules๏ผๅฃฐๆๆง็บฆๆ๏ผ
Layer 3: Hooks๏ผๅฏ็ผ็จ็บฆๆ๏ผ
Layer 4: YOLO Classifier๏ผAI ็บฆๆ๏ผ
Layer 5: Sandbox๏ผ็ณป็ป็บง็บฆๆ๏ผ
Layer 6: Hardcoded Denials๏ผไธๅฏ่ฆ็็บฆๆ๏ผ
ไธบไปไน: ๆฏไธๅฑ้ฝๅฏ่ฝ่ขซ็ป่ฟ
ๅคๅฑๅ ๅ ไฝฟ็ป่ฟๆฆ็ๆๆฐไธ้
ๆๅ
ๅฑ๏ผ็กฌ็ผ็ ๏ผๆ ๆณ้่ฟ้
็ฝฎ็ฆ็จ
6. Data-Driven Extensibility
ไธๆฏ: ไฟฎๆนๆบ็ ๆทปๅ ๆฐๅ่ฝ
่ๆฏ: settings.json + agents/*.md + skills/*.md + hooks
ไธบไปไน: ้ๅทฅ็จๅธไน่ฝๅฎๅถ Harness
ๅฎๅถไธๆ ธๅฟไปฃ็ ่งฃ่ฆ
ๅ็บงๆถไธไธขๅคฑๅฎๅถ
7. Context as a Scarce Resource
่ฎพ่ฎก: ๅทฅๅ
ทๅปถ่ฟๅ ่ฝฝใ่ฎฐๅฟๆ้้ๅ ใๅ็บงๅ็ผฉ็ฎก้
ToolSearch ๅ็ฐๆบๅถใMicrocompact ่ๅ็ญ็ฅ
ไธบไปไน: ไธไธๆ็ชๅฃๆ้๏ผๅณไฝฟ 1M token๏ผ
ๆ ๅ
ณไฟกๆฏ้ไฝๆจกๅๆง่ฝ
ๆๆฌไธ token ไฝฟ็จ้ๆๆญฃๆฏ
8. Hierarchical Configuration Overrides
7 ็บง่ฎพ็ฝฎ: CLI > Flag > Policy > Managed > Local > Project > User
ไธบไปไน: ไธๅๅฑ็บงๆไธๅ็ไฟกไปป็บงๅซ
ไผไธ็ฎก็ๅๅฏไปฅๅผบๅถ็ญ็ฅ
้กน็ฎ็ปดๆค่
ๅฏไปฅ่ฎพๅฎๅ็้ป่ฎค
็จๆทๅฏไปฅไธชไบบๅพฎ่ฐ
9. Isolated Sub-Agent Context
่ฎพ่ฎก: ๅญ Agent ไป็ฉบ็ฝๆถๆฏๅ่กจๅผๅง
ๅฎๆๅๅช่ฟๅๆ่ฆ
็ถ็บงไธไธๆไธ่ขซๆฑกๆ
ไธบไปไน: ๆข็ดขๆงไปปๅกๅฏ่ฝไบง็ๅคง้ๅช้ณ
ๅญ Agent ็ๅคฑ่ดฅไธๅบๅฝฑๅ็ถ็บง
Token ้ข็ฎ้็ฆป
10. Reversibility-First
่ฎพ่ฎก: ๆไปถ็ผ่พ้่ฟ Edit๏ผๆฟๆขๅญ็ฌฆไธฒ๏ผ๏ผไธๆฏ Write๏ผ่ฆ็๏ผ
ๆฏไธชๅทฅๅ
ท่ฐ็จๆ undo ่ฝๅ
่ชๅจๅฟซ็
ง๏ผๆไปถๅๅฒ๏ผ
ไธบไปไน: Agent ไผ็ฏ้
็จๆท้่ฆ่ฝปๆพๅๆป
"ๅ
่กๅจๅๅฎกๆฅ"ๆฏ"ๅ
ๅฎกๆฅๅ่กๅจ"ๆด้ซๆ
14.2 Entropy Management Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Entropy Management Patterns โ
โ โ
โ 1. Periodic Cleanup Agent โ
โ /loop 1h "Check for dead code, unused โ
โ imports, expired TODOs, โ
โ inconsistent naming" โ
โ โ
โ 2. Documentation Validation Agent โ
โ SessionEnd Hook โ Check whether CLAUDE.md โ
โ is consistent with actual code state โ
โ โ
โ 3. Dependency Audit Agent โ
โ /schedule "Every Monday 0:00" โ โ
โ "Check for outdated dependencies, โ
โ security vulnerabilities, license โ
โ compliance" โ
โ โ
โ 4. Test Coverage Guard โ
โ PostToolUse Hook (Write *.ts) โ โ
โ "Run coverage check; warn if decreased" โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
14.3 Bridge System โ IDE Integration
src/bridge/
โโโ bridgeMain.ts โ Main loop
โโโ bridgeMessaging.ts โ Messaging protocol
โโโ bridgePermissionCallbacks.ts โ Permission handling
โโโ replBridge.ts โ REPL bridge
โโโ jwtUtils.ts โ JWT authentication
โโโ sessionRunner.ts โ Session execution
Supports:
โโโ VS Code extension
โโโ JetBrains extension
โโโ Bidirectional communication (IDE โ Claude Code)
โโโ Permission synchronization
โโโ Session state synchronization
14.4 Plan Mode V2
5-Phase Workflow:
Phase 1: Interview
โโ Gather requirements
โโ Understand context
โโ Launch up to 3 Explore Agents (in parallel)
Phase 2: Design
โโ Launch Plan Agent
โโ Generate implementation plan
โโ May launch multiple to explore different directions
Phase 3: Review
โโ Read key files to verify the plan
โโ Ensure alignment with user intent
โโ Clarify via AskUserQuestion
Phase 4: Final Plan
โโ Write to ~/.claude/plans/<name>.md
โโ Include context, steps, file paths
โโ Verification section describes how to test
Phase 5: Exit Plan Mode
โโ Invoke ExitPlanMode
โโ Wait for user approval
โโ Begin execution upon approval
14.5 Remote Triggers and Cron Scheduling
// ็นๆง้จๆง: AGENT_TRIGGERS + AGENT_TRIGGERS_REMOTE
// ScheduleCronTool โ ๅๅปบๅฎๆถ Agent
{
schedule: "0 9 * * MON", // ๆฏๅจไธๆฉ 9 ็น
prompt: "Review open PRs and post summary to Slack",
model: "claude-sonnet-4-6",
}
// RemoteTriggerTool โ ่ฟ็จ่งฆๅ
{
trigger: "deploy-check",
prompt: "Verify deployment health",
}
14.6 Voice Integration
src/voice/ โ Voice input
src/services/voice.ts โ Voice/STT integration
src/services/voiceStreamSTT.ts โ Streaming speech-to-text
Feature gate: VOICE_MODE
Supports:
โโโ Voice input transcription
โโโ Streaming processing (real-time recognition)
โโโ Integration into the main query loop
14.7 The "Rippable Harness" Principle
Core idea: The Harness should simplify as model capabilities improve
Examples:
โโโ Today: Need 13 guard rules to prevent dangerous operations
โ โโโ Future, better models may not need half of them
โ
โโโ Today: Four-level compaction pipeline for limited context
โ โโโ Future, larger context windows may require only one level
โ
โโโ Today: YOLO classifier needs two-stage checking
โ โโโ Future, primary models may have better built-in safety awareness
โ
โโโ Design implications:
โโโ Each Harness component should be independently disableable
โโโ Settings overrides preferred over hardcoding
โโโ Simplify rather than stack as models improve
โโโ "The best Harness is the one you eventually don't need"
Appendix C: Complete ToolUseContext Type Definition
ToolUseContext is the "request context" that threads through the entire execution pipeline โ understanding it means understanding the Harness's information flow:
// src/Tool.ts โ ็ๅฎไปฃ็ ๏ผ็ฒพ็ฎๆณจ้็๏ผ
export type ToolUseContext = {
// ===== ้
็ฝฎ๏ผๅช่ฏป๏ผ=====
options: {
commands: Command[] // ๅฏ็จ็ Slash ๅฝไปค
debug: boolean // ่ฐ่ฏๆจกๅผ
mainLoopModel: string // ไธปๅพช็ฏไฝฟ็จ็ๆจกๅ
tools: Tools // ๅฏ็จๅทฅๅ
ทๅ่กจ
verbose: boolean // ่ฏฆ็ป่พๅบ
thinkingConfig: ThinkingConfig// ๆ่ๆจกๅผ้
็ฝฎ
mcpClients: MCPServerConnection[] // MCP ๆๅกๅจ่ฟๆฅ
mcpResources: Record<string, ServerResource[]> // MCP ่ตๆบ
isNonInteractiveSession: boolean // ้ไบคไบๅผไผ่ฏ
agentDefinitions: AgentDefinitionsResult // Agent ๅฎไน
maxBudgetUsd?: number // ๆๅคง้ข็ฎ๏ผ็พๅ
๏ผ
customSystemPrompt?: string // ่ชๅฎไน็ณป็ปๆ็คบ
appendSystemPrompt?: string // ่ฟฝๅ ็ณป็ปๆ็คบ
querySource?: QuerySource // ๆฅ่ฏขๆฅๆบๆ ่ฏ
refreshTools?: () => Tools // ๅทฅๅ
ทๅ่กจๅทๆฐๅฝๆฐ
}
// ===== ๆงๅถ =====
abortController: AbortController // ๅๆถไฟกๅท
messages: Message[] // ๅฝๅๆถๆฏๅๅฒ
// ===== ็ถๆ่ฏปๅ =====
readFileState: FileStateCache // ๆไปถ็ถๆ็ผๅญ
getAppState(): AppState // ่ทๅๅบ็จ็ถๆ
setAppState(f: (prev) => AppState): void // ๆดๆฐๅบ็จ็ถๆ
// ===== ๅ่ฐ =====
setToolJSX?: SetToolJSXFn // ่ฎพ็ฝฎๅทฅๅ
ท UI
addNotification?: (n: Notification) => void // ๆทปๅ ้็ฅ
sendOSNotification?: (opts) => void // ๅ้็ณป็ป้็ฅ
setInProgressToolUseIDs: (f) => void // ่ท่ธช่ฟ่กไธญ็ๅทฅๅ
ท
setResponseLength: (f) => void // ่ท่ธชๅๅบ้ฟๅบฆ
updateFileHistoryState: (updater) => void // ๆดๆฐๆไปถๅๅฒ
updateAttributionState: (updater) => void // ๆดๆฐๅฝๅ ็ถๆ
// ===== Agent ็ธๅ
ณ =====
agentId?: AgentId // Agent ๆ ่ฏ
agentType?: string // Agent ็ฑปๅ
requireCanUseTool?: boolean // ๆฏๅฆ้่ฆๆ้ๆฃๆฅ
// ===== ๅจๆ Skill/Memory =====
nestedMemoryAttachmentTriggers?: Set<string> // ๅตๅฅ่ฎฐๅฟ่งฆๅๅจ
loadedNestedMemoryPaths?: Set<string> // ๅทฒๅ ่ฝฝ็่ฎฐๅฟ่ทฏๅพ
dynamicSkillDirTriggers?: Set<string> // ๅจๆ Skill ็ฎๅฝ่งฆๅๅจ
discoveredSkillNames?: Set<string> // ๅทฒๅ็ฐ็ Skill ๅ็งฐ
// ===== ๆ้ =====
localDenialTracking?: DenialTrackingState // ๆฌๅฐๆ็ป่ท่ธช
toolDecisions?: Map<string, {...}> // ๅทฅๅ
ทๅณ็ญ่ฎฐๅฝ
contentReplacementState?: ContentReplacementState // ๅ
ๅฎนๆฟๆข็ถๆ
// ===== ้ซ็บง =====
requestPrompt?: (sourceName, summary?) => // Hook prompt ่ฏทๆฑ
(request: PromptRequest) => Promise<PromptResponse>
handleElicitation?: (serverName, params, signal) => // MCP elicitation
Promise<ElicitResult>
onCompactProgress?: (event) => void // ๅ็ผฉ่ฟๅบฆๅ่ฐ
criticalSystemReminder_EXPERIMENTAL?: string // ๅ
ณ้ฎ็ณป็ปๆ้
}
Design Analysis: ToolUseContext is a large context object, analogous to a Request object in web frameworks. It carries everything needed for tool execution โ configuration, state, callbacks, and permissions. Although the 50+ fields may appear complex, this design avoids: 1. Global state (each query has its own context) 2. Parameter explosion (no need to pass arguments individually) 3. Tight coupling (tools use only the fields they need)
Appendix D: 13 Guard Rules Reference Model
The claude-code-harness project defines 13 declarative guard rules, implemented as PreToolUse Hooks:
// ๆฅ่ช claude-code-harness/core/src/guardrails/
// R01: ้ปๆญข sudo โ ๆฐธ่ฟไธๅ
่ฎธๆๆ
{ rule: 'R01', test: (input) => /^sudo\s/.test(input.command),
action: 'deny', reason: 'sudo commands are not allowed' }
// R02: ็ฆๆญขๅๅ
ฅๆๆ่ทฏๅพ
{ rule: 'R02', test: (input) => SENSITIVE_PATHS.some(p => input.file_path?.startsWith(p)),
action: 'deny', paths: ['.git/', '.env', '~/.ssh/'] }
// R03: ็ฆๆญข Shell ๅๅ
ฅๅไฟๆคๆไปถ
{ rule: 'R03', test: (input) => isShellWriteToProtectedFile(input),
action: 'deny' }
// R04: ้กน็ฎๆ น็ฎๅฝๅคๅๅ
ฅ้็กฎ่ฎค
{ rule: 'R04', test: (input) => !isWithinProjectRoot(input.file_path),
action: 'ask', reason: 'Writing outside project root' }
// R05: rm -rf ้็กฎ่ฎค
{ rule: 'R05', test: (input) => /rm\s+(-rf|-r\s+-f|-f\s+-r)/.test(input.command),
action: 'ask', reason: 'Recursive delete detected' }
// R06: ็ฆๆญข force push
{ rule: 'R06', test: (input) => /git\s+push\s+.*--force/.test(input.command),
action: 'deny', reason: 'Force push is not allowed' }
// R07-R09: ๆจกๅผ็นๅฎ้ฒๆค
{ rule: 'R07', mode: 'work', ... } // Work ๆจกๅผ้ๅถ
{ rule: 'R08', mode: 'codex', ... } // Codex ้ๆ้ๅถ
{ rule: 'R09', mode: 'breezing', ... } // Breezing ๆจกๅผ้ๅถ
// R10: ็ฆๆญข่ทณ่ฟๅฎๅ
จ้ฉๅญ
{ rule: 'R10', test: (input) => /--no-verify|--no-gpg-sign/.test(input.command),
action: 'deny', reason: 'Skipping safety hooks is not allowed' }
// R11: ็ฆๆญข hard reset ๅฐ main
{ rule: 'R11', test: (input) => /git\s+reset\s+--hard\s+(main|master)/.test(input.command),
action: 'deny' }
// R12: ่ญฆๅ็ดๆฅๆจ้ๅฐ main
{ rule: 'R12', test: (input) => isDirectPushToMain(input),
action: 'warn' }
// R13: ่ญฆๅ็ผ่พๅไฟๆคๆไปถ
{ rule: 'R13', test: (input) => PROTECTED_FILES.includes(input.file_path),
action: 'warn', files: ['package-lock.json', 'yarn.lock', 'Cargo.lock'] }
These rules can be fully implemented using Claude Code's native Hook system:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "node guard-rules.js",
"if": "Bash(sudo *)"
}]
}
]
}
}
Chapter 15: Building a Mini Harness from Scratch (Hands-on Lab)
The preceding 14 chapters dissected Claude Code's 512K lines of source code. Now it is time to build โ we will construct a 200-line Mini Harness in Python from scratch, implementing Claude Code's core design patterns.
15.1 Goal: A Minimal Harness Running in 30 Minutes
#!/usr/bin/env python3
"""mini_harness.py โ ไธไธช 200 ่ก็ Claude Code ๅผ Agent Harness"""
import anthropic, json, os, subprocess, re
from pathlib import Path
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# ====================================================
# Layer 1: Tool System (ๅฏนๅบ Claude Code ็ src/Tool.ts)
# ====================================================
TOOLS = [
{
"name": "bash",
"description": "Run a shell command. Returns stdout and exit code.",
"input_schema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "The command to run"}
},
"required": ["command"]
}
},
{
"name": "read_file",
"description": "Read a file and return its contents.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path to read"}
},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "Write content to a file.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path to write"},
"content": {"type": "string", "description": "Content to write"}
},
"required": ["path", "content"]
}
}
]
# ====================================================
# Layer 2: Permission Rules (ๅฏนๅบ Claude Code ็ permissions.ts)
# ====================================================
DENY_PATTERNS = [
r"^sudo\s", # R01: ็ฆๆญข sudo
r"rm\s+-rf\s+/", # R05: ็ฆๆญข rm -rf /
r"git\s+push\s+--force", # R06: ็ฆๆญข force push
]
ALLOW_TOOLS = {"read_file"} # ๅช่ฏปๅทฅๅ
ทๅง็ปๅ
่ฎธ
def check_permission(tool_name: str, tool_input: dict) -> tuple[bool, str]:
"""Layer 2: ๆ้ๆฃๆฅ (็ฎๅ็ hasPermissionsToUseTool)"""
if tool_name in ALLOW_TOOLS:
return True, "allowed by rule"
if tool_name == "bash":
cmd = tool_input.get("command", "")
for pattern in DENY_PATTERNS:
if re.search(pattern, cmd):
return False, f"denied: matches {pattern}"
# ้ป่ฎค๏ผ่ฏข้ฎ็จๆท๏ผๅฏนๅบ Claude Code ็ 'ask' ๆจกๅผ๏ผ
answer = input(f" Allow {tool_name}({json.dumps(tool_input)[:80]})? [y/n] ")
return answer.lower() == 'y', "user decision"
# ====================================================
# Layer 3: Tool Execution (ๅฏนๅบ Claude Code ็ toolExecution.ts)
# ====================================================
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Layer 3: ๆง่กๅทฅๅ
ท"""
if tool_name == "bash":
result = subprocess.run(
tool_input["command"], shell=True,
capture_output=True, text=True, timeout=30
)
return f"exit_code={result.returncode}\n{result.stdout}{result.stderr}"
elif tool_name == "read_file":
return Path(tool_input["path"]).read_text()
elif tool_name == "write_file":
Path(tool_input["path"]).write_text(tool_input["content"])
return f"Written to {tool_input['path']}"
return "Unknown tool"
# ====================================================
# Layer 4: Context Engineering (ๅฏนๅบ Claude Code ็ CLAUDE.md)
# ====================================================
def load_context() -> str:
"""Layer 4: ๅ ่ฝฝ CLAUDE.md ไธไธๆ"""
claude_md = Path("CLAUDE.md")
if claude_md.exists():
return f"\n<project_context>\n{claude_md.read_text()}\n</project_context>\n"
return ""
SYSTEM_PROMPT = f"""You are a coding assistant. Use tools to help the user.
Be concise. Don't explain what you're about to do โ just do it.
{load_context()}"""
# ====================================================
# Layer 5: Agent Loop (ๅฏนๅบ Claude Code ็ query.ts)
# ====================================================
def agent_loop():
"""Layer 5: ๆ ธๅฟ Agent ๅพช็ฏ"""
messages = []
print("Mini Harness (type 'exit' to quit)")
while True:
user_input = input("\n> ")
if user_input.lower() == 'exit':
break
messages.append({"role": "user", "content": user_input})
# ๅ
ๅพช็ฏ๏ผๅทฅๅ
ท่ฐ็จ้พ๏ผๅฏนๅบ queryLoop ็ while(true)๏ผ
while True:
response = client.messages.create(
model=MODEL, max_tokens=4096,
system=SYSTEM_PROMPT,
messages=messages, tools=TOOLS,
)
# ๆถ้ๅฉๆๆถๆฏ
messages.append({"role": "assistant", "content": response.content})
# ๆ ๅทฅๅ
ท่ฐ็จ โ ๆๅฐๆๆฌๅนถ้ๅบๅ
ๅพช็ฏ
if response.stop_reason != "tool_use":
for block in response.content:
if hasattr(block, 'text'):
print(f"\n{block.text}")
break
# ๆง่กๅทฅๅ
ท่ฐ็จ
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
# ๆ้ๆฃๆฅ๏ผLayer 2๏ผ
allowed, reason = check_permission(block.name, block.input)
if not allowed:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Permission denied: {reason}",
"is_error": True,
})
print(f" โ {block.name} denied: {reason}")
continue
# ๆง่กๅทฅๅ
ท๏ผLayer 3๏ผ
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
print(f" โ {block.name} completed")
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
if __name__ == "__main__":
agent_loop()
15.2 Running Your Mini Harness
# 1. ่ฎพ็ฝฎ API key
export ANTHROPIC_API_KEY="sk-ant-..."
# 2. ๅๅปบไธไธช CLAUDE.md
echo "# Project Rules
- Use TypeScript for all new files
- Run tests after making changes: npm test" > CLAUDE.md
# 3. ่ฟ่ก
python mini_harness.py
# 4. ๆต่ฏๆ้็ณป็ป
> run sudo rm -rf /
โ bash denied: matches ^sudo\s
> read the file package.json
โ read_file completed (่ชๅจๅ
่ฎธ๏ผๆ ้็กฎ่ฎค)
> run npm test
Allow bash({"command": "npm test"})? [y/n] y
โ bash completed
15.3 Comparison with Claude Code's Design Patterns
| Mini Harness (200 lines) | Claude Code (512K lines) | Where the Gap Lies |
|---|---|---|
| 3 tools | 43+ tools | Tool count |
| Regex permission matching | 7-level hierarchy + AI classifier | Permission precision |
| User y/n confirmation | 5 modes + Hook system | Flexibility |
| No compaction | Four-level compaction pipeline | Long conversation support |
| No sub-Agents | 5 Agent types | Complex task decomposition |
| Static CLAUDE.md loading | Memory system + dynamic injection | Context management |
| No retry | 7 continue sites | Robustness |
Exercises: Try adding the following features to the Mini Harness (approximately 20 lines each): 1. Simple Hook system: Run shell scripts before and after tool execution 2. Token counter: Track token consumption per API call and cumulative cost 3. Session save/restore: Serialize messages to a JSON file with
--resumesupport 4. Sub-Agent: Add anagenttool that forks a new agent_loop() with independent messages
Chapter 16: Competitive Analysis
Claude Code is not the only AI coding Agent. Understanding competitor Harness designs helps distinguish which patterns are universal and which are unique innovations of Claude Code.
16.1 Harness Architecture Comparison of Three Major Agents
| Dimension | Claude Code | Cursor | GitHub Copilot |
|---|---|---|---|
| Runtime | Terminal CLI | VS Code fork | VS Code extension |
| Interaction Mode | Autonomous Agent | Collaborative editor | Reactive autocomplete + Agent Mode |
| Agent Loop | while(true) + 7 continue sites | Not public | Not public |
| Tool System | 43+ built-in + MCP extension | Built-in editing + terminal | Built-in editing + terminal |
| Permission Model | 5 modes + 7-level rules + AI classifier | Editor-level sandbox | GitHub permissions |
| Hook System | 26 events x 4 types | Not public | Not public |
| Context Management | CLAUDE.md + memory + four-level compaction | .cursorrules + codebase index | .github/copilot-instructions.md |
| Multi-Agent | 5 Agent types + Swarm orchestration | 8 parallel Agents (worktree) | Single Agent |
| MCP Support | 6 transport protocols | MCP support | Limited |
| Open Source Visibility | Source analyzable (512K LOC) | Closed source | Closed source |
| Evaluation Integration | SWE-bench + Headless Profiler | Not public | Not public |
| Market Share (2026) | 41% | ~15% | 38% |
Data sources: dev.to/raxxostudios, faros.ai, tech-insider.org
16.2 Key Design Difference Analysis
Unique Innovations of Claude Code: 1. Compile-time feature gating (bun:bundle dead code elimination) โ no competitor has this mechanism 2. Six-layer defense-in-depth security model โ the deepest security layering 3. Two-stage YOLO classifier โ the only Agent using an independent AI to review permissions 4. Reversible tool design (Edit uses string replacement, not file overwrite) โ reduces destruction risk 5. Prompt cache stability sorting โ tool ordering optimizes cache hit rates
Unique Innovations of Cursor: 1. Codebase indexing (global semantic understanding) โ Claude Code relies on Grep/Glob 2. 8 parallel Agents + worktree isolation โ more aggressive parallelism strategy 3. Editor-native integration โ superior diff preview and inline editing experience
Universal Patterns (shared by all competitors): 1. Project-level configuration files (CLAUDE.md / .cursorrules / copilot-instructions.md) 2. Tool call loops (ReAct pattern) 3. Permission confirmation mechanisms 4. MCP protocol support (gradually converging)
16.3 Architectural Insights from the OpenDev Paper
arXiv:2603.05344v1 "Building AI Coding Agents for the Terminal" proposes several important architectural patterns:
- Scaffolding vs. Harness Separation:
- Scaffolding = assembly before the first prompt (system prompt compilation, tool schema generation)
- Harness = everything after assembly (ReAct loop, tool dispatch, secure execution)
-
Claude Code's
main.tsx(parallel prefetch) corresponds to Scaffolding;query.ts(queryLoop) corresponds to Harness -
Dual-Mode via Subagent Rather Than State Machine:
- The paper finds that state machine patterns (Plan Mode <-> Execute Mode switching) tend to get stuck
- Recommended approach: Plan Mode as a tool-restricted sub-Agent rather than a state switch
-
Claude Code's Plan Mode follows exactly this design โ
EnterPlanModeToolchanges the available tool set -
Defense-in-Depth: Five Layers vs. Claude Code's Six:
- The paper proposes five layers (Prompt -> Schema -> Runtime Approval -> Tool Validation -> Lifecycle Hooks)
-
Claude Code adds a sixth: hardcoded denials (non-configurable, unbypassable)
-
Context Pressure as the Central Design Constraint:
- The paper argues that token budget drives all downstream architectural decisions
- Claude Code's four-level compaction pipeline, tool lazy loading, and memory prefetching all serve this end
Conclusion: From Reader to Builder
Congratulations on reading this far. If you have carefully digested the content of the preceding 14 chapters, you now possess more Harness Engineering knowledge than most AI engineers.
Three sentences to summarize the entire book:
-
The Agent is not the product; the Harness is. Models will be replaced, but your carefully designed permission rules, Hook pipelines, and sandbox configurations โ those are your moat.
-
Constraints are power, not limitations. Every precise permission rule you add enables you to confidently let the Agent do more. The six layers of defense in depth are what allow Claude Code to run dangerous commands like
rmandgit pushin production โ because every layer stands guard. -
Build incrementally; do not over-engineer. Spend 1 hour building a Level 1 Harness with CLAUDE.md and basic permissions. Add Hooks when you hit real pain points. Configure MCP and custom Agents when team collaboration demands it. The best Harness is the one that is "just enough."
One final thought exercise: Claude Code comprises 512,664 lines of code, yet its Agent Loop core is nothing more than a single
while(true)and a State object. The remaining 99.9% of the code all answers the same question โ "How do we make this loop run reliably in the real world?" That is the entire meaning of Harness Engineering.
Now go build your own Harness.
Appendix A: Claude Code Source File Index
Core Engine
| File | Size | Purpose |
|---|---|---|
src/main.tsx |
803 KB | Entry point, CLI bootstrap, parallel prefetch |
src/query.ts |
68 KB | Core Agent loop (queryLoop) |
src/QueryEngine.ts |
46 KB | LLM query engine, system prompt construction |
src/Tool.ts |
29 KB | Tool base interface, 60+ methods |
src/tools.ts |
25 KB | Tool registry, pool assembly, cache stability |
src/Task.ts |
3.2 KB | Task type definitions |
src/commands.ts |
25 KB | Command registration, conditional imports |
Permissions and Security
| File | Purpose |
|---|---|
src/utils/permissions/permissions.ts |
Main permission logic, decision pipeline |
src/types/permissions.ts |
Permission type definitions (13 KB) |
src/utils/permissions/permissionsLoader.ts |
Load rules from settings |
src/utils/permissions/denialTracking.ts |
Denial tracking |
src/utils/permissions/yoloClassifier.ts |
Auto mode classifier |
src/utils/sandbox/sandbox-adapter.ts |
Sandbox adapter (985 lines) |
src/tools/BashTool/shouldUseSandbox.ts |
Sandbox decision logic |
Hooks
| File | Purpose |
|---|---|
src/utils/hooks.ts |
Hook execution engine |
src/utils/hooks/hooksConfigManager.ts |
Hook configuration management |
src/utils/hooks/hooksSettings.ts |
Hook settings loading |
src/schemas/hooks.ts |
Zod Schema definitions |
src/types/hooks.ts |
Hook type definitions |
src/services/tools/toolHooks.ts |
Tool-specific Hook execution |
Settings and Configuration
| File | Purpose |
|---|---|
src/utils/settings/settings.ts |
Core settings management |
src/utils/settings/types.ts |
Settings Schema definitions (600+ lines) |
src/utils/settings/settingsCache.ts |
In-memory cache |
src/utils/settings/permissionValidation.ts |
Permission rule validation |
MCP
| File | Purpose |
|---|---|
src/services/mcp/client.ts |
MCP client, 6 transport types |
src/services/mcp/config.ts |
MCP configuration loading |
src/skills/mcpSkills.ts |
MCP Skills discovery |
Agents and Tasks
| File | Purpose |
|---|---|
src/tools/AgentTool/ |
Sub-Agent spawning |
src/tools/AgentTool/forkSubagent.ts |
Implicit forking |
src/tools/AgentTool/agentMemory.ts |
Agent memory |
src/coordinator/ |
Multi-Agent orchestration |
src/utils/swarm/ |
Swarm coordination logic |
Memory
| File | Purpose |
|---|---|
src/memdir/memoryScan.ts |
Memory scanning |
src/memdir/memoryTypes.ts |
Memory type definitions |
src/services/extractMemories/ |
Automatic memory extraction |
src/services/teamMemorySync/ |
Team memory synchronization |
State and UI
| File | Purpose |
|---|---|
src/state/AppState.tsx |
Main React context (23 KB) |
src/state/AppStateStore.ts |
Zustand-like Store (21 KB) |
src/cli/print.ts |
Rich text printing (212 KB) |
src/components/App.tsx |
Root component |
Appendix B: Reference Resources
Open Source Projects
| Project | Description | URL |
|---|---|---|
| learn-claude-code | 12-lesson Harness Engineering course | github.com/shareAI-lab/learn-claude-code |
| claude-code-harness | Plan-Work-Review cycle implementation | github.com/Chachamaru127/claude-code-harness |
| your-claude-engineer | Agent Harness demo (Slack+GitHub+Linear) | github.com/coleam00/your-claude-engineer |
Recommended Learning Path
Week 1: Foundations
โโโ Read Chapters 1-3 of this tutorial
โโโ Build a Level 1 personal Harness (CLAUDE.md + basic permissions)
โโโ Hands-on experiment: use Claude Code in your own project
โโโ Understand: Agent Loop, Tool Dispatch, Plan Mode
Week 2: Constraints and Security
โโโ Read Chapters 4-7 of this tutorial
โโโ Configure Permission Rules
โโโ Write a PreToolUse Hook
โโโ Understand: Permission Model, Hooks, Sandbox
Week 3: Context and Memory
โโโ Read Chapters 8-9 of this tutorial
โโโ Optimize CLAUDE.md
โโโ Configure Settings hierarchy
โโโ Hands-on experiment: write a PreToolUse Hook
โโโ Understand: Context Engineering, Memory, Compaction
Week 4: Extension and Collaboration
โโโ Read Chapters 10-12 of this tutorial
โโโ Configure MCP servers (start with GitHub MCP)
โโโ Create custom Agents and Skills
โโโ Understand: MCP, Sub-Agents, Skills
Week 5: Multi-Agent and Production
โโโ Read Chapters 13-14 of this tutorial
โโโ Build a team Harness
โโโ Implement the Plan-Work-Review cycle
โโโ Understand: Team Protocols, Worktree Isolation, Entropy Management
Week 6+: Advanced
โโโ Deep dive into Claude Code source code (start with query.ts)
โโโ Build an organization-level Harness
โโโ Explore: Bridge, Voice, Remote Triggers
Key Concept Quick Reference
| Concept | One-Line Definition |
|---|---|
| Harness | All infrastructure beyond the Agent itself |
| Agent Loop | The core cycle of message -> LLM -> tool -> loop |
| Tool | A standardized interface for Agent interaction with the outside world |
| Permission Mode | A mode determining the Agent's degree of autonomy (5 types) |
| Permission Rule | A declarative allow/deny/ask rule |
| Hook | A programmable callback for lifecycle events (4 types) |
| Sandbox | System-level file/network/process restrictions |
| CLAUDE.md | A project-level persistent context file |
| Memory | Cross-session structured memory (4 types) |
| Compaction | Context window compression strategy (4 levels) |
| MCP | Model Context Protocol, extending Harness capabilities |
| Sub-Agent | A context-isolated child Agent |
| Skill | A reusable workflow definition |
| Plan Mode | A 5-phase planning workflow |
| Worktree | Git worktree isolation preventing parallel conflicts |
| Entropy Management | Combating system degradation through periodic maintenance |
| Feature Gate | Compile-time feature toggle (bun:bundle) |
| Prompt Cache | API caching strategy optimized by tool ordering |
| YOLO Classifier | Two-stage AI automatic permission approval |
| Defense in Depth | Six stacked layers of security protection |
"Build rippable harnesses โ the best harness is the one you eventually don't need."
This tutorial is based on reverse engineering of the Claude Code source code (2026-03-31 snapshot). All referenced file paths and code patterns are derived from actual source code analysis.
References
[1] Martin Fowler. "Harness Engineering." martinfowler.com/articles/exploring-gen-ai/harness-engineering.html, 2026.
[2] OpenAI. "Harness Engineering: Leveraging Codex in an Agent-First World." openai.com/index/harness-engineering/, 2026.
[3] "Building AI Coding Agents for the Terminal: Scaffolding, Harness, Context Engineering, and Lessons Learned." arXiv:2603.05344v1, 2026.
[4] "Evaluation and Benchmarking of LLM Agents: A Survey." arXiv:2507.21504v1, 2025.
[5] Jimenez et al. "SWE-bench: Can Language Models Resolve Real-World GitHub Issues?" ICLR, 2024.
[6] NxCode. "Harness Engineering: The Complete Guide to Building Systems That Make AI Agents Actually Work." nxcode.io, 2026.
[7] Phil Schmid. "The Importance of Agent Harness in 2026." philschmid.de/agent-harness-2026, 2026.
[8] Epsilla. "Harness Engineering: Why the Focus is Shifting from Models to Agent Control Systems." epsilla.com/blogs, 2026.
[9] EleutherAI. "lm-evaluation-harness: A Framework for Few-Shot Evaluation of Language Models." github.com/EleutherAI/lm-evaluation-harness, 2024.
[10] Harness Engineering Academy. "What is Harness Engineering? A Complete Introduction." harnessengineering.academy/blog, 2026.
Last updated: 2026-04-01
Featured Findings
Two reverse-engineered findings discovered while writing the textbook above. Both cover infrastructure not previously documented in any public analysis.
๐ฌ Grove: The Hidden Training-Data Pipeline in Claude Code's Source
From a user's keyboard to BigQuery's training warehouse โ a complete, source-verifiable data pipeline.
While reverse-engineering Claude Code's 512K LOC TypeScript source, I found a previously unreported infrastructure for training-data collection. Not speculation, not analogy โ every step has a precise file path and line number. This section traces the full pipeline.
One-sentence summary
Anthropic ships a system named "Grove" in Claude Code that explicitly tells users their data will be used to "train and improve" models, extends data retention from 30 days to 5 years, ships 796 telemetry events through a Protobuf-typed dual pipeline, and lands them in privileged BigQuery columns. The same pipeline carries SWE-bench evaluation IDs โ meaning training-data collection and agent-capability evaluation share the same infrastructure.
Ring 1: Grove โ "Help Improve Claude"
In Claude Code's source, a component named Grove (src/components/grove/Grove.tsx) presents users with a toggle:
// src/components/grove/Grove.tsx
// Line 47:
<Text bold={true}>You can help improve Claude </Text>
// Line 56 โ the key copy:
"Allow the use of your chats and coding sessions to train and improve
Anthropic AI models. Change anytime in your Privacy Settings."
// Line 63 โ data-retention notice:
"Updates to data retention โ To help us improve our AI models and safety
protections, we're extending data retention to 5 years."
// Line 116 โ what the retention actually means:
"Turning ON the improve Claude setting extends data retention from
30 days to 5 years. Turning it OFF keeps the default 30-day data
retention. Delete data anytime."
This is not a hidden backdoor. It's Anthropic's official, user-facing training-data collection mechanism. What hasn't been publicly analyzed is the engineering implementation behind it.
Grove's type definitions
// src/services/api/grove.ts Lines 25-35:
export type AccountSettings = {
grove_enabled: boolean | null // user opted into "Help improve Claude"
grove_notice_viewed_at: string | null
}
export type GroveConfig = {
grove_enabled: boolean
domain_excluded: boolean // some orgs/domains are excluded
notice_is_grace_period: boolean // within the grace window
notice_reminder_frequency: number | null
}
// API endpoints:
// GET /api/oauth/account/settings
// POST /api/oauth/account/grove_notice_viewed
// PATCH /api/oauth/account/settings
// GET /api/claude_code_grove
Worth noting: the domain_excluded field means certain enterprise customers' data is auto-excluded from training. grove_enabled is the toggle โ flip it on, and your retention silently jumps from 30 days to 5 years.
Why 5 years? Product analytics typically retains data for 30โ90 days. The most plausible explanation for 5-year retention is that the data feeds training datasets โ and training datasets have a much longer lifecycle than ops logs.
Ring 2: 796 telemetry events ร dual pipeline
Claude Code's telemetry runs regardless of the Grove switch โ Grove controls retention duration (30 days vs 5 years), not whether collection happens. The source contains 796 unique analytics event names prefixed with tengu_. Every API call, every tool execution, every permission decision, every Bash command โ all logged.
These events flow through a dual pipeline:
| Channel | Endpoint | Payload | Purpose |
|---|---|---|---|
| Datadog | logs.us5.datadoghq.com | Redacted (_PROTO_* stripped) | Ops monitoring |
| 1P (first-party) | /api/event_logging/batch | Full payload incl. privileged fields | Lands in BigQuery |
// src/services/analytics/sink.ts Lines 48-71:
function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
const sampleResult = shouldSampleEvent(eventName)
if (sampleResult === 0) return
if (shouldTrackDatadog()) {
// Datadog only sees redacted data
void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
}
// 1P gets the full payload, including _PROTO_* privileged fields
logEventTo1P(eventName, metadataWithSampleRate)
}
// src/services/analytics/firstPartyEventLoggingExporter.ts Lines 714-750:
// _PROTO_* keys are PII-tagged values meant only for privileged BQ
// columns. Hoist known keys to proto fields, then defensively strip any
// remaining _PROTO_* so an unrecognized future key can't silently land
// in the general-access additional_metadata blob.
That comment spells out the architecture: _PROTO_* fields are routed to privileged columns in BigQuery, physically isolated from general-access analytics data. This isn't a casual product log โ it's an intentionally tiered, segregated data warehouse.
Ring 3: Protobuf schema โ production-grade data pipeline
These events aren't ad-hoc JSON blobs. They're strictly defined by a Protobuf schema, compiled from an external monorepo:
// src/services/analytics/firstPartyEventLoggingExporter.ts:
// Adding a field? Update the monorepo proto first (go/cc-logging):
// event_schemas/.../claude_code/v1/claude_code_internal_event.proto
// then run `bun run generate:proto` here.
//
// claude_code_internal_event.ts โ 865 lines of generated proto code
The generated schema defines 29 fields:
message ClaudeCodeInternalEvent {
string event_name = 1;
Date client_timestamp = 2;
string model = 3;
string session_id = 4;
string user_type = 5; // "ant" vs "external"
EnvironmentMetadata env = 7; // platform, version, CI flags, 30+ subfields
string additional_metadata = 13; // event-specific data (BASE64 JSON)
string device_id = 17;
// โฌ๏ธ These three are the surprising part
string swe_bench_run_id = 18;
string swe_bench_instance_id = 19;
string swe_bench_task_id = 20;
// Swarm/team agent attribution
string agent_id = 22;
string parent_session_id = 23;
string agent_type = 24;
// PII privileged columns
string skill_name = 27;
string plugin_name = 28;
string marketplace_name = 29;
}
Three things this tells us:
- Not a temporary analytics hack โ Protobuf + external monorepo (
go/cc-logging) + compile-time enforcement = production-grade data-pipeline infrastructure. - SWE-bench fields are embedded in every event โ not a separate eval system; they share the same pipeline as user data.
- Dedicated PII privileged columns โ
skill_name,plugin_nameare isolated, indicating tiered data management.
Ring 4: SWE-bench โ evals and training data share the pipeline
This is the most surprising finding. The three SWE-bench IDs aren't off in some standalone eval script โ they're embedded in the Proto schema of every analytics event.
// src/services/analytics/metadata.ts Lines 722-724:
sweBenchRunId: process.env.SWE_BENCH_RUN_ID || '',
sweBenchInstanceId: process.env.SWE_BENCH_INSTANCE_ID || '',
sweBenchTaskId: process.env.SWE_BENCH_TASK_ID || '',
// Lines 912-920 โ written into the Proto event:
if (coreFields.sweBenchRunId) {
core.swe_bench_run_id = coreFields.sweBenchRunId
}
What this means: when Anthropic runs SWE-bench internally, all 796 tengu_* events get tagged with swe_bench_run_id + swe_bench_instance_id + swe_bench_task_id. In BigQuery, they can do full agent-trajectory analysis per SWE-bench problem โ which tools fired, how many tokens each API call cost, what the permission classifier decided, whether each command succeeded.
Implication: Claude Code is not just a product. It's also Anthropic's agent-capability evaluation platform. Eval data and user-interaction data share the same Protobuf schema, the same pipeline, and the same BigQuery warehouse.
Combined with the full evaluation harness CLI surface:
// src/main.tsx โ eval-related CLI flags:
'-p, --print' // non-interactive mode, exit after output
'--output-format <format>' // "text" | "json" | "stream-json"
'--max-turns <turns>' // max agent turns
'--max-budget-usd <amount>'// budget limit
'--json-schema <schema>' // structured output validation
This is a complete evaluation loop: env vars tag the task โ --print mode runs non-interactively โ streaming NDJSON output โ exit code signals success/failure โ all telemetry lands in BigQuery.
Ring 5: Developer comments โ "training data"
Two internal developer comments are the most direct evidence:
// src/utils/messages.ts Line 245:
"content is fake, which poisons training data if submitted"
// src/utils/sessionStorage.ts Line 4388:
"Ant transcripts keep the wrapper so /share training data sees REPL usage"
The first says certain content "poisons training data" โ the developer knows message content lands in the training pipeline.
The second describes /share data as "training data" โ the most direct textual evidence.
Ring 6: Five-layer performance telemetry
Beyond functional data, Anthropic has embedded a five-layer performance-measurement system:
| Layer | Implementation | Sampling | Output sink |
|---|---|---|---|
| Headless Latency Profiler | headlessProfiler.ts | 100% internal / 5% external | tengu_headless_latency event |
| Frame Timing | interactiveHelpers.tsx | On demand | Local JSONL files |
| FPS Tracker | fpsTracker.ts | Continuous | avgFps + P99 frame time |
| Perfetto Chrome Trace | perfettoTracing.ts | Internal only | ~/.claude/traces/ |
| OpenTelemetry | instrumentation.ts | Continuous | OTLP / Prometheus / BigQuery |
Note the last row: OpenTelemetry's exporter list includes BigQuery. Performance data flows into the same warehouse.
Ring 7: Competitor detection
A small but telling detail:
// src/utils/codeIndexing.ts Lines 52, 82:
// Detect aider CLI and MCP server
// CLI: 'aider'
// MCP: /^aider$/i
// Use: track Claude Code coexisting with competitors in analytics
Anthropic tracks whether users run competitor tools (Aider) alongside Claude Code. That data also flows into BigQuery.
Full pipeline diagram
User interaction (every tool call, API request, Bash command)
โ
โโ grove_enabled = true โ retention = 5 years
โ
โผ
796 tengu_* events ร 40+ metadata fields
โ
โโโ Datadog (redacted, _PROTO_* stripped) โ ops monitoring
โ
โโโ 1P API /api/event_logging/batch (full payload)
โ
โโ Protobuf schema, strictly typed (865 lines generated)
โโ _PROTO_* fields โ BigQuery privileged columns (isolated)
โโ SWE-bench IDs embedded โ eval shares the pipeline
โ
โผ
BigQuery warehouse
โ
โโ Feedback text "approved for BQ" (after PII redaction)
โโ Transcript shares โ POST /api/claude_code_shared_session_transcripts
โโ OpenTelemetry performance data
โโ Competitor-tool detection
โ
โผ
Developer comments confirm: "training data"
โ
โผ
???? (server-side training process, not visible to client)
Why this pipeline matters
- It's complete โ every step from UI toggle to BigQuery privileged columns has a source line number.
- It's tiered โ Datadog gets redacted, 1P gets the full payload, BigQuery has privileged-column isolation.
- It's reused โ SWE-bench evals and user data share the same pipeline and schema.
- It's not speculation โ Grove UI literally says "train and improve"; developer comments literally say "training data".
- It hasn't been reported โ as of publication, no public analysis mentions "Grove", BigQuery privileged columns, or the SWE-bench-shares-telemetry finding.
What I cannot verify from the client source
To be honest about the limits of this analysis:
- What method Anthropic uses to train (RLHF? DPO? GRPO?)
- How BigQuery data is preprocessed and filtered
- Whether SWE-bench eval data is used directly for training, or only for evaluation
- How preference pairs are assembled server-side (no preference-pair construction in client code)
None of those gaps weaken the core finding: the complete data pipeline from a user's keyboard to BigQuery's training warehouse is verifiable in the client source code.
๐ก๏ธ How Claude Code Detects You Distilling Its Model โ A 5-Layer Defense, Reverse-Engineered
Reverse-engineered from 512K lines of source: the complete engineering implementation Anthropic ships to prevent model theft. Each layer has a precise file path and line number.
Why distillation is the AI industry's #1 threat
Model distillation โ recording API traffic from a frontier model and using its outputs to train a smaller model โ is one of the largest commercial threats in AI. Replicate capabilities at a fraction of the cost.
This isn't theoretical. It's actively happening:
- January 2025: OpenAI publicly accused DeepSeek of distilling its reasoning models via the API, claiming DeepSeek-R1's training data contained OpenAI model outputs [1]. OpenAI subsequently hardened chain-of-thought protection, training models to avoid leaking reasoning paths and deploying classifiers to detect and block CoT leakage [2].
- February 2026: Google disclosed that attackers used over 100,000 structured prompts to clone Gemini's reasoning logic โ specifically targeting "reasoning trace" extraction [3]. Google then trained Gemini to recognize distillation probes and refuse to comply.
- Academic follow-up: 2025โ2026 saw a surge in anti-distillation research, including information-theoretic distillation-resistant sampling [4], semantic watermark fingerprinting [5], empirical analyses of whether watermarks block distillation [6], and trace rewriting as a defense [7].
Against this backdrop, Anthropic shipped a 5-layer defense system in Claude Code. Unlike competitors', the full implementation is exposed via source โ making this the first complete, production-level analysis of a commercial AI system's anti-distillation engineering.
Overview: 5 layers
Figure 1 โ The five-layer defense: client attestation, fingerprint attribution, fake-tool injection, signature-bearing blocks, and the distillation-resistant streamlined output mode.
Layer 1: Native Client Attestation โ "Are you really Claude Code?"
How it works
Before each API request, Bun's native HTTP stack injects an attestation token into the serialized request body. The server verifies the token to confirm the request came from a real Claude Code client, not a third-party proxy or recording tool.
Source
// src/constants/system.ts Lines 64-68:
// When NATIVE_CLIENT_ATTESTATION is enabled, includes a `cch=00000` placeholder.
// Before the request is sent, Bun's native HTTP stack finds this placeholder
// in the request body and overwrites the zeros with a computed hash. The
// server verifies this token to confirm the request came from a real Claude
// Code client. See bun-anthropic/src/http/Attestation.zig for implementation.
// Lines 81-82:
const cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : ''
Engineering details
Figure 2 โ Attestation.zig at the Bun layer rewrites the cch=00000 placeholder to a real hash before the request hits the wire. The server validates the token; invalid or missing โ request rejected.
Why a placeholder instead of direct injection? The source comment explains: "same-length replacement avoids Content-Length changes and buffer reallocation" โ a 5-character placeholder gets swapped with a 5-character hash, no body length change, no memory reallocation.
Implementation language: Zig (bun-anthropic/src/http/Attestation.zig), running at Bun's native HTTP layer. The auth logic isn't in JavaScript or TypeScript โ it can't be bypassed by patching the JS layer.
Layer 2: Fingerprint Attribution โ "Which client generated this?"
How it works
Each API request carries a 3-character fingerprint computed via SHA256 over a salt + specific characters from the user's first message + the version string. This lets Anthropic trace every piece of training data back to its originating client.
Source
// src/utils/fingerprint.ts:
// Hardcoded salt (must stay in sync with backend verification)
export const FINGERPRINT_SALT = '59cf53e54c78'
export function computeFingerprint(
messageText: string,
version: string,
): string {
// Pull characters at positions 4, 7, 20 from the user's first message
const indices = [4, 7, 20]
const chars = indices.map(i => messageText[i] || '0').join('')
const fingerprintInput = `${FINGERPRINT_SALT}${chars}${version}`
// SHA256, take first 3 hex chars
const hash = createHash('sha256').update(fingerprintInput).digest('hex')
return hash.slice(0, 3)
}
// src/constants/system.ts Lines 78-91:
// Fingerprint is embedded in the version string sent in headers
const version = `${MACRO.VERSION}.${fingerprint}`
const header = `x-anthropic-billing-header: cc_version=${version};
cc_entrypoint=${entrypoint};${cch}${workloadPair}`
Compute flow
Figure 3 โ Salt + chars-at-positions-[4,7,20] + version โ SHA256 โ first 3 hex chars โ embedded in the billing header.
The source comment explicitly notes: "IMPORTANT: Do not change this method without careful coordination with 1P and 3P (Bedrock, Vertex, Azure) APIs." โ the fingerprint algorithm has to stay consistent across every API provider.
Layer 3: Fake Tools Injection โ "Honeypot tools"
How it works
This is the cleverest layer. Claude Code sends anti_distillation: ['fake_tools'] to the API; the server mixes fake tool definitions into the normal tool list. If anyone records this API traffic and trains a model on it, the distilled model will "know" the fake tools โ and that knowledge is the evidence of distillation.
Source
// src/services/api/claude.ts Lines 301-313:
// Anti-distillation: send fake_tools opt-in for 1P CLI only
if (
feature('ANTI_DISTILLATION_CC')
? process.env.CLAUDE_CODE_ENTRYPOINT === 'cli' &&
shouldIncludeFirstPartyOnlyBetas() &&
getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_anti_distill_fake_tool_injection',
false,
)
: false
) {
result.anti_distillation = ['fake_tools']
}
Trigger conditions
Figure 4 โ All four gates (feature flag, CLI entrypoint, first-party API, GrowthBook gate) must pass before fake tools are opted in.
Critical limitation: only fires on Anthropic's first-party API. Requests via Bedrock, Vertex, or other third-party proxies do not get fake tools โ shouldIncludeFirstPartyOnlyBetas() excludes those providers.
Honeypot mechanics: the actual fake-tool definitions live server-side; the client only sends an opt-in signal. Means the fake tools can be rotated server-side without shipping a new client.
Layer 4: Signature-Bearing Blocks โ "Signatures as locks"
How it works
The API's thinking blocks and connector_text blocks both carry a cryptographic signature bound to the API key that generated them. Switch keys, and those signatures instantly become invalid โ the API rejects them with a 400.
Source
// src/utils/messages.ts Lines 5060-5064:
// Strip signature-bearing blocks (thinking, redacted_thinking, connector_text)
// from all assistant messages. Their signatures are bound to the API key that
// generated them; after a credential change (e.g. /login) they're invalid and
// the API rejects them with a 400.
export function stripSignatureBlocks(messages: Message[]): Message[] {
const result = messages.map(msg => {
if (msg.type !== 'assistant') return msg
const content = msg.message.content
const filtered = content.filter(block => {
if (isThinkingBlock(block)) return false // drop thinking
if (feature('CONNECTOR_TEXT')) {
if (isConnectorTextBlock(block)) return false // drop connector_text
}
return true
})
return { ...msg, message: { ...msg.message, content: filtered } }
})
return result
}
Connector Text โ the anti-distillation mechanism
// src/utils/betas.ts Lines 279-284:
// POC: server-side connector-text summarization (anti-distillation). The
// API buffers assistant text between tool calls, summarizes it, and returns
// the summary with a signature so the original can be restored on subsequent
// turns โ same mechanism as thinking blocks.
Figure 5 โ The API buffers assistant text between tool calls, summarizes it, and returns a summary plus a signature_delta. On the next turn, the signed summary lets the API restore the original text โ but anyone recording the traffic only sees summaries.
Current limitation: Connector Text is only enabled for Anthropic-internal users (USER_TYPE === 'ant') โ measuring TTFT/TTLT/capacity impact. Feature gate: tengu_slate_prism.
Layer 5: Streamlined Mode โ "Distillation-resistant output format"
How it works
The source comment names this layer literally: "distillation-resistant output format". It strips information specifically to reduce the value of the output as distillation training data.
Source
// src/utils/streamlinedTransform.ts Lines 1-9:
// Streamlined mode is a "distillation-resistant" output format that:
// - Keeps text messages intact
// - Summarizes tool calls with cumulative counts (resets when text appears)
// - Omits thinking content
// - Strips tool list and model info from init messages
What gets filtered
| Original output | Streamlined output |
|---|---|
| Text content | โ Preserved |
| Thinking content | โ Dropped |
| Tool call details | ๐ Cumulative counts only |
| Tool definition list | โ Dropped |
| Model info | โ Dropped |
What does the would-be distiller lose?
- Thinking content: the model's reasoning chain โ the most valuable distillation signal โ is dropped entirely
- Tool definitions: the full schema for 43+ tools is removed; can't replicate the tool system
- Tool call details: only summaries like "Searched 3 files, Read 2 files" remain โ can't learn tool-use strategy
Defense matrix
| Layer | Defends against | Detect / Block | Bypass difficulty |
|---|---|---|---|
| Attestation | Spoofed clients | Block | High โ requires reversing native Zig code |
| Fingerprint | Anonymous distillation | Post-hoc trace | Medium โ algorithm is public |
| Fake Tools | API traffic recording | Post-hoc detect | Low โ don't request fake_tools |
| Signature | Cross-key data reuse | Block | High โ server-side validation |
| Streamlined | Output distillation | Reduce value | Low โ only applies to SDK mode |
Where this defense system is weak
From a pure security-research perspective (understanding the defense is how you improve the defense):
1. Fake Tools only fires on first-party. Bedrock/Vertex requests don't get fake tools, because shouldIncludeFirstPartyOnlyBetas() excludes third-party providers. If the distiller uses AWS Bedrock's Claude endpoint, this layer is wholly inactive.
2. Fingerprint algorithm is fully public via the source. Salt 59cf53e54c78, character positions [4, 7, 20], SHA256 โ all visible. A distiller can forge fingerprints (though the server may have other validation).
3. Connector Text only on internal users. process.env.USER_TYPE === 'ant' means external users' conversation text gets no Connector Text protection.
4. Streamlined Mode only applies to SDK output. Normal CLI interactions don't go through Streamlined Transform; full tool-call details and thinking content are visible in standard output.
5. Attestation is the strongest layer. Native client auth runs in Zig, outside the JavaScript-controllable surface. But if a distiller calls the API directly (not via Claude Code), this layer doesn't apply โ it protects against "impersonating Claude Code", not "using the raw API".
Key insight
Anthropic's anti-distillation strategy isn't a single point of defense โ it's a layered, complementary system. None of the layers is perfect, but stacked together they cover different attack surfaces:
Figure 6 โ Each attack vector is covered by a different defense layer.
The deepest defense isn't on the client at all: the actual reasoning capability (the weights) never leaves Anthropic's servers. Client-side defenses protect training-data quality and API usage attribution โ even if a distiller records output, Anthropic can trace its origin and detect fake-tool traces in the resulting distilled model.
Comparison with prior analyses
Alex Kim's blog [8] first reported the existence of fake tools and connector text, but only described the code snippets โ without analyzing the full defense system. This article's new contributions:
- Systematic 5-layer framework โ organizes scattered findings into a layered defense system.
- Native Client Attestation Zig analysis โ not previously mentioned in any public analysis.
- Full fingerprint algorithm reconstruction โ including salt, character positions, hash method.
- Streamlined Mode as a distillation-resistant format โ the source comment literally says "distillation-resistant", not previously reported.
- Defense matrix and weakness analysis โ assessing each layer's effectiveness and limitations from a security-research stance.
Mapping to the academic literature
Anthropic's anti-distillation implementation maps interestingly onto recent academic work:
| Anthropic implementation | Academic concept | Reference |
|---|---|---|
| Fake Tools Injection | Radioactive watermarking / data poisoning | [6] Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation? (ACL 2025) |
| Fingerprint Attribution | LLM fingerprinting | [5] LLM Fingerprinting via Semantically Conditioned Watermarks (arXiv 2505.16723) |
| Connector Text Summarization | Trace rewriting / output perturbation | [7] Protecting Language Models Against Unauthorized Distillation through Trace Rewriting (arXiv 2602.15143) |
| Streamlined Mode | Distillation-resistant decoding | [4] Towards Distillation-Resistant LLMs: An Information-Theoretic Perspective (arXiv 2602.03396) |
| 5-layer stack | Defense-in-depth for ML systems | [2] IAPS: AI Distillation Attacks โ The Case for Targeted Government Intervention |
Worth noting: academic papers are largely theoretical proposals or controlled experiments โ Claude Code is a production deployment serving millions of users. That gives this analysis distinct empirical value: we're seeing the engineering trade-offs Anthropic actually made under real adversarial pressure, not a paper algorithm.
References
[1] Berkeley Law. "The Innovation Dilemma: AI Distillation in OpenAI v. DeepSeek." The Network, 2025. Link
[2] IAPS. "AI Distillation Attacks: The Case for Targeted Government Intervention." Institute for AI Policy and Strategy, 2026. Link
[3] Google Cloud Blog. "GTIG AI Threat Tracker: Distillation, Experimentation, and Integration of AI for Adversarial Use." 2026. Link
[4] arXiv:2602.03396. "Towards Distillation-Resistant Large Language Models: An Information-Theoretic Perspective." 2026. Link
[5] arXiv:2505.16723. "LLM Fingerprinting via Semantically Conditioned Watermarks." 2025. Link
[6] arXiv:2502.11598. "Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation?" ACL 2025 Main. Link
[7] arXiv:2602.15143. "Protecting Language Models Against Unauthorized Distillation through Trace Rewriting." 2026. Link
[8] Alex Kim. "The Claude Code Source Leak: fake tools, frustration regexes, undercover mode, and more." 2026. Link
All code references taken from the Claude Code source (2026-03-31 snapshot). File paths and line numbers reflect actual source locations.