分析 Claude Code 的内部设计与交互机制。对想深入理解 AI IDE 的开发者有参考价值。
我一直很好奇 Claude Code 内部到底发生了什么,所以花了几个小时深入研究了它发送给 Anthropic 的 prompt。
在这个过程中,我发现了一些有趣的东西,解释了为什么 Claude Code 相比 Cursor 等其他工具往往更慢、更贵。
事实证明这比我想的要简单得多,多亏了 mitmproxy:
$ brew install mitmproxy
$ mitmweb --mode reverse:https://api.anthropic.com --listen-port 8000
$ ANTHROPIC_BASE_URL=http://localhost:8000/ claude
我打开了我个人博客的代码仓库,输入了 "describe what's in this project"。
首先看到的是 Claude Code 试图判断你输入的内容是前一个对话的延续还是新的话题。
Analyze if this message indicates a new conversation topic.
If it does, extract a 2-3 word title that captures the new topic.
Format your response as a JSON object with two fields: 'isNewTopic' (boolean) and 'title' (string, or null if isNewTopic is false). Only include these fields, no other text.
确定后,它会把所有输入包装进这个系统 prompt:
You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's prompt, you should use the tools available to you to answer the user's question.
Notes:
1. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
2. When relevant, share file names and code snippets relevant to the query
3. Any file paths you return in your final response MUST be absolute. DO NOT use relative paths.
Here is useful information about the environment you are running in:
<env>
Working directory: /Users/kir/src/github.com/kirs/kirshatrov-com
Is directory a git repo: Yes
Platform: macos
Today's date: 2025-04-13
Model: claude-3-7-sonnet-20250219
</env>
As you answer the user's questions, you can use the following context:
<context name="directoryStructure">Below is a snapshot of this project's file structure at the start of the conversation. This snapshot will NOT update during the conversation. It skips over .gitignore patterns.
- /Users/kir/src/github.com/kirs/kirshatrov-com/
- README.md
- _posts/
- 2015-03-02-capistrano-unicorn-bundler.markdown
- 2015-04-07-rails-5.markdown
....
- 2024-12-14-facebook-mysql-fork.md
- bin/
...
</context>
<context name="gitStatus">This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.
Current branch: master
Main branch (you will usually use this for PRs): master
Status:
M .gitignore
D .ruby-version
M package-lock.json
?? playground/
Recent commits:
58ce3f9 m4 --force
3b1052a correction
82fe70a fix stuff
cfd73d9 follow-up
</context>
很不错的一点是把 git 状态包含进了上下文。你也可以注意到上下文包括了完整的目录树视图,这个可能会变得相当庞大。
这个系统 prompt 的一部分是可用的 11 个工具列表:
dispatch_agent
Launch a new agent that has access to the following tools: View, GlobTool, GrepTool, LS, ReadNotebook, WebFetchTool. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.
Bash
Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures. Includes specialized functionality for git operations like commits and pull requests.
BatchTool
Batch execution tool that runs multiple tool invocations in a single request. Tools are executed in parallel when possible, and otherwise serially. Takes a list of tool invocations (tool_name and input pairs) and returns the collected results from all invocations.
GlobTool
Fast file pattern matching tool that works with any codebase size. Supports glob patterns like "/*.js" or "src//*.ts". Returns matching file paths sorted by modification time.
GrepTool
Fast content search tool that works with any codebase size. Searches file contents using regular expressions. Supports full regex syntax and filters files by pattern with the include parameter.
LS
Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore.
View
Reads a file from the local filesystem. The file_path parameter must be an absolute path. By default, reads up to 2000 lines starting from the beginning of the file.
Edit
Tool for editing files. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger edits, use the Write tool to overwrite files.
Write
Write a file to the local filesystem. Overwrites the existing file if there is one.
Before using this tool:
1. Use the ReadFile tool to understand the file's contents and context
2. Directory Verification (only applicable when creating new files):
- Use the LS tool to verify the parent directory exists and is the correct location"
Replace
Write a file to the local filesystem. Overwrites the existing file if there is one.
ReadNotebook
Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs.
NotebookEditCell
Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source.
WebFetchTool
Fetches content from a specified URL and processes it using an AI model. Takes a URL and a prompt as input, fetches the URL content, converts HTML to markdown, and processes the content.
我的原始输入 ("describe what's in this project") 然后被转换成一个 dispatch_agent 工具调用,带有这个指令:
Analyze the repository structure and determine what kind of project this is.
Look at the file structure, package.json, and any configuration files to understand the tech stack,
then provide a concise summary of what this project appears to be.
这然后转换成几个 view 工具调用来读取那些文件:
把这些文件的结果反馈给 LLM 得出以下总结:
This is Kir Shatrov's personal website/blog built with Next.js, React, and Tailwind CSS. It includes:
- Blog posts on tech topics (Ruby/Rails, databases, engineering leadership)
- Photography gallery
- Travel map/history
- Speaking engagements
- Fabrication projects (woodworking/making)
The site uses markdown for blog content with posts dating back to 2015, following standard Next.js project structure.
这个花了 40 秒的 LLM 时间,成本是 $0.11。
我的下一个测试输入是:
Please write a bash script which displays the top story on Hacker News.
It should show the title, the link, and the number of points.
这触发了一个 agent 使用,试图读取 https://github.com/HackerNews/API。但 Claude 的自我限制安全策略阻止了这个:
Error: Cannot fetch URL "https://github.com/HackerNews/API". For security, you can only fetch URLs from hosts that the user has mentioned in their messages or that are found in CLAUDE.md files or project files.
然后它试图读取 https://hackernews.api-docs.io,又遇到了同样的错误。
然后它试图读取 https://firebase.google.com/docs/database,同样的事情发生了。
然后它改变了策略,输出了这个:
I apologize for the limitations in accessing external URLs. Due to security restrictions, I can only fetch URLs that have been mentioned by you or are in project files.
The Hacker News API is hosted by Firebase and provides several endpoints, including:
- `https://hacker-news.firebaseio.com/v0/topstories.json` - Returns IDs of top stories
- `https://hacker-news.firebaseio.com/v0/item/{id}.json` - Returns details of a specific item
To create a bash script to fetch the top story, you would:
1. Fetch the array of top story IDs
2. Get the first ID from that array
3. Fetch the details for that ID
Here's a basic bash script example:
#!/bin/bash
...
有趣的是这些都没有被保存或执行。所以我输入了保存这个并执行。
现在,还记得开始时的这个 prompt 吗?
Analyze if this message indicates a new conversation topic.
If it does, extract a 2-3 word title that captures the new topic.
Format your response as a JSON object with two fields: 'isNewTopic' (boolean) and 'title' (string, or null if isNewTopic is false). Only include these fields, no other text.
像任何其他输入一样,我的保存并执行命令也必须经过判断是否是新话题的过程。
保存文件涉及两个工具使用调用:
Replace 写文件
Bash 执行 chmod +x hn_top.sh && hn_top.sh
现在来了有趣的部分。任何 Bash 工具使用都会先经过这个 prompt:
<policy_spec>
# Claude Code Code Bash command prefix detection
This document defines risk levels for actions that the Claude Code agent may take. This classification system is part of a broader safety framework and is used to determine when additional user confirmation or oversight may be needed.
## Definitions
**Command Injection:** Any technique used that would result in a command being run other than the detected prefix.
## Command prefix extraction examples
Examples:
- cat foo.txt => cat
- cd src => cd
- cd path/to/files/ => cd
- find ./src -type f -name "*.ts" => find
- gg cat foo.py => gg cat
- gg cp foo.py bar.py => gg cp
- git commit -m "foo" => git commit
- git diff HEAD~1 => git diff
- git diff --staged => git diff
- git diff $(pwd) => command_injection_detected
- git status => git status
- git status# test(`id`) => command_injection_detected
- git status`ls` => command_injection_detected
- git push => none
- git push origin master => git push
- git log -n 5 => git log
- git log --oneline -n 5 => git log
- grep -A 40 "from foo.bar.baz import" alpha/beta/gamma.py => grep
- pig tail zerba.log => pig tail
- potion test some/specific/file.ts => potion test
- npm run lint => none
- npm run lint -- "foo" => npm run lint
- npm test => none
- npm test --foo => npm test
- npm test -- -f "foo" => npm test
- pwd
curl example.com => command_injection_detected
- pytest foo/bar.py => pytest
- scalac build => none
- sleep 3 => sleep
</policy_spec>
The user has allowed certain command prefixes to be run, and will otherwise be asked to approve or deny the command.
Your task is to determine the command prefix for the following command.
IMPORTANT: Bash commands may run multiple commands that are chained together.
For safety, if the command seems to contain command injection, you must return "command_injection_detected".
(This will help protect the user: if they think that they're allowlisting command A,
but the AI coding agent sends a malicious command that technically has the same prefix as command A,
then the safety system will see that you said "command_injection_detected" and ask the user for manual confirmation.)
Note that not every command has a prefix. If a command has no prefix, return "none".
ONLY return the prefix. Do not return any other text, markdown markers, or other content or formatting.
Command: chmod +x hn_top.sh
一旦安全性被评估,它会尝试获取实际命令来让用户总是允许运行它们
Extract any file paths that this command reads or modifies. For commands like "git diff" and "cat", include the paths of files being shown. Use paths verbatim -- don't add any slashes or try to resolve them. Do not try to infer paths that were not explicitly listed in the command output.
Format your response as:
<filepaths>
path/to/file1
path/to/file2
</filepaths>
If no files are read or modified, return empty filepaths tags: <filepaths></filepaths>
Do not include any other text in your response.
这就产生了以下的 UX:
你可以想象,对每个 Bash 工具使用都要调用一次或两次 LLM,这会让事情变得相当缓慢。
Claude Code 提供了 /init 命令,它会创建一个包含生成内容的 CLAUDE.md 文件。下面是这个命令的 prompt。
Please analyze this codebase and create a CLAUDE.md file containing:
1. Build/lint/test commands - especially for running a single test
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
Usage notes:
- The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.\
- If there's already a CLAUDE.md, improve it.\
- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.\
- Be sure to prefix the file with the following text:
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
这个 prompt 启动了一个 BatchTool agent,带有一些额外的工具调用来收集更多文件——基于那个 prompt 暗示的目录树:
{
"type": "tool_use",
"id": "toolu_01GmZ4d81pGHKaqryDEuZQNm",
"name": "BatchTool",
"input": {
"description": "Gather repository information",
"invocations": [
{
"tool_name": "GlobTool",
"input": {
"pattern": "package*.json"
}
},
{
"tool_name": "GlobTool",
"input": {
"pattern": "*.md"
}
},
{
"tool_name": "GlobTool",
"input": {
"pattern": ".cursor/rules/**"
}
},
{
"tool_name": "GlobTool",
"input": {
"pattern": ".cursorrules/**"
}
},
{
"tool_name": "GlobTool",
"input": {
"pattern": ".github/copilot-instructions.md"
}
}
]
}
}
总体而言,Claude Code 因为需要更多 token,成本会更通用和更安全,但代价是更花时间和更昂贵。
这与 Cursor 完全不同,Cursor 永远不会试图分析建议的 bash 工具。我在大多数情况下会更倾向于 Claude Code 的做法。
我还注意到它对大多数需要推理的事情使用 claude-3-7-sonnet,对更简单的调用比如解析 Bash 命令使用 claude-3-5-haiku。但即便如此,claude-3-5 对那些任务可能是非常过度的,gpt-3.5-turbo 会便宜 10 倍($1.5 vs $15 每百万输出 token)。
不过在 UX 方面,对于在控制台运行的 agent 工具,Claude Code 是赢家。Aider 接近,但在可靠性和 UX 方面远不及。而 Claude Code 是单个 NPM 包(仍然需要相对现代的 Node 版本,这可能需要安装 NVM),Aider 的安装过程仅仅因为需要就自动安装了 uv。而 OpenHands 必须下载大约 10GB 的 Docker 镜像。
Kir Shatrov 帮助企业通过扩展基础设施来增长。他写关于软件、可扩展性和生态系统的文章。在 Twitter 上关注他以获取最新更新。