Home Gallery AISPA Paper GitHub Follow

claude-code-book system prompt

Category: Coding agents. Audited against the AISPA standard.

5 Prompts on record
0 Flagged instructions
AI audit Audit source
D1 · Identity Transparency D2 · Truthfulness & Information Integrity D3 · Privacy & Data Protection D4 · Tool/Action Safety D5 · User Agency & Manipulation Prevention D6 · Unsafe Request Handling D7 · Harm Prevention & User Safety D8 · Fairness, Inclusion & Neutrality

claude-code-book - en Part 1 Foundations 03 The Tool System Agent ...

39896 characters

# Chapter 3: The Tool System -- The Agent's Hands > "If all you have is a hammer, everything looks like a nail." > -- Abraham Maslow **Learning Objectives:** After reading this chapter, you will be able to: - Master the design patterns of Claude Code's 45+ tools and understand the design philosophy of the five-element protocol - Understand the complete architecture of the tool definition protocol, registration mechanism, and orchestration engine - Analyze the scheduling principles and practical effects of concurrency partitioning strategies - Understand the elegant design of the StreamingToolExecutor four-stage state machine - Evaluate the engineering value of the deferred tool discovery mechanism --- Maslow's quote could not be more fitting when applied to Agent tool systems. If an Agent only has a Bash tool, every task becomes a Shell command -- reading files with `cat`, searching code with `grep`, editing files with `sed`. This works, but it violates the engineering principle of "using the right tool for the right problem." Claude Code's tool system provides 45+ specialized tools, each optimized for a specific type of operation -- like equipping different professional tools for different tasks, rather than solving every problem with a single hammer. ## 3.1 The Tool Definition Protocol Every tool in Claude Code follows a unified type contract -- `Tool<Input, Output, Progress>`. This contract is defined in the tool type core module and serves as the cornerstone of the entire tool system. Understanding it means understanding the anatomical structure of the Agent's "hands." The design philosophy of this protocol can be summarized as "interface as architecture": by defining strict type interfaces, all architectural constraints of the tool system -- permission checks, concurrency control, progress reporting, UI rendering -- are enforced by the compiler. Developers cannot "forget" to implement a method, because the type checker will immediately raise an error. ### Core Types: Tool, Tools, ToolDef, buildTool The `Tool` type is a generic interface that accepts three type parameters: - `Input extends AnyObject`: The tool's input type defined using a Zod schema, ensuring each tool's input is a structured object. - `Output`: The tool's output type, freely defined. - `P extends ToolProgressData`: The tool's progress data type, used for streaming feedback. The separation of the three generic parameters is a deliberate design decision. If input and output types were merged into one, the tool's signature would become harder to read; if the progress type were omitted, the tool would be unable to provide real-time feedback during execution. Separating the three gives each concern its own type space, and the compiler can check them independently. The five elements that every tool must implement are as follows: ```mermaid flowchart TD subgraph protocol["Tool Five-Element Protocol"] name["Element 1: Name & Aliases<br/>Unique name identifier + optional aliases<br/>Supports backward compatibility"] schema["Element 2: Zod Schema<br/>Runtime validation + API communication<br/>Type safety barrier"] perm["Element 3: Permission Model<br/>validateInput → hasPermissions → checkPermissions<br/>Three-layer hierarchical check"] exec["Element 4: Execution Logic<br/>Core execution method + contextModifier<br/>Context modification channel"] ui["Element 5: UI Rendering<br/>Six rendering methods covering full lifecycle<br/>Deep React component integration"] end name --> schema --> perm --> exec --> ui classDef element fill:#e8f4f8,stroke:#2196F3,stroke-width:2px,color:#1565C0 class name,schema,perm,exec,ui element ``` **Element 1: Name and Aliases** Each tool has a unique name identifier, along with optional aliases for backward compatibility. When a tool is renamed, the old name can continue to match through an alias. The tool lookup function checks both the primary name and aliases. The existence of the alias mechanism reveals an engineering practice principle: **in public APIs, renaming is an "add-only" operation.** Even when a tool's name is no longer accurate (e.g., renaming from `SearchTool` to `GrepTool`), the old name must remain available through an alias, otherwise configurations, scripts, and user habits that depend on the old name would all break. **Element 2: Zod Schema** Each tool uses Zod to define the schema for its input parameters. The Zod schema serves a dual purpose: 1. **Runtime validation**: Before tool execution, the parameters generated by the LLM are parsed through Zod, ensuring type and constraint correctness. This embodies the "don't trust external input" principle -- the LLM's output is uncontrollable, and tools must protect themselves. 2. **API communication**: The Zod schema is converted through a transformation layer into a JSON Schema that is sent to the API, letting the model know the meaning and constraints of each parameter. This means the schema definition is the tool's "user manual" -- the parameter descriptions the model sees come from the `describe()` calls in the Zod schema. > **Cross-Reference:** Zod schema validation occurs in the first stage of the Chapter 4 permission pipeline (validateInput), which is a concrete manifestation of the "embedded security boundaries" design principle. **Element 3: Permission Model** The three permission-related methods form a layered permission check pipeline: 1. **Layer 1: Input validation (validateInput)**: Runs before permission checks, used to reject invalid inputs. This is a "data legitimacy" check, unrelated to permissions. 2. **Layer 2: Permission checks (hasPermissionsToUseTool + checkPermissions)**: Contains tool-specific permission logic. Different tools have different permission check granularity -- the Read tool may only check whether a path is in an allow list, while the Bash tool needs to parse commands and assess risk levels. 3. **Layer 3: Runtime property determination**: Affects the tool's concurrency scheduling strategy. For example, `isConcurrencySafe()` marks whether a tool can be executed in parallel. The design philosophy behind the three-layer separation is "separation of concerns": data validation doesn't care about permission policies, permission policies don't care about concurrency scheduling. Each layer does one thing, but the three layers串联 together to provide complete protection. **Element 4: Execution Logic** This is the tool's core execution method. It receives parsed input parameters, the tool use context, a permission check function, a parent message reference, and an optional progress callback. The returned result carries output data and an optional context modifier. The context modifier (contextModifier) allows a tool to modify the context after execution (e.g., updating the file cache), which is the key channel for tools to influence subsequent behavior. For example, FileWriteTool updates the file state cache through contextModifier after writing a file, so that subsequent FileReadTool invocations can see the latest file contents. **Element 5: UI Rendering** Tools have a rich set of rendering methods that cover the complete UI lifecycle: - `renderToolUseMessage`: Displayed when a tool call starts (e.g., "Reading src/foo.ts") - `renderToolUseProgressMessage`: Progress display during tool execution - `renderToolResultMessage`: Tool result display - `renderToolUseRejectedMessage`: Display when permission is denied - `renderToolUseErrorMessage`: Display when execution encounters an error - `renderGroupedToolUse`: Grouped display for multiple parallel tools Each rendering method returns `React.ReactNode`, enabling deep integration between the tool system and the React rendering pipeline. This design choice means that a tool's UI presentation can be as flexible as React components -- progress bars, color highlighting, collapsible panels, table layouts can all be implemented through React components. The coverage of these six rendering methods is noteworthy: it spans the entire "life cycle" of a tool call -- from start (renderToolUseMessage), to in-progress (renderToolUseProgressMessage), to success (renderToolResultMessage), rejected (renderToolUseRejectedMessage), error (renderToolUseErrorMessage), and grouped display for parallel execution (renderGroupedToolUse). This complete lifecycle coverage ensures that users always see clear, meaningful UI feedback regardless of the state. ### The buildTool Factory Function `buildTool` is the standard factory function for creating tools. It accepts a partial tool definition and automatically fills in safe default values. These defaults follow the "fail-closed" principle: security-related methods (such as concurrency safety determination, read-only determination) default to false, and tools must explicitly declare themselves safe to enjoy optimizations like concurrency. This design philosophy can be understood through an analogy: in airport security, the default assumption is that all luggage needs to be inspected (fail-closed), and only specially certified passengers (such as diplomats) can use the fast track. If it were the other way around -- defaulting to pass-through and only intercepting when problems are found (fail-open) -- then any missed check could cause a security incident. The type system, through clever type computation, allows developers to provide only the necessary fields, while the factory function's return type guarantees a complete tool interface. If a developer provides a particular method in the definition, the type system uses the developer-provided signature; if omitted, the default signature is used. This "optional override, safe default" pattern is highly effective in engineering practice -- simple tools need only a few lines of code, while complex tools can be fully customized. --- ## 3.2 Tool Registration and Dynamic Discovery ### getAllBaseTools() -- The Complete Tool Inventory `getAllBaseTools()` is the registration center for all built-in tools. It returns a flat array containing all available tools in Claude Code. Through this function, we can enumerate the core tool inventory and categorize them by function: | Category | Tool | Responsibility | Concurrency Safe | |----------|------|---------------|-----------------| | Execution | BashTool | Run Shell commands | No (side effects) | | File | FileReadTool, FileEditTool, FileWriteTool | Read, edit, write files | Read: yes, Edit/Write: no | | Search | GlobTool, GrepTool | Filename pattern matching, content search | Yes | | Notebook | NotebookEditTool | Jupyter Notebook editing | No | | Web | WebFetchTool, WebSearchTool | Fetch URL content, web search | Yes | | Agent | AgentTool | Sub-agent entry point | No | | Task | TodoWriteTool, TaskCreateTool, etc. | Task management | Varies by tool | | Planning | EnterPlanModeTool, ExitPlanModeV2Tool | Plan mode switching | No | | Interaction | AskUserQuestionTool | Ask user questions | No (requires user response) | | Skill | SkillTool | Invoke slash command skills | No | | Configuration | ConfigTool | Modify configuration | No | | MCP | ListMcpResourcesTool, ReadMcpResourceTool | MCP resource access | Yes | | Worktree | EnterWorktreeTool, ExitWorktreeTool | Git worktree management | No | | Notification | BriefTool | Message sending | No | | Search Discovery | ToolSearchTool | Deferred tool discovery | Yes | > **Design Insight:** Note the "Concurrency Safe" column -- more than half of the tools are marked as concurrency-unsafe. This reflects a profound engineering reality: in Agent systems, most operations have side effects (modifying files, executing commands, changing state), and operations that can truly be safely executed in parallel (pure reads, pure searches) are the minority. The core challenge of the concurrency partitioning algorithm (Section 3.4) is to maximize parallelism within this constraint. ### Dead Code Elimination in Tool Registration Claude Code's tool registration makes extensive use of conditional imports to achieve compile-time dead code elimination. When specific conditions are not met, the entire module for the corresponding tool is not included in the final build. The same applies to tools controlled by feature flags. Feature flags come from the build toolchain and are evaluated by the bundler at compile time. When a feature flag is off, the corresponding tool implementation code is removed by tree-shaking. This pattern ensures that external builds (for third-party users) do not contain internal tool code. This design has significant security implications: if internal tools (such as REPL tools, debugging tools) were included in external builds, even if they were unavailable, they could leak internal architecture information. Dead code elimination eliminates this information leakage risk at the source. In the tool registration function, conditional registration uses the spread operator, deciding whether to include specific tools based on the runtime environment and feature flags. ### ToolSearchTool Deferred Discovery Mechanism When the number of tools exceeds a certain threshold, Claude Code enables deferred tool discovery. The core idea is: instead of sending the complete schema of all tools in the initial system prompt, send only the tool name list and let the model load detailed schemas on demand through ToolSearchTool. To understand this with an analogy: the traditional approach is like placing the entire encyclopedia in front of the model -- even though most of the content won't be used in the current conversation. Deferred discovery is like giving the model a table of contents index -- the model knows which tools are available and only opens the corresponding page to view detailed parameters when needed. ToolSearchTool's implementation follows the standard factory function pattern. The logic for determining whether a tool should be deferred is: tools explicitly marked as always-loaded are not deferred, MCP tools are always deferred, and the tool search tool itself is not deferred. The core value of this mechanism is saving prompt space: when MCP servers register dozens of tools, sending all of them to the API consumes a large number of tokens. Deferred discovery allows the model to load complete tool schemas only when needed, significantly reducing the initial prompt size. > **Best Practice:** If you are building your own Agent system and connecting external tools through the MCP protocol, pay attention to the token consumption of tool schemas. Each tool's schema includes its name, description, and parameter definitions, which can consume thousands of tokens when the number of tools reaches 50+. Deferred discovery is an effective optimization strategy. ### Tool Filtering Pipeline From `getAllBaseTools()` to the final list of tools sent to the API, multiple layers of filtering are applied: ```mermaid flowchart LR all["getAllBaseTools()<br/>All built-in tools"] --> mode["Mode filtering<br/>Simple/Normal mode screening"] mode --> deny["Deny rule filtering<br/>Remove blanket denied tools"] deny --> enabled["Enabled status check<br/>Filter disabled tools"] enabled --> pool["Tool pool assembly<br/>Merge built-in + MCP<br/>Sort by name, deduplicate"] pool --> api["Tool list sent to API"] classDef filter fill:#e8f4f8,stroke:#2196F3,stroke-width:2px,color:#1565C0 classDef result fill:#f0fdf4,stroke:#22c55e,stroke-width:2px,color:#166534 class all,mode,deny,enabled,pool filter class api result ``` 1. **Mode filtering**: Filters tools based on the mode. Simple mode only retains Bash, Read, and Edit; normal mode excludes special tools. This modularized tool filtering ensures that in constrained environments, the Agent can only use the most basic tool set. 2. **Deny rule filtering**: Removes tools matched by blanket deny rules. 3. **Enabled status check**: Filters out disabled tools. 4. **Tool pool assembly**: Merges built-in tools with MCP tools, sorts by name and deduplicates. The purpose of sorting is to ensure prompt cache stability -- changes in tool order would cause cache invalidation. > **Cross-Reference:** Mode filtering and deny rule filtering are closely related to the permission pipeline in Chapter 4. Tool filtering is the first line of defense in the permission system (tool visibility filtering), ensuring that the model cannot even "see" tools it should not use. --- ## 3.3 Deep Dive into Core Tools ### BashTool: The Swiss Army Knife of Command Execution BashTool is one of Claude Code's most powerful tools, and also the most complex. It is not merely a simple Shell executor, but an execution environment with multiple layers of security protection. If the tool system is the Agent's hands, BashTool is the most powerful among them -- and the one that most needs to be constrained. BashTool's special status in the tool system is reflected in the following aspects: - **Error propagation**: When BashTool execution fails, all parallel Bash tool calls are canceled. This is because Bash commands often have implicit dependency chains (e.g., after `mkdir` fails, subsequent commands are meaningless). This design embodies the "fail fast" principle -- rather than letting subsequent commands continue executing in a corrupted environment and producing more errors, it is better to immediately stop the entire batch. - **Interruption behavior**: BashTool can customize its behavior when the user interrupts. Some long-running commands (such as test suites) may choose to block rather than cancel. This design reflects a nuanced understanding of user intent: interrupting a running `npm install` should stop immediately (the user changed their mind), but interrupting a test suite might just mean the user wants to see current progress (the results are still valuable once tests complete). - **Semantic analysis**: BashTool performs AST parsing and semantic analysis on commands, determining whether a command is a search/read operation (`isSearchOrReadCommand`), used for UI collapsible display. This embodies the "intelligent tool" design philosophy -- tools are not just passive pipelines for executing commands, but can understand command semantics and make corresponding UI decisions. - **Sandbox integration**: Through the `--dangerouslyDisableSandbox` parameter and sandbox configuration, it controls the security boundary of command execution. The sandbox is BashTool's "safety net" -- even in bypass permission mode, the sandbox can still restrict a command's filesystem access scope. ### The File Trio: FileReadTool, FileEditTool, FileWriteTool These three tools constitute Claude Code's complete file operation capability set. Their division of labor reflects the classic CRUD pattern of database operations (Create/Read/Update), except for the absence of Delete -- this is a deliberate safety decision, because "deleting files" is an irreversible operation that is typically accomplished through BashTool's `rm` command, which triggers stricter permission checks. **FileReadTool** is responsible for reading file contents. It maintains a file state cache for tracking which files have been read, avoiding duplicate memory attachment injection. This caching mechanism is key to performance optimization -- if the same file is read multiple times (in different tool call rounds), the cache ensures that actual file I/O is only triggered on the first read, and subsequent reads use cached results directly. **FileEditTool** is responsible for precise file editing. It uses an `old_string -> new_string` exact replacement pattern rather than line number ranges, ensuring that edit operations remain correct even when the file changes. This choice deserves deeper analysis: - **Why not line numbers?** Line numbers are fragile -- if another tool (or the user) modifies the file between reading and editing, the line numbers may have shifted, causing edits to be applied to the wrong location. - **Why exact string matching?** String matching is idempotent -- as long as the target string exists in the file, the edit can be correctly located. Even if the file has been partially modified, as long as the target fragment hasn't been touched, the edit is safe. FileEditTool's `isDestructive` method determines whether an edit is a destructive operation based on the edit content (e.g., deleting a large amount of code). This context-aware destructiveness determination is more precise than a simple "write equals destructive" label. **FileWriteTool** is responsible for creating or completely overwriting files. This is the "heaviest" file operation with the strictest permission checks. The difference between FileWriteTool and FileEditTool lies in scope -- Edit only modifies specific fragments within a file, while Write can completely overwrite file contents. Therefore, Write has higher permission check standards. All three tools support `contextModifier`, which updates the file state cache after execution, enabling subsequent tool calls and memory attachment injection to see the latest file state. > **Best Practice:** When designing your own Agent tools, follow the "least privilege" principle -- prefer Edit over Write, prefer Read over Bash. This is not only a security concern but also an efficiency issue: a precise Edit is faster than writing an entire file and less error-prone. ### The Search Duo: GlobTool and GrepTool **GlobTool** uses filename pattern matching to find files, powered by the `fast-glob` library under the hood. It returns a list of matching file paths, supporting ignore patterns and maximum result count limits. **GrepTool** uses regular expressions to search file contents, powered by `ripgrep` under the hood. It supports multiple output modes (filenames, content lines, counts) and rich filtering options (file types, glob patterns, etc.). The design of these two tools embodies the principle of "specialization over generalization." Although BashTool can achieve similar functionality through `find` and `grep` commands, dedicated search tools have several advantages: 1. **Structured output**: Search tools return structured result lists rather than the text output of Shell commands. The model can parse structured data more accurately. 2. **Permission control**: Search tools are read-only by default, with more lenient permission checks. If every search had to go through BashTool, users would face more permission confirmation prompts. 3. **Performance optimization**: Dedicated search tools can be optimized for specific scenarios (e.g., limiting result counts, parallel searching), while Shell commands have limited optimization room. It is worth noting that when Ant native builds embed dedicated fast search tools, GlobTool and GrepTool are disabled because the `find` and `grep` commands in the Shell have been aliased to these fast tools, and BashTool can use them directly. ### AgentTool: The Sub-Agent Entry Point AgentTool is the core tool for Claude Code's multi-agent collaboration. It allows the primary agent to spawn sub-agents to handle subtasks. Sub-agents have their own independent context windows and tool sets, and return results to the primary agent upon completion. AgentTool has several special properties within the tool system: - It may be marked as `alwaysLoad`, ensuring it remains visible in the first round even when ToolSearch is enabled. This is because sub-agents are a critical capability for handling complex tasks and should not be hidden by the deferred discovery mechanism. - Sub-agents are created through `createSubagentContext` with independent `ToolUseContext` instances, inheriting some state from the parent context (such as permission rules) but having independent message lists. This "inherit but don't share" pattern ensures that sub-agents don't accidentally modify the parent agent's state. - Sub-agent results are exposed to the primary agent through `TaskOutputTool`. > **Cross-Reference:** The complete architectural design of sub-agents will be analyzed in depth in Part 3, Extensions, including context isolation strategies, permission bubbling mechanisms, and result passing protocols. --- ## 3.4 The Tool Orchestration Engine The tool orchestration engine is the "command center" of the tool system -- it determines how multiple tool calls are scheduled, executed, and their results collected. A good orchestration engine must balance three objectives: **parallelism** (execute in parallel as much as possible to improve speed), **safety** (avoid data races caused by concurrent execution), and **ordering** (ensure that results are produced in the same order as requests). ### The runTools() Function and Concurrency Partitioning The core logic of tool orchestration resides in the tool orchestration module. The `runTools` function is an async generator responsible for scheduling the execution of a batch of tool calls. Its scheduling strategy is based on **concurrency partitioning**: 1. First, all tool calls are sequentially divided into batches. 2. Each batch is either a group of consecutive concurrency-safe tools, or a single unsafe tool. 3. Concurrency-safe batches are executed in parallel. 4. Unsafe batches are executed serially. The core logic of the partitioning algorithm: iterate through all tool calls, checking each tool's concurrency safety property. If the current tool is safe and the previous batch is also safe, merge into the same batch; otherwise, start a new batch. This algorithm can be compared to an assembly line: imagine a factory with multiple workstations. Some processes are independent (like simultaneously inspecting the quality of multiple parts) and can proceed in parallel; other processes must strictly follow sequence (like assembly before testing) and cannot be skipped. The concurrency partitioning algorithm is the scheduler that automatically identifies which processes can be parallelized and which must be serialized. For example, if the model requests four tool calls: `[Read(a.ts), Read(b.ts), Bash(ls), Read(c.ts)]`, the partitioning result is: ```mermaid flowchart LR subgraph input["Input Sequence"] direction LR t1["Read(a.ts)<br/>Safe=Yes"] ~~~ t2["Read(b.ts)<br/>Safe=Yes"] ~~~ t3["Bash(ls)<br/>Safe=No"] ~~~ t4["Read(c.ts)<br/>Safe=Yes"] end subgraph b1["Batch 1: Concurrency Safe = true -- Parallel Execution"] direction LR b1a["Read(a.ts)"] ~~~ b1b["Read(b.ts)"] end subgraph b2["Batch 2: Concurrency Safe = false -- Serial Execution"] b2a["Bash(ls)"] end subgraph b3["Batch 3: Concurrency Safe = true -- Parallel Execution"] b3a["Read(c.ts)"] end input --> b1 --> b2 --> b3 classDef safe fill:#f0fdf4,stroke:#22c55e,stroke-width:2px,color:#166534 classDef unsafe fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#991b1b class t1,t2,t4,b1,b1a,b1b,b3,b3a safe class t3,b2,b2a unsafe ``` ```mermaid flowchart LR subgraph timeline["Execution Timeline"] direction LR batch1["Batch 1: Read(a.ts) ‖ Read(b.ts)"] batch2["Batch 2: Bash(ls)"] batch3["Batch 3: Read(c.ts)"] end batch1 --> batch2 --> batch3 note["Note: Batch 3 must wait for Batch 2 to complete<br/>because Bash may have side effects"] classDef batch fill:#f0f7ff,stroke:#3b82f6,stroke-width:2px,color:#1e3a5f class batch1,batch2,batch3 batch ``` The concurrency limit for parallel execution is controlled by an environment variable, defaulting to 10. > **Design Insight:** Why can't Read(c.ts) be placed in the same batch as Bash(ls)? Because Bash commands may have side effects -- they might create new files, modify file contents, or change directory structures. If files are read while Bash is executing, Read might get either the old data from before execution or the new data from after execution, leading to unpredictable behavior. Serial execution ensures that Read(c.ts) sees the deterministic state after Bash(ls) has completed. ### StreamingToolExecutor Streaming Execution `StreamingToolExecutor` is an enhanced version of `runTools` that does not wait for the model's response to fully complete before starting tool execution -- instead, it immediately initiates execution as tool call blocks are received in the stream. The impact of this design is significant. Suppose the model requests five tool calls in one response, and each tool takes 1 second to execute. In the traditional mode, the model takes 2 seconds to generate the complete response (streaming output time), then batch tool execution takes 5 seconds, for a total of 7 seconds. In streaming execution mode, the first tool starts executing about 0.4 seconds after the model begins outputting (the time to generate the first tool_use block), and subsequent tools start one after another, for a total of about 3 seconds -- a speed improvement of over 50%. Each tracked tool has a four-stage state machine: ```mermaid stateDiagram-v2 [*] --> queued : Tool enqueued queued --> executing : Execution conditions met executing --> completed : Execution complete completed --> yielded : Sequential turn to output yielded --> [*] : Lifecycle ends note_right of queued : Waiting for execution conditions note_right of executing : Check concurrency conditions<br/>Start when no tools executing or all are concurrency-safe note_right of completed : Waiting for sequential turn to output note_right of yielded : Result has been yielded ``` - **queued**: The tool has been enqueued, waiting for execution conditions to be met. - **executing**: Currently executing. Before execution, concurrency conditions are checked: execution is allowed to start only when no tools are executing, or all executing tools are concurrency-safe. - **completed**: Execution is complete and results have been collected. But not yet yielded to the upper layer (order must be maintained). - **yielded**: The result has been yielded, and the tool's lifecycle ends. Key design decisions of StreamingToolExecutor: 1. **Order guarantee**: Even though tools can complete in parallel during streaming execution, the yielding of results still maintains the same order as the requests. The result collection function, when traversing the tool list, stops when it encounters an incomplete non-safe tool, ensuring the ordering constraint is not violated. This is a delicate balance between parallelism and consistency -- allowing parallel execution for speed, while guaranteeing ordered result presentation to simplify upper-layer processing logic. 2. **Error propagation**: BashTool execution failure cancels all parallel sibling tools. Non-Bash tool errors do not propagate -- because read/search operations are typically independent. This distinction is important: Bash command failures usually mean something is wrong with the environment (e.g., disk full, network down), and continuing to execute other commands at that point will likely also fail. File read or search operation failures are typically localized (e.g., file doesn't exist, pattern doesn't match) and don't affect other operations. 3. **Immediate progress yielding**: Progress messages during tool execution bypass the ordering constraint and are immediately yielded to the upper layer. This allows the UI to display tool execution progress in real-time without waiting for preceding tools to complete. This design reflects the priority of user experience -- progress messages are "informational" and don't need strict ordering guarantees; result messages are "factual" and must maintain order. 4. **Discard mechanism**: When a streaming fallback occurs (the model switches to a fallback model), all pending and executing tools are marked as discarded, preventing stale results from leaking. This is equivalent to an "emergency brake" -- when the model decides to change strategy, all tool call results based on the old strategy should be discarded. 5. **Signal propagation**: Each tool execution uses an independent sub-cancellation controller, forming a hierarchical cancellation signal chain. Errors from sibling tools or the user's Ctrl+C propagate to the correct tools through signal propagation. This hierarchical signal propagation ensures precision of cancellation operations -- canceling one tool won't accidentally affect unrelated other tools. > **Anti-Pattern Warning:** If you are building your own tool orchestration system, avoid using a single AbortController to manage all tool cancellations. When tool A fails and needs to cancel tool B, it should not simultaneously cancel the completely unrelated tool C. Hierarchical cancellation signals are the correct design. ### Tool State Machine Integration in the Dialog Main Loop Returning to the dialog main loop, tool execution state is tightly integrated with the dialog loop state: 1. **Streaming execution path**: When the streaming tool execution feature is enabled, a StreamingToolExecutor is created. While receiving tool call blocks in the stream, tools are immediately added to the execution queue, and the system checks whether any completed results can be immediately yielded. This implements a "receive-while-executing" pipeline pattern. 2. **Batch execution path**: When streaming execution is unavailable, the traditional batch execution function is used to execute all tools after the model's response has fully completed. This is the fallback for streaming execution, ensuring the system can still work normally when streaming functionality is disabled or encounters errors. 3. **Context propagation**: After tool execution, the context may be modified (e.g., file cache updates). These modifications propagate back to the dialog loop, affecting the execution environment of subsequent tools. Context propagation is key to "consistency" -- if tool A writes to a file but the cache isn't updated, subsequent tool B might make incorrect decisions based on stale cache data. > **Cross-Reference:** The streaming execution path is tightly integrated with phases 3 and 4 of the dialog main loop from Chapter 2. Understanding streaming execution requires grasping it within the context of the overall dialog main loop flowchart. --- ## Practical Exercises **Exercise 1: Implement a Custom Tool** Use the `buildTool` factory function to create a simple tool. Requirements: - Define a Zod schema containing a `path` field (string type) - Implement the `call` method that returns file information for the specified path - Correctly mark `isReadOnly` and `isConcurrencySafe` - Implement `renderToolUseMessage` and `renderToolResultMessage` Compare your implementation with FileReadTool to understand the role of `buildTool` default values. **Discussion Question:** Should your custom tool's `isConcurrencySafe` be marked as true or false? If marked incorrectly (a read-only tool marked as false, or a write tool marked as true), what problems would each case cause? **Exercise 2: Analyze Concurrency Partitioning Strategy** Given the following tool call sequence: ``` [GlobTool(*.ts), GrepTool(pattern), BashTool(npm test), FileReadTool(a.ts), FileEditTool(a.ts), GlobTool(*.json)] ``` Manually execute the logic of `partitionToolCalls`, and draw the batch partitioning result. Then consider: why can't FileEditTool and GlobTool be placed in the same concurrent batch? **Detailed Analysis:** Draw the safety marking for each tool, then step through the partitioning algorithm's decision process. The final answer should be: ``` Batch 1 (Concurrency Safe): [GlobTool(*.ts), GrepTool(pattern)] -- Parallel execution Batch 2 (Not Safe): [BashTool(npm test)] -- Serial execution Batch 3 (Not Safe): [FileReadTool(a.ts)] -- Serial execution (affected by Batch 2, cannot merge into Batch 1) Batch 4 (Not Safe): [FileEditTool(a.ts)] -- Serial execution Batch 5 (Concurrency Safe): [GlobTool(*.json)] -- Can be parallel (but only one tool) ``` **Exercise 3: Trace the StreamingToolExecutor Lifecycle** Set breakpoints at the tool enqueue, tool execution, and result retrieval stages of the StreamingToolExecutor. Send a request that triggers multiple parallel tool calls (e.g., "search for all TODO comments and read the related files"), and observe how tool states transition from queued to executing to completed to yielded, and how progress messages are yielded immediately. **Extended Observation:** During tool execution, press Ctrl+C and observe how the cancellation signal propagates from the user to each tool. Note how the hierarchical cancellation controllers ensure that only the currently executing tools are canceled, while the results of already completed tools are unaffected. **Exercise 4: Evaluate the Performance Impact of Deferred Discovery** If your environment has an MCP server connected, observe the difference in API call token consumption under the following two conditions: - Deferred discovery disabled (all tool schemas in the initial prompt) - Deferred discovery enabled (only tool name list in the initial prompt) Record the input token count difference in both cases, and calculate how many tokens deferred discovery saves you. --- ## Key Takeaways 1. **The five-element protocol is the DNA of the tool system**: name, schema, permissions, execution, and rendering. Each tool defines itself along these five dimensions, and `buildTool`'s default value mechanism lets simple tools focus only on core logic. The design philosophy of this protocol is "explicit declaration, safe defaults" -- tools must proactively declare themselves safe; otherwise, they default to unsafe. 2. **Dead code elimination ensures build security**: Through conditional imports based on environment variables and feature flags, internal tools are prevented from leaking into external builds. This is an important engineering practice for Agent systems in multi-tenant environments. Combined with the "progressive capability extension" principle from Chapter 1, dead code elimination ensures that different product forms can share the same codebase. 3. **Concurrency partitioning is key to performance**: The `isConcurrencySafe` determination decides whether tools can execute in parallel. Correctly marking read-only tools as concurrency-safe allows the Agent to simultaneously execute multiple search/read operations in a single turn, dramatically reducing response time. But the cost of incorrect marking is data races and unpredictable behavior -- this is a design decision that requires careful consideration. 4. **StreamingToolExecutor is a zero-wait tool scheduler**: It starts executing tools while the model is still generating tool_use blocks, and through a four-stage state machine and ordering guarantees, it strikes a balance between parallelism and consistency. It is the perfect embodiment of the "async streaming first" design principle at the tool system level. 5. **Tool system extensibility comes from the type contract**: The generic design of `Tool<Input, Output, Progress>` gives each tool its own type space, while `ToolUseContext` provides a unified execution environment. Adding new tools requires no modifications to the orchestration engine's code -- this is exactly the realization of the "progressive capability extension" principle mentioned in Chapter 1 at the tool level. In the next chapter, we will dive into the Agent's safety guardrails -- the permission pipeline. If the tool system gives the Agent the ability to act, the permission pipeline defines the boundaries of the Agent's actions. Understanding the design of the permission pipeline, you will know how Claude Code finds the precise balance between "autonomous execution" and "safety assurance."

claude-code-book - en Part 2 Core Systems 07 Context Management Ag...

32935 characters

# Chapter 7: Context Management -- The Agent's Working Memory > **Learning Objectives:** > 1. Understand the hard constraints of the Agent's context window and how effective space is calculated > 2. Master the design motivations and working mechanisms of the four-level progressive compression strategy (Snip -> MicroCompact -> Collapse -> AutoCompact) > 3. Understand how the circuit breaker pattern protects the system from cascading failures > 4. Analyze the dual-phase output structure of compression prompt engineering > 5. Be able to select the optimal context management strategy for different usage scenarios --- ## 7.1 Context Window Constraints All of a large language model's reasoning capabilities rest on a single premise: the context window. In every conversation turn, Claude Code must package the complete message history (system prompts, user messages, assistant replies, tool calls and results) and send it to the model. As the conversation progresses, this history inevitably expands until it hits the ceiling of the model's context window. You can think of the context window as a **whiteboard of limited size**. All of the Agent's working memory -- conversation history, tool results, intermediate reasoning -- must be written on this whiteboard. When the whiteboard runs out of space, you must erase old content before writing new content. The key question is: **what to erase, what to keep, and how to erase it**? This is not just a problem for Claude Code; it is a core engineering challenge that all long-conversation Agent systems must solve. Many Agent frameworks opt for "brute-force truncation" on this problem -- simply discarding the oldest messages. Claude Code's approach is far more refined. ### The Effective Window Formula Claude Code uses a precise formula to characterize the space truly available for conversation: ``` Effective Window = Model Window - Reserved Output Tokens ``` In the auto-compaction module, the `getEffectiveContextWindowSize` function implements the calculation of the effective window size: it reserves the lesser of the model's maximum output tokens and 20,000 tokens as reserved space for the compression summary. The remainder is the effective carrying capacity of the context. > **Why reserve 20,000 tokens?** Because AutoCompact (Level 4 compression) needs to call the LLM to generate a summary, and the summary itself consumes output tokens. If no space is reserved, the compression operation itself may fail due to insufficient output space -- a classic "compression paradox." Let's use concrete numbers: suppose the model context window is 200,000 tokens and the maximum output tokens are 16,384: - Reserved space = min(16,384, 20,000) = 16,384 tokens - Effective window = 200,000 - 16,384 = **183,616 tokens** These 183,616 tokens are the space truly available for carrying conversation history. ### Buffers and Thresholds Based on the effective window, Claude Code defines multiple key threshold constants, forming a **progressively tightening safety net**: ```mermaid graph LR subgraph TokenUsage["Token Usage Increasing ->"] safe["Safe Zone<br/>0% - 85%<br/>Normal Operation<br/>No Intervention Needed"] warning["Warning Zone<br/>85% - 90%<br/>WARNING<br/>Alert User"] danger["Danger Zone<br/>90% - 95%<br/>AUTOCOMPACT<br/>Auto-Compression Triggered"] blocked["Blocked Zone<br/>95% - 100%<br/>BLOCKING<br/>Reject New Requests"] end safe --> warning --> danger --> blocked classDef safeZone fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 classDef warnZone fill:#fff9c4,stroke:#f9a825,stroke-width:2px,color:#f57f17 classDef dangerZone fill:#ffe0b2,stroke:#e65100,stroke-width:2px,color:#bf360c classDef blockZone fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c class safe safeZone class warning warnZone class danger dangerZone class blocked blockZone ``` | Constant | Value | Meaning | Design Intent | |----------|-------|---------|---------------| | `AUTOCOMPACT_BUFFER_TOKENS` | 13,000 | Auto-compression trigger buffer | Reserves a safety margin before space is exhausted, avoiding "edge cases" | | `WARNING_THRESHOLD_BUFFER_TOKENS` | 20,000 | Warning threshold buffer | Early warning, giving users time to react | | `ERROR_THRESHOLD_BUFFER_TOKENS` | 20,000 | Error threshold buffer | Marks the danger zone, triggering more aggressive compression strategies | | `MANUAL_COMPACT_BUFFER_TOKENS` | 3,000 | Manual compression buffer | Minimum safety margin when users manually trigger compression | > **Best Practice:** When you see token usage exceed 80%, you should consider manually triggering compression (by typing `/compact`) rather than waiting for auto-compression to trigger. Proactive compression typically preserves more valuable content because you can provide advance guidance on which information is most important. ### Circuit Breaker Design Auto-compression does not always succeed. Network fluctuations, API errors, or structural issues in the context itself can all cause compression to fail. If the system retries blindly, it will make doomed API calls on every turn, wasting significant resources. Claude Code introduces a Circuit Breaker mechanism: when the number of consecutive failures reaches 3 (`MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES`), the system directly skips subsequent compression attempts. ```mermaid stateDiagram-v2 [*] --> CLOSED: System Startup CLOSED --> CLOSED: Compression Succeeds<br/>(Counter Reset to 0) CLOSED --> HALF_OPEN: Compression Fails<br/>(Counter Increments) HALF_OPEN --> CLOSED: Compression Succeeds<br/>(Counter Reset to 0) HALF_OPEN --> OPEN: Consecutive Failures >= 3<br/>(Circuit Breaks, No More Attempts) OPEN --> OPEN: Skip Compression OPEN --> CLOSED: New Session / Manual Compression Succeeds<br/>(Reset Condition) note right of CLOSED Normal Operating State Counter = 0 end note note right of OPEN Circuit Broken State No More Auto-Compression Attempts end note ``` On success, the failure counter resets to zero; on failure, the counter increments and is propagated to the upper-level caller. This is a classic circuit breaker pattern -- once consecutive failures reach the threshold, the circuit breaks, preventing avalanche effects. **Real-world data from the circuit breaker:** According to engineering analysis, before the circuit breaker was introduced, 1,279 sessions were observed with over 50 consecutive compression failures (up to 3,272), wasting approximately 250K API calls per day. After its introduction, such cascading failures were completely eliminated. > **Anti-Pattern Warning:** If you are building your own Agent system, do not ignore circuit breakers. A system without circuit breakers will fall into a "compression failure -> retry -> fail again" death spiral when the API is unstable, wasting resources and potentially further degrading the user experience due to increased latency. > **Cross-Reference:** The circuit breaker pattern is also discussed as a key design pattern in Chapter 15 on building your own Agent Harness. Similar state protection mechanisms also appear in the conversation loop in Chapter 2 (the `max_output_tokens_recovery` path). --- ## 7.2 The Four-Level Compression Strategy Claude Code's context management employs a four-level progressive compression strategy, escalating from low cost to high cost. Each level is activated only when the previous level is insufficient to free up space. This design philosophy can be understood through an analogy: **compression strategies are like organizing clothing storage**. You don't start by throwing away all your clothes (brute-force truncation); instead, you first put away clothes you no longer wear (Snip), then compress seasonal clothing (MicroCompact), then vacuum-seal bulky items (Collapse), and only finally do a comprehensive sort-and-discard (AutoCompact). ```mermaid graph LR subgraph CompressionStrategy["Four-Level Compression Strategy (Increasing Cost ->)"] direction LR L1["Level 1: Snip<br/>───────<br/>No LLM<br/>Token Clearance<br/>Cost ~ 0"] L2["Level 2: MicroCompact<br/>───────<br/>No LLM<br/>Time-Triggered<br/>Very Low Cost"] L3["Level 3: Collapse<br/>───────<br/>Partial LLM<br/>Active Restructuring<br/>Medium Cost"] L4["Level 4: AutoCompact<br/>───────<br/>Full LLM<br/>Conversation Summary<br/>High Cost"] end L1 -->|"Insufficient Space"| L2 L2 -->|"Insufficient Space"| L3 L3 -->|"Insufficient Space"| L4 L1 -.- t1["Manual<br/>Targeted Clearance"] L2 -.- t2["Auto-Triggered<br/>Cache Expiry"] L3 -.- t3["Auto-Triggered<br/>Space Pressure"] L4 -.- t4["Auto-Triggered<br/>Final Fallback"] classDef level1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20 classDef level2 fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,color:#0d47a1 classDef level3 fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 classDef level4 fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#b71c1c classDef trigger fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,color:#616161 class L1 level1 class L2 level2 class L3 level3 class L4 level4 class t1,t2,t3,t4 trigger ``` ### Level 1: Snip Snip is the lightest-weight compression method. It does not invoke any LLM; instead, it directly clears old tool result content. When a user marks messages as no longer needed through the Snip tool, the system replaces the tool call results with a brief marker text (e.g., `[Old tool result content cleared]`), thereby freeing token space. In the micro-compaction module, you can see the definition of this marker text: `'[Old tool result content cleared]'`. Snip operations record the number of tokens freed and pass this information to the auto-compression decision function, enabling a more accurate assessment of whether higher-level compression needs to be triggered. **The design wisdom of Snip:** Why replace messages with marker text instead of deleting them outright? Because deleting messages breaks the continuity of the message chain -- subsequent messages may reference earlier tool call IDs. The marker text both frees space and maintains message structural integrity. **Typical usage scenario:** You just used the Read tool to read 10 files, each with 500 lines of code, consuming approximately 15,000 tokens. Once the analysis is complete, these file contents are no longer needed. At this point, using Snip to clear these tool results immediately reclaims a large amount of space. ### Level 2: MicroCompact MicroCompact is a time-triggered, large-scale tool result cleanup. When the system detects that the time interval since the last assistant message exceeds a configured threshold, it means the server-side cache has expired. At that point, regardless of how important the content is, a full rewrite is unavoidable -- so it is better to proactively clear old tool results before the request, reducing the rewrite payload. > **Why is this related to cache expiry?** Claude's API supports Prompt Caching -- if consecutive requests share the same prefix, cached portions can significantly reduce cost and latency. However, over time, caches expire. When a cache expires, the full content must be resent regardless. At that point, keeping old tool results only adds unnecessary payload. The core logic for time-based triggering resides in the time evaluation function: it checks whether the feature flag is enabled, whether the message source is the main thread, and then calculates the time interval since the last assistant message. If the interval exceeds the configured threshold, micro-compaction is triggered. Once triggered, the system retains the most recent N compressible tool results (`config.keepRecent`, minimum value of 1) and replaces all other tool result content with the clearance marker text. Compressible tool types include Read, Bash, Grep, Glob, WebSearch, WebFetch, Edit, and Write. Additionally, MicroCompact has a cache editing-based path that uses the `cache_edits` mechanism at the API layer to delete tool results without breaking the cache prefix -- a more advanced lossless optimization. **Core trade-offs of MicroCompact:** | Dimension | Description | |-----------|-------------| | **Trigger Condition** | Time since last assistant message exceeds threshold | | **Retention Policy** | Keep the most recent N tool results, clear the rest | | **Cost** | Zero LLM calls, string replacement only | | **Information Loss** | Old tool result content is lost, but message structure is preserved | | **Applicable Scenario** | "Natural breakpoints" in long conversations (user returns after a pause) | ### Level 3: Collapse Collapse is context restructuring-level compression. When the Context Collapse feature is enabled, the system begins committing (commit) compression operations at 90% context utilization and blocks new spawns at 95%. The design philosophy of this level is to shift context management from "reactive compression" to "proactive restructuring." Collapse mode suppresses the triggering of auto-compression because the two would compete at the 93% critical point. Collapse, as a more refined context management system, has higher priority. > **Design Philosophy:** Collapse represents a different mindset -- not "compress when space runs out," but "proactively restructure before space pressure appears." This is similar to an operating system's memory prefetching strategy, which starts defragmenting before memory is exhausted. **Key differences between Collapse and AutoCompact:** | Feature | Collapse | AutoCompact | |---------|----------|-------------| | Trigger Timing | 90% utilization (proactive) | Exceeds threshold (reactive) | | Compression Granularity | Selectively restructures message groups | Full conversation summary | | Information Retention | More original details preserved | Only summary retained | | Relationship with Fork | Blocks new spawns (95%) | Does not affect spawns | ### Level 4: AutoCompact AutoCompact is the most thorough compression level -- it invokes the LLM to summarize the complete conversation. When the above three levels cannot effectively free space and token usage exceeds the auto-compression threshold, the system initiates AutoCompact. This is the final fallback and also the most "expensive" -- it requires an additional API call to generate the summary. The compression process is driven by the `compactConversation` function, whose core steps are: ```mermaid flowchart TD start["AutoCompact Triggered"] --> step1["1. Execute PreCompact Hook"] step1 --> step2["2. Build Compression Prompt<br/>Select Template BASE / PARTIAL / PARTIAL_UP_TO"] step2 --> step3["3. Stream Summary Generation<br/>Via forked agent or main thread API call"] step3 --> step4{"4. prompt-too-long?"} step4 -->|Yes| retry["Truncate Oldest API Turn Group<br/>and Retry (Max 3 Times)"] retry --> step3 step4 -->|No| step5["5. Rebuild Context<br/>CompactBoundaryMessage + Summary + Attachments + Hook Results"] step5 --> step6["6. Execute PostCompact Hook"] step6 --> result["Output: CompactionResult"] classDef step fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1 classDef decision fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 classDef output fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 class step1,step2,step3,step5,step6 step class step4 decision class result output ``` The `CompactionResult` interface describes the complete structure of the compression output, including: boundary marker (boundaryMarker), summary messages (summaryMessages), re-injected attachments (attachments), hook results (hookResults), messages retained during partial compression (messagesToKeep), and token counts before and after compression. Notably, the `buildPostCompactMessages` function ensures consistent output message ordering across all compression paths: boundary marker, summary messages, retained messages, attachments, hook results. > **Cross-Reference:** AutoCompact's forked agent execution method is closely related to the Fork pattern discussed in Chapter 9. The compression operation executes in a restricted sub-Agent that runs for at most 1 turn (generating only a summary, no tool calls), ensuring compression does not produce side effects. > **Cross-Reference:** PreCompact and PostCompact hooks are important application scenarios of the hook system covered in Chapter 8. Users can inject custom compression instructions through PreCompact hooks (e.g., "specifically preserve all discussions related to the database"). --- ## 7.3 Compression Prompt Engineering The quality of compression directly depends on prompt design. Claude Code's compression prompt engineering is a carefully designed system with multiple variants and strict output format constraints. You can think of compression prompts as instructions to a stenographer: you need to clearly tell them "what to record, what not to record, and what format to use." If the instructions are not precise enough, the summary will either lose critical information or be stuffed with unnecessary details. ### Compression-Specific Prompt Templates The compression prompt module defines three compression prompt templates, corresponding to three different compression scenarios: ```mermaid graph TD subgraph CompressionTemplates["Three Compression Templates"] BASE["BASE_COMPACT_PROMPT<br/>───────<br/>Scenario: Full Conversation Summary<br/>Scope: All Messages from Start to Current<br/>Use Case: Regular Auto-Compression"] PARTIAL["PARTIAL_COMPACT_PROMPT<br/>(from direction)<br/>───────<br/>Scenario: Summarize Only Recent Messages<br/>Scope: From Specified Message to Current<br/>Use Case: First Half Already Compressed"] PARTIAL_UP["PARTIAL_COMPACT_UP_TO_PROMPT<br/>(up_to direction)<br/>───────<br/>Scenario: Summarize Context Before Specified Message<br/>Scope: From Start to Specified Message<br/>Use Case: Preserve Recent Complete Messages"] end classDef base fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1 classDef partial fill:#e8f5e9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 classDef upto fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 class BASE base class PARTIAL partial class PARTIAL_UP upto ``` Each prompt includes a critical anti-tool-call preamble: it instructs the model to respond only in text form and not to invoke any tools (including Read, Bash, Grep, etc.). This directive ensures the summary generation process does not trigger tool calls, because compression runs in a restricted forked agent environment (maximum 1 turn), and a rejected tool call would directly result in empty output. > **Design Philosophy:** Why must compression execute in a restricted environment? Because compression is a "rewriting" operation on conversation history -- if new conversation history is generated during the rewriting process, it creates a recursion problem. Restricting it to a single turn with no tool calls ensures compression is a pure "read-summarize-output" process. ### Dual-Phase Output Structure The compression prompt requires the model to output two XML blocks: - `<analysis>` block: A thinking scratchpad used to organize thoughts and ensure comprehensive coverage. This block is discarded in the final result. - `<summary>` block: The formal summary content, containing a structured set of 9 sections. The `formatCompactSummary` function handles post-processing: it first discards the `<analysis>` block (thinking scratchpad), then extracts the content of the `<summary>` block as the formal summary. This design pattern is noteworthy: the `<analysis>` block serves as a Chain-of-Thought carrier that improves summary quality, but it does not enter the final context window, avoiding token waste. ```mermaid flowchart TD llm["LLM Raw Output"] --> analysis["&lt;analysis&gt;<br/>Thinking Process: Analyze Conversation Structure,<br/>Identify Key Decisions..."] llm --> summary["&lt;summary&gt;<br/>## Goals and Intent<br/>## Key Decisions and Changes<br/>## Unresolved Issues<br/>## File Change Summary<br/>...Total 9 Sections"] analysis --> discard["Discard analysis<br/>Save Tokens"] summary --> keep["Keep summary<br/>Enter New Context"] classDef raw fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c classDef discard_node fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c classDef keep_node fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 class llm raw class analysis,discard discard_node class summary,keep keep_node ``` **Why is the dual-phase design so important?** If you directly ask the model to output a summary (without an analysis phase), the model will often miss important information -- because it lacks "thinking" space. But if you keep the analysis in the context, you waste valuable tokens. The dual-phase design perfectly resolves this contradiction: **thinking is the process, the summary is the result**. The process is not counted; only the result enters the context. > **Best Practice:** If you need to customize compression behavior in a PreCompact hook, you can adjust the priority of the 9 sections within `<summary>`. For example, if your work focuses on API design, you can inject an instruction in the hook: "In the summary, specifically preserve all API endpoint definitions and their rationale for changes." ### CompactBoundaryMessage After each compression is completed, the system inserts a `CompactBoundaryMessage` into the message stream as a dividing line between pre-compression and post-compression. This marker carries compression metadata: trigger type (manual/automatic), pre-compression token count, and number of messages involved in the compression. The `logicalParentUuid` field associates the boundary marker with the last message before compression, constructing logical continuity of the message chain. The presence of the boundary marker enables subsequent compression operations to accurately identify "which messages have already been compressed," avoiding redundant compression of already-summarized content. > **Cross-Reference:** `CompactBoundaryMessage` is directly related to the message chain mechanism in the conversation loop from Chapter 2. When building API requests, the conversation loop needs to correctly handle boundary markers -- messages before the boundary marker have been replaced by summaries and should not be sent again. --- ## 7.4 Token Budget Tracking Token management is not just about reactive compression triggering; it also includes proactive budget planning and early warning systems. ### Multi-Level Warning States The `calculateTokenWarningState` function calculates the current token usage state and returns multiple boolean flags: | Flag | Trigger Condition | UI Behavior | |------|-------------------|-------------| | `isAboveWarningThreshold` | Token usage >= threshold - 20,000 | Display yellow warning | | `isAboveErrorThreshold` | Token usage >= threshold - 20,000 | Display red warning | | `isAboveAutoCompactThreshold` | Token usage >= auto-compression threshold | Trigger auto-compression | | `isAtBlockingLimit` | Token usage >= effective window - 3,000 | Block new requests | These flags drive warning display at the UI level and trigger compression behavior. The `percentLeft` field shows the user the percentage of remaining space. ### Post-Compression Token Budget After compression is complete, the system does not simply release all space. The compression module defines strict token budget constants: | Constant | Value | Purpose | |----------|-------|---------| | `POST_COMPACT_MAX_FILES_TO_RESTORE` | 5 | Maximum number of files to restore | | `POST_COMPACT_TOKEN_BUDGET` | 50,000 | Total token budget cap | | `POST_COMPACT_MAX_TOKENS_PER_FILE` | 5,000 | Token cap per file | | `POST_COMPACT_MAX_TOKENS_PER_SKILL` | 5,000 | Token cap per skill | | `POST_COMPACT_SKILLS_TOKEN_BUDGET` | 25,000 | Independent skill budget | These budgets limit the amount of content re-injected into the context after compression, ensuring that compression does not immediately trigger another compression due to excessive attachment injection. > **Anti-Pattern Warning:** A common mistake is to immediately reload all previously read files after compression. Doing so rapidly depletes the token budget, causing compression to trigger again after just a few conversation turns, creating a vicious "compress-expand-recompress" cycle. The correct approach is to only reload files needed for the current task. ### True Token Estimation `truePostCompactTokenCount` is an estimate of the actual post-compression context size, including the sum of tokens for the boundary marker, summary messages, attachments, and hook results. This value is used to determine whether compression would immediately trigger another compression in the next turn, providing critical diagnostic information for telemetry. If the post-compression token count still exceeds the auto-compression threshold, the system knows the compression "was done in vain" -- this situation typically occurs when the conversation structure is extremely complex or the summary itself is too long. --- ## 7.5 Context Management Strategies for Long Conversations Having understood the compression mechanisms, let's look at how to optimize context management in practice. ### Strategy 1: Proactive Compression Over Reactive Compression ```mermaid flowchart LR subgraph ReactiveCompression["Reactive Compression (Not Recommended)"] direction TB u1["User"] --> a1["Agent Conversation"] a1 -->|"95%"| ac1["Auto-Compression"] ac1 --> r1["Summary<br/>(May Lose Important Details)"] end subgraph ProactiveCompression["Proactive Compression (Recommended)"] direction TB u2["User"] --> a2["Agent Conversation"] a2 -->|"70%"| compact["User Types /compact<br/>(With Key Points to Preserve)"] compact --> r2["Summary<br/>(Preserves User-Specified Key Points)"] end classDef bad fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c classDef good fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 classDef neutral fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,color:#424242 class ReactiveCompression bad class ProactiveCompression good ``` When you sense that a conversation is becoming lengthy during extended work, proactively type `/compact` with a hint (e.g., "/compact preserve all database schema related content") to make the compression more targeted. ### Strategy 2: Phased Work For large projects, divide work into multiple phases: 1. **Research Phase**: Read files, understand code structure -> compress when complete 2. **Planning Phase**: Formulate plans based on the summary -> compress when complete 3. **Implementation Phase**: Execute modifications based on the plan -> compress when complete Compression at the end of each phase ensures ample context space for the next phase. ### Strategy 3: Leverage the Memory System to Supplement Context > **Cross-Reference:** The memory system from Chapter 6 is an important supplement to context management. Compression loses conversation details, but if key information has already been saved as memory files, the Agent can still recover critical context by reading memories after compression. This means you should develop the habit of having the Agent save memories when important decisions are made -- so that even if the conversation is compressed, critical information is not lost. ### Context Strategies for Multi-File Projects When working with large projects, context management is especially critical: | Scenario | Recommended Strategy | |----------|---------------------| | After reading 10+ files | Use Snip to clear analyzed file contents | | Returning after a long pause | MicroCompact automatically clears expired cache | | Implementing multiple features consecutively | Manually compress after completing each feature | | Refactoring across multiple subsystems | Phased work + memory system support | --- ## Practical Exercises **Exercise 1: Token Budget Calculator** Assume your model context window is 200,000 tokens and the maximum output tokens are 16,384. Please calculate: - Effective context window size - Auto-compression trigger threshold - Warning threshold - Blocking limit > *Advanced Challenge:* If the conversation includes a system prompt that consumes 50,000 tokens, how much effective conversation space do you have left? What impact does this have on the choice of compression strategy? **Exercise 2: Design a Custom Compression Strategy** Design the most appropriate compression level combination for the following scenarios: - Scenario A: Code review session, user works continuously for 2 hours, contains extensive file read results - Scenario B: Automated CI/CD Agent, long-running task pipeline - Scenario C: Interactive teaching session, needs to maintain precise quotations from early conversation > *Advanced Challenge:* Design a PreCompact hook instruction for each scenario, guiding the summary on which key information to preserve. **Exercise 3: Circuit Breaker Behavior Analysis** Trace the circuit breaker state changes through the following event sequence: 1. Compression succeeds (consecutiveFailures = ?) 2. Compression fails (consecutiveFailures = ?) 3. Compression fails (consecutiveFailures = ?) 4. Compression fails (consecutiveFailures = ?) 5. Will compression be attempted in the next turn? > *Advanced Challenge:* If the circuit breaker threshold is changed from 3 to 5, with an API failure rate of 30%, how many additional API calls would be wasted per day? (Hint: refer to the data from 1,279 sessions in the text) **Exercise 4: Context Compression in Practice** Start a long conversation session using Claude Code: 1. Have the Agent read 8-10 files consecutively 2. Observe changes in token usage 3. When usage reaches 60%, manually type `/compact` with the key points you want to preserve 4. Compare token counts before and after compression > *Advanced Challenge:* Try manually clearing unnecessary tool results with the Snip tool before compression. Compare the difference between "Snip first, then compact" versus "compact directly." **Exercise 5: Cross-Chapter Comprehensive Analysis** Combining Chapter 2 (Conversation Loop) and Chapter 9 (Fork Pattern), analyze the following questions: - In the conversation loop's preprocessing pipeline, at which step does context compression execute? Why at that position? - When a Fork creates a sub-Agent, if the parent Agent's context has already been compressed, what does the sub-Agent inherit? What impact does this have on the sub-Agent's behavior? --- ## Key Takeaways 1. **Effective Window = Model Window - Reserved Output Tokens**: Claude Code reserves up to 20,000 tokens for compression summary output space, ensuring the compression operation itself does not fail due to insufficient space. 2. **Four-Level Progressive Compression**: Snip -> MicroCompact -> Collapse -> AutoCompact, with costs escalating at each level. Each level is an "upgrade" of the previous one. 3. **Circuit Breaker Protection**: After 3 consecutive compression failures, further attempts are stopped, preventing avalanche effects from wasted API calls. This design stems from actual data analysis of 1,279 sessions. 4. **Dual-Phase Prompt Structure**: `<analysis>` thinking scratchpad + `<summary>` formal summary. The former is discarded in the final context to save tokens -- "thinking is the process, the summary is the result." 5. **CompactBoundaryMessage**: The compression boundary marker carries metadata and maintains logical continuity of the message chain through `logicalParentUuid`, enabling subsequent operations to accurately identify already-compressed content. 6. **Post-Compression Budget Control**: Re-injected content has strict token budget limits (50,000 total budget, 5,000 per file), preventing immediate re-triggering of compression. 7. **Time-Triggered Micro-Compression**: When server-side cache expires (timeout since last assistant message), old tool results are proactively cleared to reduce rewrite costs. 8. **Proactive Compression Over Reactive Compression**: Manually triggering compression at key work milestones with annotation of key points preserves more valuable information than waiting for auto-compression. 9. **Memory System as a Context Supplement**: Important decisions should be saved as memory files, so that even if the conversation is compressed, critical information is not lost. See Chapter 6 for details.

claude-code-book - en Part 2 Core Systems 06 The Memory System Age...

43150 characters

# Chapter 6: The Memory System -- Agent Long-Term Memory > **Learning Objectives:** Master the design intent and automatic extraction mechanism of four memory types, understand the cache-aware architecture based on the Fork pattern, and learn how to design a persistent Agent memory system. Through this chapter, you will understand how to leverage the memory system to make the Agent increasingly understand you with use, and how to manage the lifecycle of memories in a multi-project environment. --- Humans can maintain coherence across multiple conversations because we have memory. Similarly, a truly useful Agent cannot start from scratch every conversation -- it needs to remember who the user is, what the project is doing, and which practices have been validated. Claude Code's memory system (memdir) was built for exactly this purpose: a file-based, typed, cross-session persistent memory architecture. Comparing the memory system to "long-term memory" is a biologically precise analogy. Human memory is divided into sensory memory (milliseconds), working memory (seconds, corresponding to the context management in Chapter 7), and long-term memory (minutes to years, corresponding to the memory system in this chapter). Claude Code's design follows this same layering: the context window is "working memory," temporarily holding information within a single session; while memdir is "long-term memory," persistently storing non-derivable critical knowledge across sessions. ## 6.1 Taxonomy of the Four Memory Types ### 6.1.1 Closed Type System Claude Code's memory is constrained to a closed four-type classification system, defined in memory type constants: user, feedback, project, and reference. The design philosophy of these four types is: **only save information that cannot be derived from the current project state.** Code patterns, architecture, file structure, and Git history can all be obtained in real time through tools (grep, git log), and therefore do not fall within the scope of memory. **Why must it be a closed system?** An open type system (allowing arbitrary custom types) appears more flexible but has fatal flaws in the Agent scenario: (1) Type explosion -- different users and projects might create dozens of types, making it impossible for the Agent to efficiently determine which memories are relevant to the current conversation when reading; (2) Classification ambiguity -- the same piece of information might belong to multiple custom types, leading to duplicate storage; (3) Index bloat -- the MEMORY.md index would need to maintain classification logic for each type, adding unnecessary complexity. The closed four-type design embodies a "constraint is freedom" philosophy: constraining the classification method yields efficient consistent reasoning and precise relevance judgment. ### 6.1.2 Detailed Analysis of the Four Types The relationship between the four memory types can be understood through a two-dimensional matrix: ```mermaid graph LR subgraph MemoryTypeMatrix["Memory Type 2D Matrix"] direction TB label_row1["Subjective/Directive"] label_row2["Objective/Factual"] label_col1["Personal Dimension<br/>(About the Person)"] label_col2["Project Dimension<br/>(About the Work)"] user["user<br/>User Profile<br/>Who is using?"] feedback["feedback<br/>Feedback Directives<br/>What practices are validated?"] empty_cell["(Usually not needed)"] project["project<br/>Project State<br/>Why is it done this way?"] reference["reference<br/>External References<br/>Where to find more info?"] end label_row1 --- user label_row1 --- feedback label_row2 --- empty_cell label_row2 --- project label_row2 --- reference label_col1 --> user label_col1 --> empty_cell label_col2 --> feedback label_col2 --> project label_col2 --> reference classDef subjective fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px,color:#1a237e classDef objective fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20 classDef dim fill:#f5f5f5,stroke:#bdbdbd,stroke-width:1px,color:#757575 class user,feedback subjective class project,reference objective class empty_cell,dim dim ``` > **user** -- User Profile Stores the user's role, goals, and knowledge background. Helps the Agent adjust its collaboration style for users of different expertise levels -- it should communicate differently with a senior engineer versus a beginner. ``` when_to_save: When learning about the user's role, preferences, or knowledge background how_to_use: When needing to adjust explanation depth and collaboration style based on the user profile ``` Example: When a user says "I've been writing Go for ten years, but this is my first time touching React," the Agent saves a user-type memory and uses backend analogies when explaining frontend concepts in the future. **Practical Application Scenarios:** Scenario 1: Cross-project user preferences. After a user expresses preferences in project A, the Agent can apply the same preferences in project B. Because user-type memories are stored in the user's global directory, they naturally support cross-project sharing. Scenario 2: Progressive understanding of the user. In the first conversation, the user mentions being a data scientist, and the Agent records this as a user memory. In the fifth conversation, the user demonstrates advanced Python skills, and the Agent updates the memory to add "proficient in Python, familiar with pandas/numpy." This progressive user profile building enhances the Agent's collaboration capabilities over time. **feedback -- Feedback Directives** Records the user's corrections and confirmations regarding Agent behavior. This is one of the most important memory types -- it enables the Agent to maintain behavioral consistency across future conversations. ``` when_to_save: When the user corrects your approach ("don't do that") or confirms a non-obvious successful practice body_structure: The rule itself + Why: reason + How to apply: applicable scenarios ``` Key design: It records not only failures (corrections) but also successes (confirmations). If only corrections are saved, the Agent becomes overly cautious, deviating from validated methods. **Practical Application Scenarios:** Scenario 1: Code style preferences. The user says "don't use var, use const and let for everything," and the Agent saves this as a feedback memory. In all subsequent conversations, the Agent's generated code defaults to using const/let. Scenario 2: Process requirements. The user says "lint must be run before committing code," and the Agent saves this as a feedback memory. Before every subsequent git commit execution, the Agent automatically runs the lint command. Scenario 3: Lessons from failure. The user says "last time you directly modified package.json which caused version conflicts; from now on, check with me before changing dependencies," and the Agent saves this as a feedback memory, proactively requesting confirmation when modifying dependency files in the future. **project -- Project State** Records the non-code state of a project -- decisions, deadlines, work in progress. Code and Git history are derivable, but information like "why it was done this way" and "when it needs to be completed" is not. ``` when_to_save: When learning about who is doing what, why, and when it will be completed body_structure: Fact or decision + Why: motivation + How to apply: impact on recommendations ``` Special attention: Relative dates must be converted to absolute dates ("Thursday" -> "2026-03-05"), because memories persist across sessions, and relative dates lose their meaning in future conversations. **Practical Application Scenarios:** Scenario 1: Architecture Decision Records (ADR). The user says "the authentication module uses JWT instead of Session because it needs to support mobile clients," and the Agent saves this as a project memory. When authentication-related code needs modification in the future, the Agent can understand the background of this decision. Scenario 2: Work in progress. The user says "I'm migrating the user module from REST to GraphQL; I've completed the query part and need to work on the mutation part next," and the Agent saves this as a project memory. In the next conversation, the Agent can continue working from the correct context. Scenario 3: Team conventions. The user says "our team agreed that all API responses use camelCase, but database fields use snake_case," and the Agent saves this as a project memory, following this convention when generating code. **reference -- External References** Pointers to external systems -- Linear projects, Grafana dashboards, Slack channels. This information is not in the code repository but is critical for understanding project context. ``` when_to_save: When learning about external system resources and their purposes how_to_use: When the user references external systems or needs to look up external information ``` **Practical Application Scenarios:** Scenario 1: Monitoring dashboards. The user says: "the production Grafana dashboard is at [https://grafana.company.com/d/abc123](https://grafana.company.com/d/abc123)." The Agent saves this as a reference memory. When the user asks, "any anomalies recently," the Agent can remind the user to check this dashboard. Scenario 2: Documentation links. The user says: "the API docs are on Confluence at [https://confluence.company.com/pages/api-docs](https://confluence.company.com/pages/api-docs)." The Agent saves this as a reference memory. Scenario 3: Communication channels. The user says "backend team discussions are in the #backend-dev Slack channel," and the Agent saves this as a reference memory, reminding the user when cross-team coordination is needed. ### 6.1.3 Explicitly Excluded Information The memory type validation module explicitly lists content that should not be saved as memory: - Code patterns, conventions, architecture, file paths -- derivable by reading code - Git history -- `git log` / `git blame` are authoritative sources - Debugging solutions -- the fix is already in the code, the context is in the commit message - Documentation already in CLAUDE.md - Temporary task details -- transient state of the current conversation Even when a user **explicitly requests** saving such information, the system guides toward a more valuable direction: "If you want to save a list of PRs, tell me what's **surprising** or **non-obvious** about them -- that's what's worth saving." **The Deeper Logic of This Exclusion Principle** Many users, when first using the memory system, try to have the Agent memorize "the project's file structure" or "the API route list." This instinct is understandable -- humans确实 need to understand this information when taking over a new project. But there is a key difference between Agents and humans: Agents can read the file system in real time during every conversation. ``` Information Acquisition Cost Comparison: Human Developer: Memorize file structure -> hours of reading and understanding Recall when needed next time -> seconds (if remembered) -> Value of memory = time saved from re-reading Agent: Read file structure in real time -> milliseconds for a tool call Cost of re-acquiring each time -> a few hundred tokens -> Value of memory ≈ 0 (because real-time acquisition cost is minimal) ``` Therefore, the memory system should focus on saving information that "cannot be acquired in real time" -- people's preferences, the rationale behind decisions, external links. The common characteristic of this information is that it exists in people's minds or external systems and cannot be obtained by reading the code repository. ### 6.1.4 Best Practices for Memory Usage **Memories That Should Be Saved (Positive Examples):** | Scenario | Memory Type | Content to Save | |----------|-------------|-----------------| | User expresses preference | feedback | "User prefers Vitest over Jest" + Why: faster test execution | | User corrects behavior | feedback | "Don't modify files in the generated folder" + Why: they are auto-generated by protoc | | Architecture decision | project | "Use event-driven architecture instead of direct calls" + Why: need for service decoupling | | External system link | reference | "Monitoring alerts are in PagerDuty's X service" | | User background | user | "User is a full-stack developer, proficient in TypeScript and Python" | **Memories That Should NOT Be Saved (Negative Examples):** | Scenario | Why Not to Save | Correct Approach | |----------|----------------|------------------| | Project file listing | Can be obtained in real time via `ls` | No memory needed | | API endpoint list | Can be obtained by reading route code | If there are non-obvious design decisions, save only the decisions | | Bug fix steps | Already recorded in commit messages | If the fix involves counter-intuitive reasons, save the "why" | | Third-party library versions | Can be obtained by reading package.json | If there are special reasons for the selection, save the reasons | ### 6.1.5 Common Misconceptions in Memory Management **Misconception 1: More Memories Is Better** This is the most common misconception. Some users have the Agent memorize every detail from conversations, leading to MEMORY.md index bloat and a large accumulation of low-value files in the memory directory. Too many memories not only increase the context burden for each conversation but may also cause the Agent to be distracted by "noise" and overlook truly important memories. **Correct approach:** Regularly review the memory directory and delete outdated or low-value memories. A good memory should pass the test of "if this memory were deleted, would the Agent's behavior be substantively different?" **Misconception 2: Treating Memory as a Documentation System** Some users try to use the memory system as a replacement for project documentation, having the Agent memorize all technical specifications and design documents. This violates the principle of "only save non-derivable information" -- technical specifications should be placed in the code repository's documentation directory, not in the memory system. **Correct approach:** Place technical documentation in the `docs/` directory, and the "why" behind architectural decisions in memory. **Misconception 3: Ignoring the Relative Date Problem** The user says "this feature launches next Tuesday," and the Agent saves "launches next Tuesday." But two days later in the next conversation, "next Tuesday" has become "this Tuesday," and a week later it becomes "last Tuesday." This memory is not only useless but potentially misleading. **Correct approach:** All time-related memories must use absolute dates. The Agent should convert "next Tuesday" to a specific date (e.g., 2026-04-07) before saving. > **Cross-Reference:** The exclusion principle for memories shares the same philosophy as the compression strategy in Chapter 7 (Context Management) -- only retain information that cannot be re-acquired. Context compression clears old tool results (which can be re-acquired by re-executing tools), and the memory system excludes code patterns (which can be re-acquired by re-reading code). ## 6.2 Memory File Format ### 6.2.1 Frontmatter Format Each memory is an independent Markdown file that uses YAML Frontmatter to declare metadata. The format requires three fields: name (memory name), description (a one-line description used to determine relevance in future conversations), and type (one of the four types). The `type` field must be one of the four types (strictly validated); legacy files without a type field can continue to work but cannot be filtered by type. **Why use Markdown files instead of a database?** This is an architectural choice worth analyzing. Using a file system instead of a database has the following advantages: 1. **Readability**: Developers can directly view and edit memory files with a text editor 2. **Version control**: Memory files naturally support Git tracking (if placed in a project directory) 3. **Portability**: The file system is the lowest common denominator, requiring no additional dependencies 4. **Debuggability**: When problems occur, simply `ls` and `cat` to diagnose 5. **Cost**: No need to maintain database connections, indexes, or backups The downside is limited query capability -- complex relational queries or full-text searches are not possible. However, for the Agent's memory scenario, the query pattern is a simple "load all relevant memories" rather than complex relational queries, and the file system's capabilities are sufficient. ### 6.2.2 MEMORY.md Index File `MEMORY.md` is the entry point of the memory system -- it is not a memory itself but an index file. At the start of each conversation, it is automatically loaded into the context, allowing the Agent to quickly understand the overview of existing memories. The memory directory module's constants define the index capacity limits: the index file is named `MEMORY.md`, with a maximum of 200 lines and 25KB. The format for index entries requires one line per entry, no more than 150 characters: ```markdown - [Title](file.md) -- One-line hook description ``` The `truncateEntrypointContent` function implements dual capacity protection: first truncation by lines (200-line limit), then truncation by bytes (25KB limit). When limits are exceeded, a warning is appended to the end of the file indicating which limit was triggered. **The Design Wisdom of Dual Capacity Protection** Why are two layers of limits needed? The line limit and byte limit each address different concerns: - **Line limit (200 lines)**: Protects the Agent's comprehension efficiency. Even if each line is short, an index of more than 200 lines requires the Agent to spend more tokens understanding and filtering. The line limit ensures that the index always remains a "quick browse" tool rather than a "deep reading" document. - **Byte limit (25KB)**: Protects the context budget. A single memory's index entry may contain a long description (approaching the 150-character limit), and 200 such entries could reach 30KB, putting pressure on the context window. The byte limit provides a hard cost ceiling. The order of the two layers also matters -- truncation by lines first, then by bytes. This means: when there are fewer memory entries but longer descriptions, the byte limit triggers first; when there are more memory entries but shorter descriptions, the line limit triggers first. In either case, there is a corresponding protection mechanism. ### 6.2.3 Directory Structure of Memory Files The storage path for memory files is determined by functions in the path resolution module. The default path is: ``` ~/.claude/projects/<sanitized-git-root>/memory/ ``` Path resolution priority: 1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` environment variable (full path override for Cowork mode) 2. `autoMemoryDirectory` setting (only policySettings/localSettings/userSettings, **projectSettings is excluded**) 3. Default `<memoryBase>/projects/<sanitized-git-root>/memory/` projectSettings is once again excluded for the same reason as in the previous chapter -- preventing a malicious repository from redirecting writes to sensitive directories via `autoMemoryDirectory: "~/.ssh"`. The path validation function performs strict security checks on this: rejecting relative paths, root paths, Windows drive letter roots, UNC paths, and null byte injection. **Complete Defense Lines of Path Security Validation** The path validation function's rejection list reveals the various path manipulation techniques an attacker might attempt: | Attack Technique | Example | Defense Method | |-----------------|---------|---------------| | Relative path | `../../etc/passwd` | Reject non-absolute paths | | Root path | `/` | Reject root paths | | Windows drive letter root | `C:\` | Reject drive letter root paths | | UNC path | `\\server\share` | Reject UNC paths | | Null byte injection | `foo\0.txt` | Reject paths containing null bytes | | Outside user directory | `/tmp/mem` | Reject paths not under allowed base directories | These checks stack on top of each other to form a defense-in-depth system. Even if one check is bypassed, the next check can still block the attack. > **Cross-Reference:** The projectSettings exclusion mechanism is consistent with the security boundary design in Chapter 5 (Settings and Configuration). Memory path validation is a specific application scenario of the configuration system's security policy. ## 6.3 Automatic Memory Extraction ### 6.3.1 Background Extraction Based on the Fork Pattern The core of the memory extraction system resides in the memory extraction module. It is not executed directly by the main conversation but runs in the background through `runForkedAgent` -- a "perfect fork" pattern. The so-called "perfect fork" means the background Agent shares the exact same system prompt and tool set as the main conversation. This means: - The background Agent has the same context understanding capabilities as the main Agent - The background Agent uses the same tool set but with stricter permission restrictions - **Prompt cache is shared between the main conversation and the background Agent** The extraction is triggered at the end of each complete query loop (triggered when the model produces a final response with no tool calls). ```mermaid flowchart TD start["Conversation Ends"] --> check_flag{"Did the main Agent<br/>actively save a memory?"} check_flag -->|Yes| skip["Skip Extraction<br/>(Mutex Mechanism)"] check_flag -->|No| throttle{"Has the throttle counter<br/>reached the threshold?"} throttle -->|No| wait["Wait for next conversation"] throttle -->|Yes| fork["Fork Background Agent"] fork --> shared["Share System Prompt + Tool List<br/>(Cache-Aware Design)"] shared --> extract["Background Agent Analyzes Conversation<br/>Extracts Valuable Memories"] extract --> save["Write Memory Files<br/>(Memory Directory Only)"] save --> update_index["Update MEMORY.md Index"] trailing{"Is there pending<br/>trailing context?"} update_index --> trailing trailing -->|Yes| trail_extract["Trailing Extraction<br/>(Skip Throttle Counter)"] trailing -->|No| done["Complete"] trail_extract --> done classDef process fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1 classDef decision fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 classDef skip_node fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c classDef success fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 class fork,shared,extract,save,update_index process class check_flag,throttle,trailing decision class skip skip_node class done,wait success ``` **Why not extract memory directly in the main conversation?** This design choice involves trade-offs across multiple dimensions: ``` Direct extraction in main conversation: Pros -> Simple implementation, no inter-process communication overhead Cons -> Increases user wait time, consumes main conversation's token budget, extraction logic failures may affect main conversation stability Fork-based background extraction: Pros -> Does not affect user experience, independent token budget, failures do not affect main conversation, can share cache to reduce costs Cons -> Higher implementation complexity, requires mutex mechanism to prevent duplicate writes ``` User experience is the most critical consideration. At the end of a long conversation, the user expects to see the final result immediately and start the next interaction. If they also have to wait for memory extraction to complete, even just a few seconds of delay, the cumulative impact would severely degrade interaction fluency. ### 6.3.2 Mutex Mechanism The mutex check function in the memory extraction module implements an elegant mechanism: if the main Agent has already written a memory file during the current conversation, the background extraction is skipped entirely. The main conversation's system prompt already includes complete memory saving instructions. When the main Agent actively saves a memory, the forked background Agent detects this fact and skips the current extraction -- the two are **mutually exclusive**, preventing duplicate writes. **The mutex mechanism's implementation is an elegant "eventual consistency" design.** ```mermaid sequenceDiagram participant MainConversation as Main Conversation participant MemoryFlag as Memory Flag participant BackgroundAgent as Background Agent Note over MainConversation: Case 1: Main Agent actively saves memory MainConversation->>MainConversation: T1: Conversation starts MainConversation->>MemoryFlag: T3: Main Agent saves feedback memory Note over MemoryFlag: Write flag is set MainConversation->>BackgroundAgent: T4: Conversation ends, triggers background extraction BackgroundAgent->>MemoryFlag: T5: Detects write flag Note over BackgroundAgent: Skip extraction (mutex takes effect) Note over MainConversation,BackgroundAgent: --- Note over MainConversation: Case 2: No active memory save MainConversation->>MainConversation: T1: Conversation starts MainConversation->>BackgroundAgent: T2: Conversation ends, no active save BackgroundAgent->>BackgroundAgent: T4: Analyzes conversation, extracts 2 memories Note over BackgroundAgent: Normal extraction ``` The advantage of this design is avoiding redundant memories caused by "duplicate extraction." Imagine: if the main Agent saves "user prefers pnpm," and the background Agent independently analyzes and determines "user prefers pnpm," the same information produces two memories, wasting storage space and increasing the burden of future filtering. ### 6.3.3 Tool Permission Allowlist The background Agent's tool permissions are strictly controlled through the `createAutoMemCanUseTool` function: | Tool | Permission | Design Rationale | |------|-----------|-----------------| | Read / Grep / Glob | Unrestricted (read-only) | Need to read code to understand conversation context | | Bash | Read-only commands only (ls, find, grep, etc.) | Need to view file state but cannot execute modification commands | | Edit / Write | Paths within memory directory only | Need to write memory files but cannot modify project code | | REPL | Allowed (but internal calls are subject to above restrictions) | May need to execute code to verify information | | All other tools | Denied | Background Agent should not trigger side effects like network requests | This design gives the background Agent sufficient read capabilities to understand conversation content, while write capabilities are restricted to the memory directory -- it cannot modify project code or execute dangerous commands. **Perfect Embodiment of the Principle of Least Privilege** The tool permission allowlist embodies the Principle of Least Privilege in security design: the background Agent is granted only the minimum set of permissions needed to fulfill its responsibilities. Specifically: - **Why is Glob allowed?** The background Agent may need to discover relevant files to verify memory accuracy (e.g., whether a file mentioned in memory still exists) - **Why is Bash restricted to read-only?** To prevent the background Agent from executing destructive commands (like `rm -rf`) without the user's knowledge - **Why are Edit/Write restricted to the memory directory?** To prevent the background Agent from modifying project code (which could change build results or introduce bugs) ### 6.3.4 Throttling and Coordination Extraction does not run after every conversation. The system implements a counter-based throttling mechanism: extraction is triggered only after every several rounds, with the threshold configured through feature flags. Additionally, there is a **trailing extraction** mechanism to handle concurrency issues. When an extraction is in progress and another conversation completes, the new context is staged. After the current extraction completes, a trailing extraction runs using the latest staged context. Trailing extraction bypasses the throttle counter -- it handles already-completed work and should not be delayed by throttling. **Timing Diagram of the Trailing Extraction Mechanism** ```mermaid sequenceDiagram participant ConvA as Conversation A participant ExtrA as Extraction A participant ConvB as Conversation B participant TrailB as Trailing Extraction B participant ConvC as Conversation C participant ExtrC as Extraction C ConvA->>ExtrA: Conversation completes, triggers extraction Note over ExtrA: Normal extraction in progress... ConvB->>ConvB: Conversation completes, stages context Note over ConvB: Waiting for Extraction A to complete... ExtrA-->>TrailB: Extraction completes TrailB->>TrailB: Uses Conversation B context<br/>Skips throttle counter ConvC->>ExtrC: Conversation completes, triggers extraction ``` Without the trailing extraction mechanism, Conversation B's context might never be extracted -- because the next extraction uses Conversation C's context, and Conversation B may have contained unique, memory-worthy information that would be missed. **Design Considerations for the Throttle Counter** The throttle counter design reflects a cost-benefit analysis: each memory extraction requires a complete API call (even with cache sharing, output token costs still apply). For frequent short conversations (like simple Q&A), the cost of memory extraction may exceed its value. The throttling mechanism ensures that extraction is triggered only after accumulating enough conversation rounds, improving the "information density" of each extraction. ## 6.4 Cache-Aware Memory Architecture ### 6.4.1 Prompt Cache Sharing In LLM APIs, prompt cache is an important cost optimization mechanism -- if two requests share the same prefix, the API can reuse the already-computed KV cache, significantly reducing latency and cost. Claude Code's forked Agent pattern implements cache sharing through the `CacheSafeParams` type. The parameter extraction function extracts shared parameters from the context, including the system prompt, user context, system context, tool usage context, and message history. This means the background extraction Agent's API request prefix is identical to the main conversation's -- the API provider can hit the cached prefix, avoiding recomputation. In a typical session, this can save substantial token consumption. **Cost Impact of Cache Sharing -- A Simplified Calculation** ``` Assumptions: - System prompt + tool definitions ≈ 30,000 tokens - Message history ≈ 50,000 tokens (a medium-length conversation) - Cached input price = $0.10 / MTok (cache hit) - Standard input price = $3.00 / MTok (no cache) Without cache sharing: Extraction Agent resends 80,000 tokens → $0.24 With cache sharing: Extraction Agent reuses cache → $0.008 (pays only cache read fees) Savings: 96.7% ``` In scenarios with frequent Agent usage (dozens of conversations per day), the cost savings from cache sharing are substantial. ### 6.4.2 Tool List Consistency Requirement Cache sharing has an implicit constraint: **the tool list is part of the API cache key.** If the forked Agent uses a different tool set from the main Agent, the cache cannot be hit. This is why tool permission filtering uses the `canUseTool` callback rather than a different tool list -- the tool list remains consistent, with filtering applied only at execution time. **This design choice demonstrates an important architectural principle: consistent interface, variable behavior.** ```mermaid flowchart LR subgraph ApproachA["Approach A (Not Recommended): Different Tool Lists"] ma["Main Agent Tool Set<br/>Read, Write, Edit, Bash, Grep, Glob..."] ea["Extraction Agent Tool Set<br/>Read, Grep, Glob, MemoryWrite"] ma -.->|"Different cache key"| x["Cache Miss<br/>Increased Cost"] ea -.-> x end subgraph ApproachB["Approach B (Recommended): Same Tool List, Different Permissions"] mb["Main Agent Tool Set<br/>Read, Write, Edit, Bash, Grep, Glob..."] eb["Extraction Agent Tool Set<br/>Read, Write, Edit, Bash, Grep, Glob..."] perm["Extraction Agent Permissions<br/>canUseTool Callback Filter"] mb -.->|"Same cache key"| check["Cache Hit<br/>Cost Savings"] eb -.-> check eb --- perm end classDef agent fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1 classDef bad fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c classDef good fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 classDef perm_node fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 class ma,mb,eb agent class x bad class check good class perm perm_node ``` This principle applies not only to the memory system but also serves as general guidance for designing high-performance Agent architectures: keep cache-related interface parameters (tool lists, system prompt prefixes) unchanged as much as possible, and place differentiation logic in runtime behavioral control. ### 6.4.3 Memory Lifecycle The decision chain for enabling memory is defined in the path resolution module: 1. `CLAUDE_CODE_DISABLE_AUTO_MEMORY` environment variable (1/true -> disabled) 2. `--bare` (SIMPLE mode) -> disabled 3. CCR without persistent storage (no `CLAUDE_CODE_REMOTE_MEMORY_DIR`) -> disabled 4. `autoMemoryEnabled` field in `settings.json` 5. Default: enabled Background extraction also needs to pass through a GrowthBook feature gate. This is dual-layer control: a compile-time feature flag plus a runtime GrowthBook experiment. **Memory Lifecycle State Machine** ```mermaid stateDiagram-v2 [*] --> MemoryDoesNotExist MemoryDoesNotExist --> MemoryCreated: Agent saves memory / Background extraction MemoryCreated --> MemoryActive: Loaded in subsequent conversation Note right of MemoryCreated: MEMORY.md index updated MemoryActive --> MemoryInvalidatedDeleted: Memory content outdated / Manual deletion Note right of MemoryActive: Agent references in conversation MemoryInvalidatedDeleted --> [*] Note right of MemoryInvalidatedDeleted: MEMORY.md index entry removed ``` Memory "invalidation" is a topic worth discussing. Claude Code does not implement an automatic memory expiration mechanism -- once a memory is saved, it persists indefinitely unless manually deleted or overridden by a subsequent memory. This means memory quality management relies on the Agent's judgment (not saving information that isn't worth saving) and the user's active maintenance. ### 6.4.4 Memory Reading and Validation The memory type validation module defines the core principle for memory reading: **memory is a point-in-time snapshot, not a current fact.** ``` "Memory says X exists" does not mean "X currently exists." ``` Specific validation rules: - If the memory names a file path: check if the file exists - If the memory names a function or flag: grep to find it - If the user is about to act on your recommendation (rather than asking about history): validate first This reflects a deep design philosophy of Claude Code: **memory is a trusted clue, not a trusted conclusion.** It guides the Agent on where to find information but does not replace the Agent's independent verification of the current state. **Design Philosophy of Validation Levels** ```mermaid graph TD subgraph TrustLevels["Memory Trust Levels (Low to High)"] L0["Level 0: No Trust<br/>Re-acquire all information"] L1["Level 1: Trust as Clue<br/>Memory indicates direction, independently verify facts<br/>✓ Claude Code adopts this level"] L2["Level 2: Trust as Fact<br/>Memory is the current truth"] end L0 --> L1 --> L2 classDef low fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c classDef mid fill:#c8e6c9,stroke:#388e3c,stroke-width:3px,color:#1b5e20 classDef high fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c class L0 low class L1 mid class L2 high ``` Claude Code choosing Level 1 is a pragmatic balance. Level 0 is too conservative -- if memories are completely untrusted, the memory system loses its value. Level 2 is too aggressive -- the code repository's state may have changed, and outdated memories can mislead decisions. Level 1 lets memories serve as "indices" and "guides" while maintaining independent verification of the current state. This principle is particularly important in the following scenarios: 1. **Code references**: The memory says "user authentication is in `src/auth/handler.ts`," but the file may have been moved during a refactoring. The Agent should check if the file exists before directly referencing it. 2. **Dependency versions**: The memory says "the project uses React 18," but the team may have upgraded to React 19. The Agent should read `package.json` to confirm the current version. 3. **Decision rationale**: The memory says "PostgreSQL was chosen because complex queries are needed," and this decision rationale is unlikely to become outdated -- it explains "why" rather than "what," so it can be directly trusted. > **Cross-Reference:** The "clue not conclusion" principle for memory validation is closely related to the compression strategy in Chapter 7 (Context Management). Compressed summaries follow the same principle -- summaries are historical clues, and the Agent should verify the current state mentioned in summaries when necessary. --- ## Practical Exercises ### Exercise 1: Memory Type Classification What memory type should the following information be saved as? 1. "Our team uses the Linear project 'BACKEND' to track backend bugs" 2. "The user is a junior developer, using TypeScript for the first time" 3. "Integration tests must use a real database, no mocking -- last time mocking caused a production incident" 4. "The authentication middleware rewrite was due to legal compliance requirements, not technical debt" 5. "The API documentation Swagger UI is at http://localhost:3000/api-docs" 6. "Every time you create a new component, write tests before implementation" **Reference Answers:** 1. `reference` -- external system pointer 2. `user` -- user profile 3. `feedback` -- behavioral directive (includes Why: production incident) 4. `project` -- project decision (includes Why: legal compliance) 5. `reference` -- external reference (development environment URL) 6. `feedback` -- behavioral directive (includes an explicit rule and an implied reason) ### Exercise 2: Frontmatter Writing Write the Frontmatter and content for a memory file for the following scenario: Scenario: The user says "from now on, run `npm run lint` before every code commit; last time someone committed unlinted code and CI failed for a whole day." **Reference Answer:** ```markdown --- name: pre-commit-lint-requirement description: Must run npm run lint before every commit; CI failed for a full day due to unlinted code type: feedback --- **Rule**: Run `npm run lint` before every code commit. **Why**: A previous commit with unlinted code caused CI to fail for an entire day, blocking the team. **How to apply**: Before using git commit, always run `npm run lint` first and fix any errors. This applies to all files changed in the commit, not just new files. ``` **Extended Reflection:** If the user says a week later "lint rules have been integrated into the pre-commit hook, no need to run manually anymore," how should the Agent handle this memory? Should it delete it or update it? ### Exercise 3: Cache-Aware Architecture Analysis Suppose you want to add a new tool `MemorySearch` (for semantic search of memory files) to the forked Agent. Which of the following two approaches is better? - Approach A: Add `MemorySearch` to the forked Agent's tool list, replacing `Grep` - Approach B: Keep the tool list unchanged, restrict Grep to only search the memory directory through the `canUseTool` permission callback **Reference Answer:** Approach B is better. Approach A changes the tool list, resulting in a different API cache key and preventing sharing of the main conversation's prompt cache. Approach B maintains tool list consistency, with permissions filtered at execution time rather than definition time, preserving cache sharing capability. ### Exercise 4: Memory Management Strategy Design You maintain 5 projects simultaneously, each with 20-30 memories in its memory directory. Design a memory management strategy that addresses the following issues: - How to avoid cross-project memory confusion? - How to handle outdated memories? - How to ensure the MEMORY.md index stays within limits? **Reference Answer Hints:** - Memories are naturally isolated by project (paths are based on Git root directory); only user-type memories are shared across projects - Regularly review the memory directory and delete low-value memories that fail the "would behavior change if deleted" test - Control the description length of each memory entry, and periodically merge memory entries on related topics --- ## Key Takeaways 1. **Closed Four-Type System**: user, feedback, project, reference -- only save information that cannot be derived from code, excluding derivable content like code patterns and Git history. The closed type system enables efficient consistent reasoning. 2. **Dual Capacity Protection**: The MEMORY.md index is limited to 200 lines / 25KB, with line truncation applied first then byte truncation, ensuring the index always remains a "quick browse" tool rather than a "deep reading" document. 3. **Fork Pattern**: The background Agent perfectly forks the main conversation, sharing prompt cache and limiting write permissions through the `canUseTool` allowlist. It does not affect user experience and has an independent token budget. 4. **Mutex Extraction**: Memory writes by the main Agent and the background Agent are mutually exclusive -- when the main Agent writes, the background skips, avoiding redundant memories from duplicate extraction. 5. **Cache Awareness**: Tool list consistency is a prerequisite for cache sharing. Permission filtering uses runtime callbacks rather than compile-time different tool lists. Consistent interface, variable behavior. 6. **Validation First**: Memory is a snapshot, not a fact; the current state must be validated before making recommendations. "Clue not conclusion" is the correct mindset for using memory. 7. **Least Privilege**: The background Agent's tool permission allowlist strictly limits write capabilities, embodying the Principle of Least Privilege. 8. **Trailing Extraction**: The trailing extraction mechanism ensures no conversation's memory extraction opportunity is missed during concurrent extraction periods.

claude-code-book - en Part 2 Core Systems 05 Settings and Configur...

41565 characters

# Chapter 5: Settings and Configuration -- Agent DNA > **Learning Objectives:** Understand the merge rules and security boundaries of the six-layer configuration sources, master the compile-time optimization mechanism of the feature flag system, and grasp the design philosophy of the Zustand-like immutable state store. Through this chapter, you will be able to design reasonable configuration strategies for teams of different scales and understand how the configuration system serves as the first line of defense in Agent security. --- Claude Code's behavior is not determined by a single configuration file, but by six layers of configuration sources merged in sequence. These configuration sources are like the Agent's "DNA" -- they are written before the Agent starts, determining what the Agent can do, what it cannot do, and how it does things. Understanding this system is the first step toward mastering Claude Code's behavior customization capabilities. The analogy of the configuration system as "DNA" is fitting: just as an organism's genes are determined at the moment of fertilization and expressed layer by layer during development, Claude Code's configuration is loaded at startup and takes effect layer by layer at runtime. The difference is that the Agent's "DNA" can be precisely edited and overridden -- this is both a powerful capability and a security challenge. ## 5.1 The Priority System of Six Configuration Sources ### 5.1.1 Configuration Source Definition and Order Claude Code's configuration sources are defined as an ordered array in the configuration constants module, containing five configuration origins: user global settings (userSettings), project shared settings (projectSettings), project local settings (localSettings, gitignored), CLI flag settings (flagSettings), and enterprise policy settings (policySettings). The configuration loading function `loadSettingsFromDisk()` follows a key principle: **later-loaded sources override earlier ones**. The merge is not a simple full replacement, but uses a deep merge strategy with custom rules. In practice, there is also a hidden lowest-priority layer: **pluginSettings** (plugin settings). In the configuration loading process, plugin settings are loaded first as the base for merging, and subsequent configuration layers are stacked on top of this base. Therefore, the complete priority chain from low to high is: **pluginSettings -> userSettings -> projectSettings -> localSettings -> flagSettings -> policySettings** To better understand the relationship between these six configuration sources, we can use a "geological strata" model as an analogy: ```mermaid graph BT subgraph ConfigSourcePriority["Configuration Source Priority (Low to High)"] plugin["pluginSettings<br/>Plugin Settings<br/>Base Default Values"] user["userSettings<br/>User Global Settings<br/>Personal Global Defaults"] project["projectSettings<br/>Project Shared Settings<br/>Team Shared, in Git"] local["localSettings<br/>Project Local Settings<br/>Personal Preferences, not in Git"] flag["flagSettings<br/>CLI Flag Settings<br/>One-time Override"] policy["policySettings<br/>Enterprise Policy Settings<br/>Highest Priority: Enterprise-level Lockdown"] end plugin --> user --> project --> local --> flag --> policy classDef baseLayer fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20 classDef midLayer fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 classDef topLayer fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#b71c1c class plugin baseLayer class user,project,local midLayer class flag,policy topLayer ``` > Each layer can override the configuration of lower layers, but will not delete them -- they are simply "shadowed." This design ensures that each layer can be independently understood and maintained. ### 5.1.2 Merge Rules ```mermaid flowchart LR subgraph MergeStrategy["Merge Strategy Rules"] A["Array Type<br/>Concatenate and Deduplicate"] B["Object Type<br/>Deep Merge<br/>Nested Properties Overridden Layer by Layer"] C["Scalar Type<br/>Latter Directly Overrides Former"] end example1["permissions.allow<br/>Accumulates Rules from All Sources"] --> A example2["hooks.PreToolUse<br/>Nested Properties Merged Layer by Layer"] --> B example3["model<br/>Higher Priority Source Directly Overrides"] --> C classDef strategy fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1 classDef example fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,color:#424242 class A,B,C strategy class example1,example2,example3 example ``` The core of the merge logic lies in the custom merge strategy function `settingsMergeCustomizer`: this function performs concatenation and deduplication for array types, while other types are handled by the default deep merge logic. The key characteristics of these rules are: - **Array type**: Concatenate and deduplicate (rather than replace). For example, the `permissions.allow` field accumulates rules from all sources. - **Object type**: Deep merge. Nested properties are overridden layer by layer. - **Scalar type**: The latter directly overrides the former. This means that if `userSettings` sets `model: "claude-sonnet-4"` and `policySettings` sets `model: "claude-opus-4"`, the final effective value is `"claude-opus-4"`. But if both set `permissions.allow: ["Bash(*)"]`, the final result is a merged, deduplicated array. **Why do arrays use concatenation instead of replacement?** This is a deliberate design decision. In the permission system, each rule is a "defense line" -- if a higher-priority source's array replaced a lower-priority source's array, the higher-priority source would have to completely enumerate all needed permission rules, and any omission would become a security vulnerability. The concatenation strategy allows each layer to only care about the rules it wants to "add," and the system automatically merges all layers' security policies. > Anti-Pattern Warning: Do not use array concatenation to "revoke" rules from lower layers. For example, you cannot clear lower-layer permissions by setting an empty array in an upper layer -- after concatenation, the lower-layer rules still exist. If you need to revoke, you should use the `permissions.deny` field to explicitly deny. Let's demonstrate the merge process with a complete example: ``` Scenario: A Frontend Team's Project Configuration // ~/.claude/settings.json (userSettings - Developer Xiao Zhang's personal global settings) { "model": "claude-sonnet-4", "permissions": { "allow": ["Bash(npm *)", "Bash(node *)"] }, "verbose": true } // .claude/settings.json (projectSettings - Team shared settings, committed to Git) { "permissions": { "allow": ["Bash(npm run lint)", "Bash(npm test)", "Read(*)"] }, "hooks": { "PreToolUse": [{ "matcher": "Bash(*)", "hooks": [{ "type": "command", "command": "audit-log.sh" }] }] } } // .claude/settings.local.json (localSettings - Xiao Zhang's local override) { "model": "claude-opus-4", "permissions": { "allow": ["Bash(git *)"] } } Merge Result: { "model": "claude-opus-4", // localSettings overrides userSettings "verbose": true, // Only set by userSettings, remains unchanged "permissions": { "allow": [ "Bash(npm *)", // From userSettings "Bash(node *)", // From userSettings "Bash(npm run lint)", // From projectSettings "Bash(npm test)", // From projectSettings "Read(*)", // From projectSettings "Bash(git *)" // From localSettings ] // Array concatenated and deduplicated }, "hooks": { "PreToolUse": [{ ... }] // Only set by projectSettings } } ``` ### 5.1.3 Actual Paths of Configuration Files Each configuration source corresponds to a specific file path, determined by the configuration path mapping function: | Configuration Source | File Path | Description | In Git? | |---------------------|-----------|-------------|---------| | userSettings | `~/.claude/settings.json` | Global user settings | N/A (User directory) | | projectSettings | `<project>/.claude/settings.json` | Project shared settings (committed to Git) | Yes | | localSettings | `<project>/.claude/settings.local.json` | Project local settings (added to .gitignore) | No | | flagSettings | CLI `--settings` parameter specified path | One-time override | N/A | | policySettings | Platform-specific managed-settings.json | Enterprise managed | N/A | The resolution of `policySettings` is the most complex. It follows a **"first source wins"** (first non-empty source wins) strategy, with priority from high to low: ```mermaid flowchart TD start["policySettings Resolution"] --> remote["1. Remote API Settings<br/>getRemoteManagedSettingsSyncFromCache"] remote -->|"Non-empty"| result["Use This Source"] remote -->|"Empty"| mdm["2. MDM Settings<br/>macOS plist / Windows HKLM"] mdm -->|"Non-empty"| result mdm -->|"Empty"| file["3. managed-settings.json<br/>+ managed-settings.d/*.json"] file -->|"Non-empty"| result file -->|"Empty"| hkcu["4. HKCU Registry<br/>Windows User-level"] hkcu -->|"Non-empty"| result hkcu -->|"Empty"| empty["No Policy Configuration"] classDef source fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 classDef result fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 classDef empty fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c class remote,mdm,file,hkcu source class result result class empty empty ``` Note that policySettings uses "first non-empty source wins" rather than "deep merge." This difference is crucial: enterprise management policies are typically a complete, audited configuration scheme, and policies from different sources should not "leak" into each other. For example, a policy delivered via remote API already contains complete security rules and should not be diluted by partial configurations from a local managed-settings.json file. ### 5.1.4 The Special Status of the Policy Layer Unlike other configuration sources that use deep merge, `policySettings` employs a completely different resolution logic. Instead of reading from files and merging, it searches for the first non-empty source by priority (remote API settings > MDM settings > managed-settings.json file > HKCU registry). This means enterprise administrators only need to configure policies in one location, and the system will use the highest-priority source rather than merging all sources. **Design Decision Analysis: Why is the merge logic for policySettings different from other configuration sources?** This stems from two different trust models. The five configuration layers from userSettings to flagSettings follow an "incremental accumulation" model -- each layer adds its preferences on top of trusting the lower layers. policySettings, on the other hand, follows a "single authority" model -- enterprise policies are completely delivered from a single authoritative source, and merging between different sources would introduce unpredictable behavior. Imagine if two MDM systems respectively delivered different model restrictions and permissions rules. After merging, semantic conflicts could arise: one restricts the available model list, another restricts the permission scope, but the merged result might allow users to use a restricted model to bypass permission restrictions. The "first non-empty source wins" strategy ensures the determinism and auditability of the policy source. ### 5.1.5 Real-World Project Practices for Configuration Loading In real-world projects, the proper use of the six-layer configuration system can greatly enhance team collaboration efficiency and security. Here are several common configuration strategy patterns: **Pattern 1: Personal-Team Separation** This is the most common pattern. Developers place personal preferences in `userSettings` and `localSettings`, and team-shared rules in `projectSettings`: ``` ~/.claude/settings.json -> Personal model preferences, commonly used personal permission rules .claude/settings.json -> Team-wide lint rules, permission baselines, shared hooks .claude/settings.local.json -> Personal overrides (debug mode, special permissions) ``` **Pattern 2: CI/CD-Specific Configuration** In automated pipelines, use `flagSettings` to inject one-time configuration via CLI parameters, avoiding modifications to any persistent configuration files: ``` claude --settings /path/to/ci-settings.json ``` This approach ensures that CI environment configuration is temporary and traceable, without polluting developers' local environments. **Pattern 3: Enterprise Unified Control** Large organizations uniformly deliver policySettings through MDM or remote APIs, locking down security-related configuration items (allowed tools, hooks whitelist, etc.) while allowing teams to customize non-security-related behaviors in projectSettings: ``` policySettings -> Locked: model, permissions.deny, allowManagedHooksOnly projectSettings -> Customized: hooks (non-security), MCP server configuration userSettings -> Personalized: verbose, theme and other UI preferences ``` ## 5.2 Security Boundary Design The core security challenge of the configuration system is: `projectSettings` (`.claude/settings.json`) is committed to Git repositories, which means users who clone a malicious repository may unknowingly load the attacker's configuration. Claude Code's defense strategy is: **systematically exclude `projectSettings` in security-sensitive checks**. This is like a building's access control system: project keycards can open meeting rooms and break rooms (projectSettings), but can never open server rooms and secure rooms (security-sensitive operations). Different levels of access are controlled by different trust levels. ### 5.2.1 Threat Model of Supply Chain Attacks Before understanding the security boundary design, we need to clarify the threat model. The particularity of supply chain attacks in the Agent scenario is: **Traditional Supply Chain Attacks vs. Agent Configuration Supply Chain Attacks** | Dimension | Traditional Software Supply Chain | Agent Configuration Supply Chain | |-----------|----------------------------------|--------------------------------| | Attack Vector | Malicious dependency packages, tampered build artifacts | Malicious configuration files, hooks injection | | Victim | System running the software | Developer using the Agent | | Attack Surface | Build pipeline, runtime environment | File system, code execution, data access | | Stealth | Medium (requires bypassing security detection) | Very High (configuration files appear normal) | | Impact Scope | Software users | Developer's entire work environment | A concrete attack scenario: An attacker creates a seemingly normal open-source project with a `PreToolUse` hook configured in `.claude/settings.json` that sends the user's sensitive information (API keys, environment variables) to the attacker's server on every tool call. When a developer clones the project and starts Claude Code, the malicious hook executes without their knowledge. **Why is projectSettings the highest-risk configuration source?** Unlike other configuration sources, projectSettings has three unique properties that make it the primary risk point: 1. **Untrusted Source**: projectSettings comes from cloned third-party repositories, not written by the user themselves 2. **Automatic Loading**: Configuration takes effect automatically upon entering the project directory, without user confirmation 3. **Executable Code**: Hooks configuration can execute arbitrary shell commands userSettings and localSettings do not have the first property (they are on the user's own filesystem), flagSettings requires the user to explicitly specify it (does not have the second property), and policySettings is controlled by administrators (does not have the first property). Therefore, projectSettings is the only configuration source that simultaneously possesses all three high-risk properties. ### 5.2.2 shouldAllowManagedHooksOnly In the hooks configuration snapshot module, the `shouldAllowManagedHooksOnly` function determines whether only managed hooks are allowed to run. It checks whether `allowManagedHooksOnly` is enabled in the policy settings, returning true if it is. When this function returns `true`, hooks execution only uses hooks from `policySettings`; all hooks from user/project/local sources are skipped. This is an enterprise security feature: administrators can ensure that only audited hooks run within the organization. **Real-World Scenario: Financial Institution Compliance Requirements** Suppose a financial institution requires all code changes to be logged through an internal audit system. An administrator can configure the following in managed-settings.json: - Set `allowManagedHooksOnly: true` to block all non-managed hooks - Configure an audit logging hook in policySettings' hooks - This way, regardless of what hooks are in developers' local projectSettings, only the administrator's audit hook will run This "managed-only" mode ensures that hooks execution within the organization is predictable and auditable -- developers cannot bypass auditing by modifying local configurations. ### 5.2.3 pluginOnlyPolicy The plugin policy module implements the `strictPluginOnlyCustomization` strategy, which defines four lockable "customization surfaces": skills, agents, hooks, and mcp. The core judgment function `isRestrictedToPluginOnly` checks the policy configuration: if the policy is `true`, all surfaces are locked; if the policy is an array, only the specified surfaces are locked. Within locked surfaces, only the following sources are trusted: - **plugin** -- separately managed through `strictKnownMarketplaces` - **policySettings** -- set by administrators, inherently trusted - **built-in / bundled** -- shipped with the CLI, not user-written User-level (`~/.claude/*`) and project-level (`.claude/*`) customizations are completely blocked. The elegance of this design lies in "selective locking." Enterprise administrators don't need to bluntly prohibit all customization but can finely control which customization surfaces need to be locked. For example: - Lock `mcp`: Prevent developers from connecting to unapproved MCP servers (preventing data leaks) - Lock `hooks`: Prevent developers from executing unaudited custom scripts - Don't lock `skills`: Allow developers to create custom skills (enhancing productivity) ### 5.2.4 Systematic Exclusion of projectSettings In security-sensitive functions, `projectSettings` is consistently excluded. The following functions demonstrate the same pattern: when checking `skipDangerousModePermissionPrompt`, they only read from userSettings, localSettings, flagSettings, and policySettings, **intentionally excluding projectSettings**. The same exclusion pattern appears in `hasAutoModeOptIn()`, `getUseAutoModeDuringPlan()`, and `getAutoModeConfig()`. The comments consistently point to the same reason: **projectSettings is intentionally excluded -- a malicious project could otherwise auto-bypass the dialog (RCE risk)**. The assumption behind this defense is: the user's own settings (`userSettings`/`localSettings`) are trusted because they are on the user's filesystem and edited by the user themselves; whereas `projectSettings` may come from cloned third-party repositories, posing a supply chain attack risk. **Design Principle Summary: Decreasing Trust Radius** Claude Code's security boundaries follow a clear "decreasing trust radius" principle: ```mermaid graph TD subgraph TrustLevel["Trust Level (High to Low)"] direction TB policy["policySettings (5 stars)<br/>Enterprise administrator configured, through audit process"] flag["flagSettings (4 stars)<br/>User explicitly specified CLI parameters"] local["localSettings (4 stars)<br/>User local files, not in Git"] user["userSettings (4 stars)<br/>User global files, user-controlled"] project["projectSettings (2 stars)<br/>May come from third-party repositories, untrusted"] plugin["pluginSettings (1 star)<br/>Plugin ecosystem, requires additional verification"] end policy --> flag --> local --> user --> project --> plugin classDef trusted fill:#c8e6c9,stroke:#388e3c,stroke-width:2px,color:#1b5e20 classDef caution fill:#fff9c4,stroke:#f9a825,stroke-width:2px,color:#f57f17 classDef danger fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#b71c1c class policy,flag,local,user trusted class project caution class plugin danger ``` > In security-sensitive checks, the system only reads from configuration sources with a trust level of 4+ stars. This principle runs throughout the security design of the entire configuration system. > Cross-Reference: The `projectSettings` exclusion mechanism discussed in this chapter is closely related to the security model in Chapter 8 (Hooks System). The loading of hooks similarly follows this trust model -- when `allowManagedHooksOnly` is enabled, hooks from projectSettings are completely blocked. ## 5.3 Feature Flag System Claude Code's feature flag system is divided into two layers: **compile-time** `feature()` function and **runtime** GrowthBook experimentation framework. This dual-layer design reflects a classic tradeoff in software release engineering: compile-time flags provide zero-overhead feature control, while runtime flags provide rapid iteration capability without redeployment. ### 5.3.1 Compile-Time Dead Code Elimination The `feature()` function is introduced through the bundler. When `feature('FEATURE_NAME')` returns `false`, the bundler completely removes the corresponding code branch. This is a form of compile-time dead code elimination. Throughout the codebase, this pattern appears extensively: feature flags determine whether certain functionality code is compiled, and unenabled features are completely absent from the build artifacts. **Why choose compile-time elimination over runtime conditional checks?** Consider two approaches: ``` Approach A (Runtime Check): if (featureFlags.isEnabled('KAIROS')) { ... } Approach B (Compile-Time Elimination): if (feature('KAIROS')) { ... } // Bundler completely removes this branch when false ``` The problems with Approach A are: (1) unenabled feature code still occupies package size, affecting load times; (2) conditional branches produce minor performance overhead on hot paths; (3) code for unenabled features may contain uncovered bugs or security vulnerabilities. Approach B solves all three problems through compile-time elimination -- unenabled features simply do not exist in the build artifacts. The main feature flags include: | Feature Flag | Scope | Description | Architectural Significance | |-------------|-------|-------------|--------------------------| | `KAIROS` | Core Architecture | Assistant mode (persistent sessions) | Core interaction mode switching | | `EXTRACT_MEMORIES` | Memory System | Background memory extraction | Directly related to Chapter 6 Memory System | | `TRANSCRIPT_CLASSIFIER` | Permission System | Automatic mode classifier | Foundation for automated permission decisions | | `TEAMMEM` | Collaboration System | Team memory | Key capability for multi-person collaboration scenarios | | `CHICAGO_MCP` | Tool System | Computer Use MCP | Interface for extending the tool ecosystem | | `TEMPLATES` | Task System | Templates and workflows | Infrastructure for reusable workflows | | `BUDDY` | UI System | Companion sprite | Enhancement of user interaction experience | | `DAEMON` | Architecture | Background daemon | Capability for persistent background operation | | `BRIDGE_MODE` | Architecture | Bridge mode | Bridge for cross-process communication | From these feature flags, we can discern the evolutionary direction of Claude Code's architecture: KAIROS (persistent sessions) and DAEMON (background daemon) point to the evolution path from "on-demand invocation" to "continuous operation"; TEAMMEM (team memory) and TEMPLATES (template workflows) point to the evolution path from "personal tool" to "team infrastructure." ### 5.3.2 GrowthBook Experimentation Framework For features that need to be dynamically controlled at runtime, Claude Code uses the GrowthBook A/B testing framework. The main entry function `getFeatureValue_CACHED_MAY_BE_STALE` accepts a feature name and default value, and synchronously reads the experiment configuration from cache. The "CACHED_MAY_BE_STALE" in the function name plainly states its semantics: the value comes from cache and may be stale in cross-process scenarios. This is to avoid asynchronous waiting on the startup critical path -- synchronously reading from cache is preferable to blocking while waiting for remote configuration. **This naming convention is worth learning for all system designers.** In API design, encoding non-obvious behavioral constraints directly into function names "reminds" callers of this constraint every time they use it. In contrast, a function named `getFeatureValue` would give the false expectation of "returning the latest value." In the memory system, we can see the usage of GrowthBook feature flags -- checking randomly named feature flags to determine whether to enable certain memory features. These feature flags named with random animal names (e.g., `tengu_passport_quail`, `tengu_coral_fern`, `tengu_moth_copse`) are standard practice for GrowthBook experiments -- random names avoid semantic bias and do not conflict with the compile-time `feature()` function. **Decision Tree for Compile-Time vs. Runtime Flags:** ```mermaid flowchart TD start["Need a Feature Flag?"] --> q1{"Can the feature code be<br/>determined at release time?"} q1 -->|Yes| compile["Use feature() Compile-Time Flag"] q1 -->|No| q2{"Need runtime dynamic control?"} q2 -->|Yes| growthbook["Use GrowthBook Runtime Flag"] q2 -->|No| q3{"Need A/B testing?"} q3 -->|Yes| ab["Use GrowthBook + Random Experiment Groups"] q3 -->|No| hardcode["Hardcode Directly<br/>No Flag Needed"] compile --> compile_pro["Pros: Zero runtime overhead, reduced package size<br/>Cons: Changes require redeployment"] growthbook --> growthbook_pro["Pros: Adjustable without redeployment<br/>Cons: Runtime overhead, depends on cache"] ab --> ab_pro["Random naming avoids semantic bias<br/>Does not conflict with compile-time feature()"] classDef decision fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1 classDef option fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c classDef detail fill:#e8f5e9,stroke:#388e3c,stroke-width:1px,color:#1b5e20 class q1,q2,q3 decision class compile,growthbook,ab,hardcode option class compile_pro,growthbook_pro,ab_pro detail ``` > Cross-Reference: The compile-time feature flag `EXTRACT_MEMORIES` directly controls whether the background memory extraction feature in Chapter 6 (Memory System) is compiled into the final artifact. This is a classic case of the configuration system influencing core functionality. ## 5.4 State Management System State management is the "nervous system" of an Agent system -- configuration is the DNA that determines the Agent's potential; state is the nervous system that transmits and coordinates the Agent's runtime behavior in real time. Claude Code has chosen a minimalist yet powerful state management solution whose design philosophy deserves in-depth analysis. ### 5.4.1 Store: A Minimalist Immutable State Container The state management module implements a generic Store in just 34 lines, inspired by Zustand. The Store provides three core methods: `getState` retrieves the current state, `setState` updates state through an updater function, and `subscribe` subscribes to state changes. Three key design decisions: 1. **Immutable Updates**: `setState` accepts an updater function `(prev: T) => T`, requiring callers to return an entirely new state object. It compares old and new references using `Object.is` -- only when the reference changes are listeners notified. 2. **Generic `onChange` Callback**: Passed in when the Store is created, it is called with both old and new states on every state change. This is used in `AppStateProvider` to respond to external setting changes. 3. **Set-based Listener Management**: Uses `Set` instead of arrays for automatic deduplication; `subscribe` returns an unsubscribe function, following React's cleanup pattern. **Why Only 34 Lines? -- The "Less Is More" Philosophy of State Management** In today's frontend ecosystem, state management libraries are层出不穷 -- Redux, MobX, Recoil, Jotai, Valtio... each trying to solve different pain points. Claude Code's Store has chosen a minimalist path, which is not laziness but a precise grasp of requirements. The state management needs of an Agent CLI tool are fundamentally different from those of a complex Web application: | Dimension | Web Application | Agent CLI | |-----------|----------------|-----------| | State Change Frequency | Very High (millisecond-level UI interactions) | Medium (second-level tool calls) | | Concurrent Updates | Common (multiple users operating simultaneously) | Rare (single user, single session) | | Time-Travel Debugging | Valuable (tracing complex interactions) | Unnecessary | | Middleware Needs | High (async operations, side effects) | Low (primarily synchronous) | | Learning Curve Tolerance | Low (team collaboration) | Low (but for a different reason: keeping code concise) | A 34-line Store means: no reducer boilerplate, no action type definitions, no middleware configuration, no DevTools integration -- only the core get/set/subscribe. Every removed feature is a deliberate "no." **The Deeper Implications of Immutable Updates** The `Object.is` reference comparison has subtle semantics. It means: ```javascript // Will NOT trigger notification -- returns the same reference setState(prev => prev) // Will trigger notification -- returns a new reference setState(prev => ({ ...prev, count: prev.count + 1 })) // Will NOT trigger notification -- same value, but this is correct behavior setState(prev => ({ ...prev, count: prev.count })) // Note: In this case, a new object is created but nothing actually changed // Production code should check whether the value truly changed before creating a new object ``` This design delegates the judgment of "whether a change occurred" to the caller -- the caller expresses the semantics of "whether the state changed" through whether they return a new reference. This is a "convention over configuration" design style: the convention is "return new reference = state changed," rather than providing an explicit `hasChanged` callback. ### 5.4.2 AppState: Global State Type Definition The global state type `AppState` is marked as DeepImmutable, containing over 50 state fields that cover: - **Settings Layer**: `settings` (merged SettingsJson), `verbose`, `mainLoopModel` - **UI Layer**: `expandedView`, `footerSelection`, `statusLineText` - **Tool Permission Layer**: `toolPermissionContext` (current permission mode, allowed tools list, etc.) - **MCP Layer**: `mcp.clients`, `mcp.tools`, `mcp.commands` - **Plugin Layer**: `plugins.enabled`, `plugins.disabled`, `plugins.errors` - **Bridge Layer**: `replBridgeEnabled`, `replBridgeConnected`, and ten other bridge-related states - **Agent Layer**: `agentDefinitions`, `agentNameRegistry`, `teamContext` - **Speculative Execution Layer**: `speculation` (idle/active state machine) The default state is constructed by `getDefaultAppState()`, which loads the merged settings at startup and initializes all subsystems to safe default values. **DeepImmutable's Type-Level Guarantee** `AppState` uses the `DeepImmutable<T>` type marker, which means the TypeScript compiler prevents any attempt to directly modify state fields at compile time. This is a manifestation of the "make the right thing easy and the wrong thing impossible" design philosophy at the type system level. The categorization of state fields maps to the architectural layers of the Agent system: ```mermaid graph TD AppState["AppState<br/>Global State (DeepImmutable)"] AppState --> config["Settings Layer<br/>settings, verbose, mainLoopModel<br/>Determined at startup, rarely changes at runtime"] AppState --> permission["Permission Layer<br/>toolPermissionContext<br/>Changes dynamically with tool calls"] AppState --> integration["Integration Layer<br/>mcp.*, plugins.*<br/>Subsystem state, managed by respective initialization flows"] AppState --> communication["Communication Layer<br/>replBridge*, teamContext<br/>Cross-process/cross-Agent communication state"] AppState --> execution["Execution Layer<br/>speculation, agentDefinitions<br/>Dynamically created and destroyed at runtime"] classDef root fill:#e8eaf6,stroke:#3f51b5,stroke-width:3px,color:#1a237e classDef layer fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#e65100 class AppState root class config,permission,integration,communication,execution layer ``` > This layered structure implies a core architectural principle of the Agent system: state ownership. Each layer's state changes are driven by the corresponding layer's logic, while other layers only read. ### 5.4.3 AppStateProvider: React Context Wrapper `AppStateProvider` wraps the Store as a React Context. The Store is created only once (through `useState` lazy initialization), and the Provider itself does not re-render due to state changes. Consumers access state through two hooks: - **`useAppState(selector)`**: Uses `useSyncExternalStore` to subscribe to a state slice. It only triggers re-render when the selector's return value changes by `Object.is`. This is a fine-grained subscription mechanism that avoids the problem of "subscribing to the entire state tree causing the entire component tree to re-render." - **`useSetAppState()`**: Only retrieves the `setState` function without subscribing to any state. The returned reference never changes, so components using this hook do not re-render due to state changes. There is also a safe variant `useAppStateMaybeOutsideOfProvider` for components that may render outside of `AppStateProvider` -- it returns `undefined` instead of throwing an exception when there is no Provider. **The Significance of Choosing `useSyncExternalStore`** Claude Code chose React 18's `useSyncExternalStore` over a custom subscription implementation (like useEffect + useState). This choice is worth analyzing. `useSyncExternalStore` is React's official hook designed for "external state sources," providing three key guarantees: 1. **Consistency**: In concurrent mode, the state snapshot during rendering is consistent (no torn reads) 2. **Batched Updates**: Multiple `setState` calls trigger only one re-render 3. **Server Compatibility**: Supports SSR snapshot mode For an Agent system where state changes may trigger side effects like tool calls and file operations, consistency guarantees are particularly important. Imagine: if permission state were inconsistent during rendering, one component might think permission was granted while another thinks it wasn't, producing contradictory behavior. **Performance Optimization Pattern Comparison** ``` Pattern A: Subscribe to Entire State (Anti-Pattern) const state = useAppState(s => s); // Any field change triggers re-render Pattern B: Precisely Subscribe to Individual Fields (Recommended) const model = useAppState(s => s.mainLoopModel); const verbose = useAppState(s => s.verbose); // Only triggers re-render when corresponding fields change Pattern C: Write-Only, No Read (Recommended) const setState = useSetAppState(); // Never re-renders due to state changes // Suitable for components that only need to modify state but don't need to read it ``` > Best Practice: > > 1. Follow the "principle of minimum subscription" -- components should only subscribe to the portion of state they actually use > 2. Use `useSetAppState()` instead of `useAppState(s => s.setState)` -- the former does not subscribe to any state > 3. Avoid creating new objects in selectors -- `useAppState(s => ({ a: s.a, b: s.b }))` returns a new reference on every call, causing infinite re-renders. Instead, subscribe separately or use shallow comparison > Cross-Reference: The `mcp.*` fields in AppState are directly related to Chapter 9 (MCP Tool System); `toolPermissionContext` is related to the permission system; `speculation` state machine is related to the speculative execution architecture. Understanding AppState's structure is key to understanding how subsystems collaborate. --- ## Practical Exercises ### Exercise 1: Configuration Merge Prediction Assume the following configurations exist: - `~/.claude/settings.json`: `{ "permissions": { "allow": ["Bash(ls)"] }, "model": "sonnet" }` - `.claude/settings.json`: `{ "permissions": { "allow": ["Read(*)"] }, "hooks": { "Stop": [...] } }` - `.claude/settings.local.json`: `{ "permissions": { "allow": ["Bash(git *)"] } }` Predict the merged `permissions.allow` and `model` values. **Reference Answer**: `permissions.allow` is `["Bash(ls)", "Read(*)", "Bash(git *)"]` (array concatenation with deduplication), `model` is `"sonnet"` (no higher-priority override). **Extended Thinking**: If you now pass `{ "model": "opus" }` via CLI parameter `--settings`, what would the `model` value become? If the enterprise policy sets `"model": "haiku"`, what would the final value be? ### Exercise 2: Security Boundary Analysis If you are an enterprise administrator and want to ensure: (1) All users can only use administrator-approved hooks; (2) Users cannot self-install MCP servers. Which fields should you set in `managed-settings.json`? **Reference Answer**: Set `allowManagedHooksOnly: true` and `strictPluginOnlyCustomization: ["mcp"]`. **Extended Thinking**: If, in addition to the above requirements, you also want to ensure team members cannot override the model setting, how should you configure it? Hint: Consider whether the `model` field can be directly locked in policySettings. ### Exercise 3: State Subscription Optimization A component needs to display the current model name and the verbose flag. Which of the following two approaches is better? - **Approach A**: Subscribe to the entire state object via `useAppState` - **Approach B**: Precisely subscribe to `mainLoopModel` and `verbose` fields separately via `useAppState` **Reference Answer**: Approach B is better. Approach A subscribes to the entire state tree, and any state change will trigger a re-render. Approach B precisely subscribes to two fields, only triggering re-render when those two fields change. **Extended Thinking**: If a component needs to display the boolean value "whether the current model is Opus," which of the following approaches is better? - Approach C: `useAppState(s => s.mainLoopModel)` and then determine in the component - Approach D: `useAppState(s => s.mainLoopModel?.includes('opus'))` ### Exercise 4: Configuration Strategy Design Design a configuration strategy for a 20-person development team with the following requirements: - The team uniformly uses a specified model and permission baseline - Each developer can customize UI preferences and local permission extensions - The CI/CD environment follows the principle of least privilege Specify what content should be placed in each configuration source and explain why. --- ## Key Takeaways 1. **Six-Layer Priority Chain**: pluginSettings -> userSettings -> projectSettings -> localSettings -> flagSettings -> policySettings, where the latter overrides the former. Understanding the role of each layer is the foundation for designing sound configuration strategies. 2. **Merge Strategy**: Arrays are concatenated and deduplicated, objects are deeply merged, scalars are directly overridden. The array concatenation design ensures that permission rules are only ever added, never accidentally removed. 3. **Security Boundaries**: `projectSettings` is excluded in all security-sensitive checks to prevent supply chain attacks from malicious repositories. This is a direct manifestation of the "decreasing trust radius" principle. 4. **Special Merge for the Policy Layer**: policySettings uses "first non-empty source wins" instead of deep merge, ensuring the determinism and auditability of enterprise policies. 5. **Dual Feature Flags**: `feature()` provides compile-time dead code elimination (zero runtime overhead), while GrowthBook provides runtime experiment control (flexible but cache-dependent). The choice between the two depends on whether the feature needs to be dynamically adjusted after release. 6. **Immutable State**: The Store uses `Object.is` reference comparison to enforce immutable update patterns, combined with React's `useSyncExternalStore` for fine-grained subscription rendering. 34 lines of code implement all necessary state management capabilities. 7. **DeepImmutable Types**: Prevents direct state modification at the type system level, making the right thing easy and the wrong thing impossible.

claude-code-book - 附录 A 源码导航地图

16631 characters

# 附录 A:架构导航地图 本附录为 Claude Code 系统架构的概念性导航,帮助读者从宏观层面理解各功能模块的职责与协作关系。无论你是想快速定位某个功能的架构归属,还是追踪一条完整的数据流路径,本地图都是你的首选参考。 **如何使用本地图**: - 如果你初次阅读,建议按 A.1 -> A.2 -> A.3 的顺序通读,建立整体认知 - 如果你想查找某个具体模块,直接在 A.1 索引表中定位,再跳转到相关小节 - 如果你想理解某个功能的数据流,在 A.3 中查找对应的流程图 - 如果你关注模块间的协作契约,参考 A.4 的接口说明 - 本地图中的模块名称与本书正文各章节紧密对应,可交叉参阅 --- ## A.1 核心模块索引表 下表列出 Claude Code 架构中的所有核心模块。每个模块包含职责描述、关键数据结构概述以及与正文章节的交叉引用。 | 模块名称 | 职责描述 | 关键数据结构 | 章节引用 | |---------|---------|------------|---------| | CLI 入口与 REPL | 命令行入口点,负责参数解析、REPL 交互循环的渲染与会话初始化。是整个系统的启动锚点,解析命令行标志后委派给对话主循环 | 命令行参数对象、REPL 渲染状态树 | 第 2 章 | | 对话主循环 | 基于 turn 的对话循环,管理消息收发与工具调度的核心调度器。是系统的心脏,每个 turn 代表一轮"用户输入 -> 模型推理 -> 工具执行 -> 结果回填"的完整迭代 | messages 数组(Message[])、turn 计数器、stop reason 状态 | 第 2 章 | | 查询引擎 | 封装 API 调用、流式响应处理、重试逻辑的底层引擎。管理与 Anthropic Messages API 的所有通信,包括 system prompt 组装、token 计数和缓存策略 | API 请求配置、流式响应块(ContentBlock[])、缓存断点标记 | 第 2 章、第 13 章 | | 工具类型系统 | 工具的泛型接口定义、执行上下文与工具工厂函数。定义了所有工具必须遵循的标准协议,包括输入验证、权限检查、并发安全等维度 | 工具接口定义(Tool interface)、Zod 输入 schema、工具描述模板 | 第 3 章 | | 工具注册表 | 全局工具注册、发现与组装,负责将内置工具与动态工具合并。运行时通过工具池组装函数将内置工具与 MCP 动态工具合并,按名称排序后去重 | 工具映射表(Map<name, Tool>)、已注册工具列表 | 第 3 章 | | 子智能体系统 | 子智能体的生成、恢复、分叉(fork)及内置智能体定义。实现了智能体的递归组合能力,允许一个智能体在执行过程中派生出独立上下文的子任务执行者 | 子智能体配置、fork 状态快照、智能体恢复上下文 | 第 9 章、第 8 章 | | Shell 执行引擎 | Shell 命令的权限校验、只读检测与安全沙箱执行。将 Shell 命令的执行抽象为统一的工具接口,包含命令注入防护、超时管理和输出截断 | 命令执行请求、输出流(stdout/stderr)、退出状态码 | 第 3 章、第 4 章 | | 上下文压缩 | auto-compact、micro-compact、session memory 等多层压缩策略。在上下文接近 token 上限时自动触发,确保对话不会因窗口溢出而中断 | 压缩摘要消息、token 使用量计数、压缩阈值配置 | 第 7 章 | | 钩子系统 | Pre/Post tool use hooks、session hooks 及异步钩子的注册与执行。提供了贯穿整个生命周期的扩展点机制,允许用户在关键事件节点注入自定义逻辑 | 钩子注册表、钩子执行上下文、钩子输出结果 | 第 8 章 | | 设置系统 | 全局/项目/本地三级配置、权限规则定义与 schema 验证。采用层级叠加模型,确保配置的灵活性与可审计性 | 三级配置对象(Settings)、权限规则数组、Zod 验证 schema | 第 5 章 | | 记忆系统 | CLAUDE.md 发现、嵌套记忆、团队记忆与相关性匹配。实现了智能体跨会话的知识持久化,通过层次化的记忆文件系统保留用户偏好和项目知识 | 记忆文件(CLAUDE.md)、记忆层级树、相关性匹配评分 | 第 6 章 | | 技能系统 | 内置技能管理、动态加载与 slash command 注册。技能是可安装的扩展能力包,通过提示词模板和工具定义扩展智能体在特定领域的专业能力 | 技能注册表、slash command 映射、技能提示词模板 | 第 11 章 | | MCP 集成 | MCP 连接管理、协议适配、资源读写与权限通道。实现了 Model Context Protocol 的完整客户端,允许外部工具服务器向模型提供上下文和可调用工具 | MCP 连接配置、资源描述符、工具能力声明 | 第 12 章 | | IDE 桥接 | VSCode/JetBrains 双向通信、JWT 认证与远程会话管理。在 Claude Code CLI 与 IDE 插件之间建立安全的双向通信管道 | 桥接消息队列、JWT token、IDE 状态快照 | 第 7 章 | | 协调器模式 | 多智能体协调场景下的 worker 分配与结果汇总。在多智能体协作中充当中枢角色,负责任务分发、进度跟踪和结果整合 | worker 注册表、任务队列、结果汇总状态 | 第 10 章 | | 状态管理 | 全局应用状态 store、React 集成与 selector 模式。采用中心化的状态管理策略,通过 selector 模式实现高效的状态订阅和更新 | 全局状态树(AppState)、selector 函数、状态更新事件 | 第 2 章、第 13 章 | > **导航提示**:上表中的"章节引用"列指向本书正文中对该模块有深入讨论的章节,建议结合正文阅读以获得完整理解。 --- ## A.2 模块依赖关系 Claude Code 的核心依赖链如下(自上而下): ```mermaid flowchart TD CLI["CLI 入口层<br/>参数解析 + REPL 初始化"] CLI --> MainLoop["对话主循环<br/>turn-based 消息循环"] MainLoop --> QueryEngine["查询引擎<br/>API 调用 + 流式响应"] MainLoop --> ToolRegistry["工具注册表<br/>工具发现与路由"] ToolRegistry --> ToolTypeSystem["工具类型系统<br/>工具接口与工厂"] ToolRegistry --> ToolImpls["各工具实现<br/>按功能域划分"] ToolImpls --> SubAgent["子智能体<br/>递归调用对话主循环"] ToolImpls --> ShellExec["Shell 执行"] ToolImpls --> FileEdit["文件编辑"] MainLoop --> Compact["上下文压缩服务<br/>被对话主循环调用"] MainLoop --> Hooks["钩子系统<br/>拦截工具调用前后"] MainLoop --> AppState["全局状态<br/>AppState store"] MainLoop --> Memory["记忆系统<br/>CLAUDE.md 注入"] MainLoop --> Skills["技能系统<br/>技能调用"] MainLoop --> MCP["MCP 服务<br/>动态工具注入"] MainLoop --> IDE["IDE 桥接<br/>外部通信"] MainLoop --> Coordinator["多智能体协调模式<br/>协调器模式"] classDef core fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef infra fill:#7eb8da,stroke:#4a90d9,color:#fff classDef tool fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef service fill:#dda0dd,stroke:#9a5c9a,color:#fff class CLI,MainLoop core class QueryEngine,AppState infra class ToolRegistry,ToolTypeSystem,ToolImpls,SubAgent,ShellExec,FileEdit tool class Compact,Hooks,Memory,Skills,MCP,IDE,Coordinator service ``` ### A.2.1 分层架构说明 Claude Code 的架构可以清晰地划分为四个层次,每一层都有明确的职责边界和依赖方向: ```mermaid flowchart TD subgraph 表现层["表现层 Presentation Layer"] direction LR CLI["CLI 入口"] REPL["REPL 渲染<br/>Ink / React"] end subgraph 编排层["编排层 Orchestration Layer"] direction LR ML["对话主循环<br/>turn 调度枢纽"] SM["状态管理<br/>AppState store"] end subgraph 能力层["能力层 Capability Layer"] direction LR Tools["工具实现<br/>File/Bash/Grep/..."] SA["子智能体系统<br/>递归组合"] Skill["技能系统<br/>领域扩展"] end subgraph 基础设施层["基础设施层 Infrastructure Layer"] direction LR QE["查询引擎"] Shell["Shell 执行"] MCP_I["MCP 集成"] IDE_I["IDE 桥接"] Settings["设置系统"] Mem["记忆系统"] Hooks_I["钩子系统"] end 表现层 --> 编排层 编排层 --> 能力层 编排层 --> 基础设施层 能力层 --> 基础设施层 classDef presentation fill:#ff9800,stroke:#e65100,color:#fff classDef orchestration fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef capability fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef infra fill:#ce93d8,stroke:#7b1fa2,color:#fff class CLI,REPL presentation class ML,SM orchestration class Tools,SA,Skill capability class QE,Shell,MCP_I,IDE_I,Settings,Mem,Hooks_I infra ``` **表现层(Presentation Layer)** - 包含 CLI 入口与 REPL 渲染模块 - 使用 Ink 框架(基于 React)将组件树渲染为终端文本输出 - 负责用户输入捕获、输出渲染、快捷键处理、主题切换等 UI 相关职责 - 不包含任何业务逻辑,所有用户操作都委派给下一层 - 依赖方向:仅向下依赖编排层 **编排层(Orchestration Layer)** - 包含对话主循环和状态管理两大核心模块 - 对话主循环是整个系统的调度枢纽,协调查询引擎、工具注册表、压缩服务之间的协作 - 状态管理模块维护全局应用状态树,通过 selector 模式向表现层提供响应式的状态订阅 - 这一层的核心不变量是 turn 的完整性:每个 turn 必须走完"模型调用 -> 工具执行 -> 结果回填 -> 再次调用"的完整闭环 - 依赖方向:向下依赖能力层和基础设施层 **能力层(Capability Layer)** - 包含所有工具实现、子智能体系统、技能系统 - 每个工具都是独立的能力单元,通过统一的工具接口与编排层对接 - 子智能体系统通过递归调用编排层的对话主循环来实现嵌套执行 - 技能系统作为更高级的能力扩展机制,可以组合多个工具形成领域专用的能力包 - 依赖方向:向下依赖基础设施层 **基础设施层(Infrastructure Layer)** - 包含查询引擎、Shell 执行引擎、MCP 集成、IDE 桥接、设置系统、记忆系统、钩子系统 - 这一层的模块提供最底层的技术能力,不包含业务逻辑 - 查询引擎封装了与 Anthropic API 的所有通信细节 - 设置系统提供三级配置的存储和验证能力 - 记忆系统提供 CLAUDE.md 文件的发现和注入能力 - 依赖方向:不依赖上层,可被任何上层模块调用 ### A.2.2 核心循环与数据驱动模型 **核心循环**:CLI 入口初始化 REPL -> 用户输入触发对话主循环的 turn 循环 -> 查询引擎发起 API 调用 -> 模型返回 tool_use -> 工具注册表路由到具体工具 -> 工具执行后结果回填 -> 再次调用 API,直到模型输出结束(stop reason = "end_turn")。 核心循环体现了 Claude Code 的基本设计哲学:**数据驱动的循环模型**。系统不使用传统的命令式流程控制,而是通过消息数组的不断追加来驱动执行。每个 turn 的输入是一个不断增长的 messages 数组,模型的每次推理都基于这个数组的完整内容做出决策。 ```mermaid flowchart LR subgraph 核心循环["核心循环:数据驱动模型"] direction LR Input["用户输入"] --> Append["追加到<br/>messages[]"] Append --> API["查询引擎<br/>API 调用"] API --> Response["流式响应<br/>text / tool_use"] Response --> ToolExec["工具执行<br/>结果回填"] ToolExec --> Append end subgraph 横切关注点["横切关注点"] direction LR HC["钩子系统<br/>工具调用前后拦截"] PC["权限系统<br/>安全护栏"] CC["压缩系统<br/>上下文窗口管理"] MC["记忆系统<br/>CLAUDE.md 注入"] end HC -.- ToolExec PC -.- ToolExec CC -.- Append MC -.- API classDef core fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef cross fill:#dda0dd,stroke:#9a5c9a,color:#fff class Input,Append,API,Response,ToolExec core class HC,PC,CC,MC cross ``` **横切关注点**: - **钩子系统**(hooks)在工具调用前后插入拦截逻辑,允许用户在不修改核心代码的情况下扩展行为。参见第 8 章的详细讨论 - **权限系统**通过统一的权限检查函数决定是否允许执行,形成了贯穿所有工具执行的安全护栏。参见第 4 章的权限管线详解 - **压缩系统**在上下文接近 token 上限时自动触发,是对 context window 这一根本硬件约束的工程应对。参见第 7 章的上下文管理策略 - **记忆系统**在每轮开始时扫描 CLAUDE.md 并注入系统提示,实现了跨会话的知识持久化。参见第 6 章的记忆架构 ### A.2.3 模块间耦合度分析 理解模块间的耦合程度有助于读者在阅读源码或进行扩展开发时把握正确的切入点: | 耦合关系 | 耦合程度 | 说明 | |---------|---------|------| | 对话主循环 <-> 查询引擎 | 紧耦合 | 对话主循环直接依赖查询引擎的流式输出接口,两者共享消息数组的数据模型 | | 对话主循环 <-> 工具注册表 | 紧耦合 | 工具调度是对话主循环的核心职责之一,工具路由逻辑嵌入在主循环的 turn 处理中 | | 工具注册表 <-> MCP 集成 | 松耦合 | MCP 工具通过动态注册机制接入,运行时按需发现和加载 | | 子智能体 <-> 对话主循环 | 递归耦合 | 子智能体通过递归调用对话主循环来实现嵌套执行,形成自相似的分形结构 | | 钩子系统 <-> 工具执行 | 事件耦合 | 钩子通过生命周期事件触发,不影响工具执行的核心路径 | | 记忆系统 <-> 查询引擎 | 松耦合 | 记忆内容作为系统提示的一部分注入,不直接参与 API 调用逻辑 | | 设置系统 <-> 全局 | 配置耦合 | 设置系统通过配置对象影响几乎所有模块的行为,但模块间无直接依赖 | --- ## A.3 数据流路径速查 本节以流程图的形式展示 Claude Code 中几个关键操作的完整数据流。每个流程图都标注了经过的核心模块和关键决策点,帮助读者快速追踪数据在系统中的流转路径。 **快速导航**: - [标准工具调用流程](#标准工具调用流程) -- 最核心的循环,理解 Claude Code 运行机制的起点 - [权限判定路径](#权限判定路径) -- 安全护栏的核心决策链 - [上下文压缩触发路径](#上下文压缩触发路径) -- 长对话的生命线 - [记忆注入路径](#记忆注入路径) -- 跨会话知识如何进入对话 - [MCP 工具动态注册路径](#mcp-工具动态注册路径) -- 外部能力如何接入系统 - [子智能体 Fork 执行路径](#子智能体-fork-执行路径) -- 任务的递归分解 ### 标准工具调用流程 这是 Claude Code 最核心的数据流路径,描述了一个完整的 turn 中数据从用户输入到最终输出的流转过程。所有其他路径都是这个核心循环的变体或子集。 ```mermaid flowchart TD A["[1] 用户输入"] B["[2] 对话主循环:processUserMessage()<br/>追加用户消息到 messages[]<br/>注入 CLAUDE.md 记忆内容"] C["[3] 查询引擎:queryAPI()<br/>组装 system prompt + messages + tools<br/>调用 Anthropic Messages API(流式)"] D["[4] 流式响应处理<br/>逐步接收 text 和 tool_use blocks<br/>渲染到 REPL 终端"] E{"[5] 工具路由<br/>查找工具实例 / Zod schema 验证<br/>权限检查"} F["[6] 工具执行<br/>PreToolUse 钩子 → 工具实际执行 → PostToolUse 钩子"] G["[7] 结果回填<br/>tool_result 追加到 messages[]<br/>大结果持久化到磁盘"] H{"[8] 循环判断<br/>stop_reason === end_turn ?"} I["[9] turn 结束<br/>输出最终文本响应<br/>检查是否需要 auto-compact"] J["[10] 等待下一轮用户输入"] A --> B --> C --> D --> E E -->|tool_use| F --> G --> H H -->|否,继续循环| C H -->|是,结束| I --> J classDef user fill:#ff9800,stroke:#e65100,color:#fff classDef core fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef decision fill:#ffeb3b,stroke:#f9a825,color:#333 classDef action fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef endpoint fill:#ce93d8,stroke:#7b1fa2,color:#fff class A,J user class B,C,D,G core class E,H decision class F action class I endpoint ``` **关键决策点说明**: - 步骤 [5] 的权限检查是系统中最重要的安全决策点,决定了工具是否被执行 - 步骤 [7] 的大结果持久化机制防止了 token 窗口被单个工具结果撑满 - 步骤 [8] 的循环条件(stop_reason)是整个系统从"无限循环"中退出的唯一机制 ### 权限判定路径 权限管线是 Claude Code 安全模型的核心。每个工具调用都必须通过这条完整的决策链才能被执行。权限判定的结果决定了工具是否执行、是否需要用户确认、或者是否直接拒绝。 ```mermaid flowchart TD A["工具调用请求"] B["[1] 输入合法性校验<br/>Zod schema 验证"] C{"验证是否通过?"} D["[2] 工具级权限逻辑检查<br/>checkPermissions() 方法<br/>返回 allow / deny / ask"] E["[3] PreToolUse 钩子<br/>用户自定义钩子<br/>可继续/修改参数/阻止"] F["[4] 通用权限系统检查<br/>alwaysAllow / alwaysDeny / alwaysAsk<br/>权限模式检查"] G{"[5] 分类器辅助决策<br/>Transcript Classifier / Bash Classifier"} H["执行工具"] I["拒绝执行"] J["[6] PostToolUse 钩子<br/>执行后钩子<br/>日志记录/结果验证"] A --> B --> C C -->|失败| I C -->|通过| D --> E --> F --> G G -->|allow| H --> J G -->|deny| I classDef start fill:#ff9800,stroke:#e65100,color:#fff classDef check fill:#ffeb3b,stroke:#f9a825,color:#333 classDef hook fill:#ce93d8,stroke:#7b1fa2,color:#fff classDef result fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef reject fill:#ef5350,stroke:#c62828,color:#fff class A start class B,D,F check class C,G check class E,J hook class H result class I reject ``` **权限模式的互动关系**: | 权限模式 | 工具执行策略 | 用户体验 | |---------|------------|---------| | ask(默认) | 所有写操作都需要用户确认 | 最安全,但交互频繁 | | auto-edit | 文件编辑自动放行,其他写操作仍需确认 | 平衡安全与效率 | | full-auto | 所有操作自动执行(受 alwaysDeny 规则约束) | 最流畅,但风险最高 | | plan | 仅允许只读工具,进入纯规划模式 | 用于安全审查任务方案 | ### 上下文压缩触发路径 上下文压缩是 Claude Code 应对 context window 限制的核心工程策略。当对话历史接近 token 上限时,系统会自动触发压缩以释放空间。 ```mermaid flowchart TD A["对话主循环 turn 结束"] B{"[1] 检查 token 使用量<br/>是否接近阈值(80%-90%)?"} C["[2] 触发压缩策略选择"] C1["micro-compact<br/>轻量级压缩,快速缩减"] C2["auto-compact(标准)<br/>完整压缩,API 生成摘要"] C3["history snip<br/>智能裁剪已处理历史"] C4["context collapse<br/>最激进折叠(实验性)"] D["[3] 执行压缩<br/>分组历史消息为压缩单元<br/>调用 API 生成摘要<br/>替换原始消息为摘要消息"] E["[4] 压缩后处理<br/>验证 token 使用量在安全范围<br/>更新 messages[] 数组"] F["[5] 恢复对话主循环<br/>压缩后的 messages[] 作为后续输入"] A --> B B -->|未接近阈值| F B -->|接近阈值| C C --> C1 --> D C --> C2 --> D C --> C3 --> D C --> C4 --> D D --> E --> F classDef trigger fill:#ff9800,stroke:#e65100,color:#fff classDef decision fill:#ffeb3b,stroke:#f9a825,color:#333 classDef strategy fill:#ce93d8,stroke:#7b1fa2,color:#fff classDef action fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef done fill:#8fbc8f,stroke:#5a8a5a,color:#fff class A trigger class B decision class C,C1,C2,C3,C4 strategy class D,E action class F done ``` **压缩策略对比**: | 策略 | 触发条件 | 压缩力度 | 质量损失 | 缓存友好 | |------|---------|---------|---------|---------| | micro-compact | 接近阈值时预防性触发 | 轻度 | 低 | 是(若启用 CACHED_MICROCOMPACT) | | auto-compact | 达到阈值时触发 | 中度 | 中 | 取决于压缩范围 | | history snip | 持续执行 | 中高度 | 中高 | 否 | | context collapse | 手动或紧急触发 | 极高 | 高 | 否 | ### 记忆注入路径 记忆系统在每轮对话开始时将 CLAUDE.md 文件中的知识注入到对话上下文中,这是智能体实现跨会话知识持久化的关键路径。 ```mermaid flowchart TD A["[1] 会话启动 / turn 开始"] B["[2] 记忆发现<br/>逐级查找 CLAUDE.md 文件<br/>全局 / 项目级 / 目录级 / 团队"] C["[3] 相关性匹配<br/>对记忆片段进行相关性评分<br/>评估与当前上下文的匹配度"] D["[4] 记忆注入<br/>将选中记忆注入到系统提示中<br/>按优先级排序,超出预算则截断"] E["[5] 记忆生效<br/>注入内容在当前 turn 的模型推理中生效"] A --> B --> C --> D --> E classDef start fill:#ff9800,stroke:#e65100,color:#fff classDef discover fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef match fill:#ce93d8,stroke:#7b1fa2,color:#fff classDef inject fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef done fill:#66bb6a,stroke:#2e7d32,color:#fff class A start class B discover class C match class D inject class E done ``` ### MCP 工具动态注册路径 MCP(Model Context Protocol)允许外部工具服务器动态地向 Claude Code 注册新工具,这是系统扩展性的重要机制。 ```mermaid flowchart TD A["[1] MCP 连接初始化<br/>读取 MCP 服务器列表<br/>建立独立连接 / 协议握手"] B["[2] 工具发现<br/>查询服务器工具列表<br/>获取输入 schema 和描述<br/>适配为内部工具接口"] C["[3] 工具注册<br/>将 MCP 工具加入工具注册表<br/>与内置工具合并、排序、去重<br/>内置工具在同名冲突时优先"] D["[4] 运行时调用<br/>tool_use 走相同路由流程<br/>通过协议通道转发到 MCP 服务器<br/>结果回填到消息数组"] E["[5] 连接生命周期管理<br/>断线自动重连 / 会话结束清理资源"] A --> B --> C --> D --> E classDef conn fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef discover fill:#7eb8da,stroke:#4a90d9,color:#fff classDef register fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef runtime fill:#ff9800,stroke:#e65100,color:#fff classDef lifecycle fill:#ce93d8,stroke:#7b1fa2,color:#fff class A conn class B discover class C register class D runtime class E lifecycle ``` ### 子智能体 Fork 执行路径 子智能体通过 fork 机制创建独立执行单元,每个子智能体拥有自己的上下文和权限范围,但可以继承父智能体的关键配置。 ```mermaid flowchart TD A["[1] 父智能体决定 fork<br/>模型决定创建子智能体处理子任务<br/>通过 AgentTool 触发 fork"] B["[2] Fork 上下文创建<br/>复制消息历史作为初始上下文<br/>继承权限规则和设置配置<br/>子智能体获得独立 messages 数组"] C["[3] 子智能体独立执行<br/>递归调用对话主循环<br/>独立上下文中执行工具调用<br/>支持嵌套 fork(受深度限制)"] D["[4] 结果回收<br/>子智能体结果回填到父消息数组<br/>Fork 创建的临时资源被清理"] E["[5] 恢复父智能体执行<br/>从 fork 点继续执行<br/>子智能体上下文释放 token 预算"] A --> B --> C --> D --> E classDef trigger fill:#ff9800,stroke:#e65100,color:#fff classDef context fill:#4a90d9,stroke:#2c5f8a,color:#fff classDef exec fill:#8fbc8f,stroke:#5a8a5a,color:#fff classDef collect fill:#ce93d8,stroke:#7b1fa2,color:#fff classDef done fill:#66bb6a,stroke:#2e7d32,color:#fff class A trigger class B context class C exec class D collect class E done ``` --- ## A.4 模块接口契约概述 本节从概念层面描述核心模块之间的接口契约,帮助读者理解模块间如何通过明确的接口进行协作。注意,此处描述的是架构设计模式,而非具体源码引用。 ### A.4.1 对话主循环与查询引擎的契约 对话主循环通过查询引擎与 Anthropic Messages API 交互。两者之间的核心契约是: - **输入**:system prompt、messages 数组、可用工具列表、模型配置参数 - **输出**:流式的内容块序列(text 或 tool_use),每个块都附带元数据 - **保证**:查询引擎负责处理网络重试、流式解析、错误恢复;对话主循环只关心内容块的语义 ### A.4.2 对话主循环与工具注册表的契约 对话主循环通过工具注册表将模型返回的 tool_use 路由到具体的工具实现。核心契约是: - **路由接口**:给定工具名称,返回对应的工具实例 - **执行接口**:给定工具实例和输入参数,执行工具并返回结构化结果 - **权限接口**:给定工具实例和输入参数,返回权限判定结果 ### A.4.3 工具类型系统的标准协议 所有工具(包括内置工具和 MCP 动态工具)都必须遵循工具类型系统定义的标准协议。核心协议方法包括: - **isEnabled()**:判断工具是否在当前上下文中启用 - **isReadOnly()**:判断工具是否仅执行读取操作 - **isConcurrencySafe()**:判断工具是否可安全并行执行 - **isDestructive()**:判断工具是否执行不可逆操作 - **checkPermissions()**:执行权限检查,返回 allow/deny/ask - **toAutoClassifierInput()**:生成用于自动分类的特征描述 - **userFacingName()**:返回面向用户的友好名称 ### A.4.4 钩子系统的生命周期契约 钩子系统定义了三个核心生命周期扩展点: - **PreToolUse**:在工具执行前触发,可以修改输入参数或阻止执行 - **PostToolUse**:在工具执行后触发,可以处理执行结果或记录日志 - **Session Hooks**:在会话级别的事件(如会话开始、会话结束)触发 每个钩子都接收结构化的上下文对象,包含事件类型、工具信息、输入输出数据等。 --- ## A.5 架构设计模式速览 Claude Code 的架构中体现了多种经典设计模式,理解这些模式有助于读者更深入地把握系统的设计意图。 | 设计模式 | 应用位置 | 设计意图 | |---------|---------|---------| | Agent Loop(智能体循环) | 对话主循环 | 将 LLM 的推理能力与工具执行结合为迭代循环,直到任务完成 | | Factory Method(工厂方法) | 工具类型系统 | 通过统一工厂函数创建工具实例,确保所有工具遵循相同接口 | | Registry(注册表模式) | 工具注册表、技能注册表 | 通过名称注册和查找能力单元,支持运行时动态扩展 | | Plugin(插件模式) | MCP 集成、技能系统 | 允许外部模块在不修改核心代码的情况下扩展系统能力 | | Observer(观察者模式) | 钩子系统 | 在生命周期事件上注册观察者,实现松耦合的横切关注点 | | Strategy(策略模式) | 上下文压缩 | 多种压缩策略可互换使用,根据场景选择最优策略 | | Layered Architecture(分层架构) | 整体系统 | 表现层 -> 编排层 -> 能力层 -> 基础设施层的清晰分层 | | Async Generator(异步生成器模式) | 查询引擎、流式输出 | 使用 async function* 逐步产出流式结果,天然支持背压控制 | | Hierarchical Agent(层级智能体) | 子智能体系统 | 通过 fork 实现智能体的层级组合,形成分形结构 | | Feature Flag(功能标志模式) | 全系统 | 编译时注入布尔开关,实现死代码消除和渐进式功能发布 |

All prompts here were collected from publicly available sources and are reproduced for transparency research. Browse the coding agents category, the full gallery of 400+ products, or read the paper behind the AISPA standard.