Claude Code 为什么更听话?源码级深度解析
这是 Claude Code 与其他 AI 编程工具最本质的区别。
每次工具调用都会经过权限检查,返回三种行为之一:
type PermissionBehavior = 'allow' | 'deny' | 'ask'
- allow
:直接执行 - deny
:拒绝执行,记录原因,继续 - ask
:弹窗让用户确认,用户可以选择 allow 或 deny -
这三种行为覆盖了所有场景:安全的操作直接放行,有风险的操作要么拒绝要么确认。
用户可以配置权限模式,不同场景用不同的松紧度:
const EXTERNAL_PERMISSION_MODES = [
'acceptEdits', // 直接接受所有编辑
'bypassPermissions', // 完全绕过权限检查,危险但高效
'default', // 默认模式
'dontAsk', // 不询问,直接 deny,任何触发 ask 的操作都会被转为 deny
'plan', // 只在 plan 模式下询问
] as const
// 如果开启了 TRANSCRIPT_CLASSIFIER feature gate,还有:
type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
每种模式对应一种使用场景:
|
|
|
|---|---|
default |
|
acceptEdits |
|
bypassPermissions |
|
dontAsk |
|
plan |
|
auto |
|
auto 模式是 Claude Code 最特别的地方。它内置了一个分类器,用 AI 来判断工具调用是否安全。 分类结果有三种置信度:
type ClassifierResult = {
matches: boolean
confidence: 'high' | 'medium' | 'low'
reason: string
}
Claude Code 是怎么判断置信度的? 整个判断流程分两个阶段: 第一阶段:Fast Path(快速通道) 如果判断”高置信度 allow”,直接返回,不弹窗也不记日志。这是日常操作的常见路径——常用命令(git status、npm install 等)已经被内置到白名单里,高置信度匹配,直接放行。 第二阶段:Thinking Stage(深度推理) 如果第一阶段没有得出高置信度结论,进入深度推理阶段。Claude 会分析更完整的上下文,包括:命令语义、工作目录、项目配置文件(.claude/ 目录)、用户之前的操作历史,来做更准确的判断。 置信度到行为的映射:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
日常操作不会被频繁打断,只有真正模糊的情况才会触发确认。
auto 模式有个隐藏的风险:如果 AI 一直说”这个不安全,我拒绝”,用户怎么知道发生了什么? Claude Code 用了 denialTracking 来解决:
// denialTracking.ts
export const DENIAL_LIMITS = {
maxConsecutive: 3, // 连续拒绝超过 3 次
maxTotal: 20, // 或累计拒绝超过 20 次
} as const
export function shouldFallbackToPrompting(state: DenialTrackingState): boolean {
return (
state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive ||
state.totalDenials >= DENIAL_LIMITS.maxTotal
)
}
逻辑很清晰:
-
连续拒绝 3 次 → 下次直接弹窗,不再自动拒绝 -
累计拒绝 20 次 → 彻底放弃 auto 模式,回退到弹窗
// 成功执行后,重置连续计数
export function recordSuccess(state: DenialTrackingState): DenialTrackingState {
if (state.consecutiveDenials === 0) return state
return {
...state,
consecutiveDenials: 0, // 重置连续计数
}
}
-
一次成功的操作,就重置连续拒绝计数。这防止了 AI 因为一次误判就持续拒绝所有后续操作。 -
这个机制回答了一个关键问题:如果 AI 一直自作主张拒绝执行,我怎么干预?答案是:连续拒绝3次以上,Claude Code 会强制弹窗;累计拒绝20次,彻底回退到弹窗模式。AI 的判断可以被打破,用户始终有最终决定权。
当 AI 在 auto 模式下拒绝了某个操作,这些拒绝会被记录下来:
// denialTracking.ts
export function recordAutoModeDenial(params: {
toolName: string
display: string
reason: string
timestamp: number
}): void
通过 /permissions 命令可以查看所有的权限决策记录,包括哪次操作被拒绝、为什么被拒绝。AI 的每一次”自作主张”都是透明可见的。
Claude Code 不只是处理对话,它管理着一套完整的任务系统。
export type TaskType =
| 'local_bash' // 本地命令执行
| 'local_agent' // 本地子 Agent
| 'remote_agent' // 远程 Agent
| 'in_process_teammate' // 同进程内的子 Agent
| 'local_workflow' // 本地工作流脚本
| 'monitor_mcp' // MCP 服务器状态监控
| 'dream' // 探索/头脑风暴模式
每种任务类型对应不同的执行策略:
|
|
|
|
|---|---|---|
local_bash |
自动
|
|
local_agent |
手动
|
|
remote_agent |
手动
|
|
in_process_teammate |
自动/手动
|
|
local_workflow |
手动
|
|
monitor_mcp |
自动
|
|
dream |
自动
|
|
触发方式的核心区别:
- 自动触发
(local_bash、monitor_mcp、dream):Claude 决定是否创建,用户不感知 - 手动触发
(local_agent、remote_agent、local_workflow):需要用户或 Claude 显式调用工具 - subagent_type 的作用:
-
当调用 Agent 工具时,可以通过 subagent_type指定子 Agent 的类型:
// 不指定 subagent_type → fork(继承完整上下文)
// 指定 subagent_type → 启动独立子会话
subagent_type: "code-reviewer" // 代码审查专家
subagent_type: "worker" // Coordinator 模式下的通用 worker
-
有 subagent_type 时,子 Agent 从零开始,没有之前的对话上下文,需要在 prompt 里提供完整任务描述。没有 subagent_type 时,子 Agent 继承父会话的完整上下文,适合中间步骤不需要保留结果的场景。
export type TaskStatus =
| 'pending' // 已创建,等待执行
| 'running' // 执行中
| 'completed' // 正常完成
| 'failed' // 执行失败
| 'killed' // 被手动终止
每一步都有状态记录,状态机会确保任务不会在未知状态停留。
export function isTerminalTaskStatus(status: TaskStatus): boolean {
return status === 'completed'
|| status === 'failed'
|| status === 'killed'
}
只有进入终态(completed/failed/killed)的任务才会从 AppState 中清理。这意味着:任务在界面消失 ≠ 任务已经结束,必须等状态机确认才认为任务真正结束。
每个任务都有一个唯一 ID,用随机字节生成:
const TASK_ID_PREFIXES: Record<string, string> = {
local_bash: 'b',
local_agent: 'a',
remote_agent: 'r',
in_process_teammate: 't',
local_workflow: 'w',
monitor_mcp: 'm',
dream: 'd',
}
export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type)
const bytes = randomBytes(8)
let id = prefix
for (let i = 0; i < 8; i++) {
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
}
return id // 例如: a3f7c2d1e9b4
}
ID 前缀标识任务类型,后8位是随机字符串。36^8 ≈ 2.8万亿组合,足以防止暴力碰撞攻击。
QueryEngine.ts(1300+行)是整个系统的中枢,管理一次对话生命周期内的所有状态。
源码里有一段关键的注释和实现:
// Persist the user's message(s) to transcript BEFORE entering the query
// loop. If the process is killed before the API responds,
// the transcript is left with only queue-operation entries.
// Writing now makes the transcript resumable from the point
// the user message was accepted, even if no API response ever arrives.
if (persistSession && messagesFromUserInput.length > 0) {
const transcriptPromise = recordTranscript(messages)
if (isBareMode()) {
void transcriptPromise // 异步,不阻塞
} else {
await transcriptPromise
// ...
}
}
用户消息先写到磁盘,再开始 API 调用。 这样即使在 API 响应过程中进程被杀死,会话也是可恢复的。这是 Claude Code 实现 --resume 功能的底层基础。
QueryEngine 用 wrappedCanUseTool 包装了所有工具调用:
const wrappedCanUseTool: CanUseToolFn = async (
tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision
) => {
const result = await canUseTool(
tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision
)
// Track denials for SDK reporting
if (result.behavior !== 'allow') {
this.permissionDenials.push({
tool_name: tool.name,
tool_use_id: toolUseID,
tool_input: input,
})
}
return result
}
所有非 allow 的调用都被记录到 permissionDenials 数组里,最终随结果一起报告给用户。这让 AI 的每一次”自作主张”都有案可查。
submitMessage 是一个 AsyncGenerator,它流式返回消息:
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown> {
for await (const message of query(...)) {
yield* normalizeMessage(message)
// 每条消息都实时 yield,支持流式 UI 更新
}
}
这种设计让 Claude Code 能实时显示 AI 的思考过程和工具调用,而不需要等到 AI 完全回答完才看到结果。
if (maxBudgetUsd !== undefined && getTotalCost() >= maxBudgetUsd) {
yield {
type: 'result',
subtype: 'error_max_budget_usd',
total_cost_usd: getTotalCost(),
permission_denials: this.permissionDenials,
num_turns: turnCount,
// ...
}
}
Claude Code 会追踪每次 API 调用的消耗,超出预算时主动停止并生成详细报告,包括:总花费、Token 用量、拒绝操作列表、对话轮次。
以上是底层机制的三个核心模块。权限系统决定了 AI 能做什么、不能做什么;任务状态机确保每个任务都能被追踪和管理;QueryEngine 则是整个执行流程的调度中枢。 下篇我们会深入上层能力:Hooks 生命周期如何实现全链路拦截、Coordinator Mode 怎样让多个 Agent 协同工作、以及 verify 和 /stuck 这些内置 Skills 怎么让 Claude Code 实现自我验证和自我诊断。 敬请期待。
夜雨聆风