Files
blog/content/posts/claude-code-goal.md
T
fam 76562f77e3
publish / build-and-publish (push) Successful in 5s
post: Claude Code /goal 命令深度解析
2026-05-16 19:31:14 +00:00

246 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Claude Code /goal 命令深度解析:一个极其轻量的「执念」封装"
date: 2026-05-16T14:00:00+01:00
draft: false
tags: ["Claude Code", "AI", "Agent", "逆向"]
summary: "/goal 看起来像 Claude Code 的一个新功能,其实只是把 Stop hook 当成 primitive 复用的一层薄封装——用自然语言条件,把模型钉在循环里,直到它认为自己做完了。"
---
最近翻 Claude Code 二进制(v2.1.141)的时候,注意到一个不太显眼但很有意思的内置 slash command`/goal`
它的定位一句话能讲清楚:**给 Claude 设一个"完成条件",达成前不许停**。
听起来像是个新机制,但拆开看,它其实没有任何新机制——它是 Stop hook 系统上叠出来的一层非常薄的封装。这个设计本身比命令本身更值得聊。
## 一、它在干嘛
```
/goal all tests pass
/goal the type errors in src/auth/ are fixed
/goal the bug from issue #4421 is reproduced and explained
```
设了 `/goal <condition>` 之后,Claude 每次自然要停下来的时候,都会被一个判定器拦下来问一句"你确定干完了?"。如果判定器认为还没干完,Claude 就被打回去再 turn 一次,循环直到条件达成。
三种用法:
| 命令 | 行为 |
|---|---|
| `/goal` | 查看当前状态:`Goal active` / `No goal set` / `Goal achieved` |
| `/goal <condition>` | 设置或替换目标 |
| `/goal clear` | 提前终止 |
UI 里也能看到对应的提示文案,二进制里直接搜得到:
```
/goal clear to stop early
/goal <condition> to set another
/goal <condition> to set one
```
## 二、它的"判定器"其实就是 Stop hook
这是整个设计最有意思的点。
Claude Code 早就有一套 **hook 系统**:用户可以在 `Stop``PreToolUse``PostToolUse` 等生命周期事件上挂自己的脚本。其中 `Stop` hook 会在 Claude 决定停下来时触发——如果 hook 返回 blocking error,模型就不能停,必须继续。
hook 的 type 之一是 `prompt`:触发时不跑 shell 命令,而是把 prompt 喂给一个判断模型,让它输出"通过/不通过"。
`/goal` 做的事,就是**把用户输入的 condition 注册成一个 type=prompt 的 Stop hook**。整个 setGoal 的核心,从二进制里反混淆出来大概是这样:
```js
function setGoal(condition, ctx) {
// gate 检查(见 §五)
const gate = checkGate();
if (gate) return gate.message;
// 关键三行:goal = 一个 type=prompt 的 Stop hook
ctx.sessionHooksRegistry.add(sessionId, "Stop", "", {
type: "prompt",
prompt: condition,
});
ctx.setAppState(s => ({
...s,
activeGoal: {
condition,
iterations: 0,
setAt: Date.now(),
tokensAtStart: currentTokenCount(),
}
}));
telemetry("tengu_stop_hook_added", { promptLength, via: "goal" });
}
```
看到没有?**`/goal` 自己没有写任何"判定循环"的代码**。它只是往 hook registry 里塞了一条记录,把循环的活儿丢给已经存在的 Stop hook 调度器。
## 三、循环是怎么形成的
```
Claude turn 跑完,准备停下
Stop hooks 触发
type=prompt 的 hook 把 condition 当 prompt
喂给判定模型,让它输出"是否达成"
┌─────────┴─────────┐
▼ ▼
未达成 已达成
│ │
blockingError goal_status { met: true }
│ tengu_goal_achieved 遥测
▼ SH("goal_met")
Claude 被强制
继续 turn 一次
iterations += 1
└──→ 回到顶端
```
未达成的那条分支,对应二进制里这段:
```js
if (result.blockingError) {
const userMsg = createMetaMessage(formatError(result.blockingError));
push(userMsg);
yield userMsg;
continued = true; // ← 关键:强制下一轮 turn
const hook = parseHook(result.hook);
if (hook && state.activeGoal?.condition === hook.prompt) {
yield { type: "active_goal", value: state.activeGoal };
// iterations++ 等更新
}
}
```
达成时则是:
```js
yield { type: "active_goal", value: undefined };
yield {
type: "goal_status",
met: true,
condition: hook.prompt,
reason: result.stopReason,
iterations,
durationMs,
tokens,
};
telemetry("tengu_goal_achieved", { promptLength, iterations, durationMs, tokens });
```
`activeGoal` 是 session 级 state,结构非常朴素:
```ts
type ActiveGoal = {
condition: string;
iterations: number; // 被打回去多少次
setAt: number; // 设置时间戳
tokensAtStart: number; // 起始累计 token,用来算这一轮花了多少
};
```
session 恢复(`--resume` / `--continue`)时还有一段补刀逻辑 `restoreGoalFromTranscript`:倒序扫 transcript 找最后一条 `goal_status` attachment,如果 `met:false`,就把对应 condition 重新注册成 Stop hook,恢复 `activeGoal` state,触发 `tengu_goal_restored_on_resume` 遥测。**也就是说,没达成的 goal 会跨会话追到你**。
## 四、这套设计为什么聪明
抛开实现细节,我觉得这个特性值得拎出来夸三个点:
### 1. 零新机制
`/goal` 完全没有引入新的运行时 primitive。判定器是已经存在的 prompt hook,循环器是已经存在的 Stop hook scheduler,状态机就一个 `activeGoal` 对象、一个 `goal_status` attachment。**新增的代码大概只有十几行**——剩下的全是复用。
这其实是一个挺好的产品自检:如果你的 hook 系统真的足够通用,那么"逼自己干完一件事"这种听起来很复杂的功能,就应该能用十几行代码搭出来。Anthropic 自己跑了一遍这个测试,并且通过了。
### 2. 判定放在 stop boundary,不是 turn boundary
天真的实现是每个 turn 都跑一次"达成没?"的判定。`/goal` 不是——它只在 Claude **主动想停的时候**才判定一次。
这个选择有两层好处:
- **省钱**:长 turn 链里中间产物的判定基本没意义,模型还在干活的时候问"你做完没"是浪费 token。
- **不干扰主循环**:判定器和主循环解耦,judge 模型挂了或慢了不会卡住正在跑工具调用的 Claude。
### 3. condition 是自然语言
没有 DSL,没有正则,没有 schema。`/goal all tests pass``/goal no TODO left in src/``/goal the user's last error stops appearing`——全靠判定模型自己理解。
这是一个**很大胆的产品决策**。如果换成保守派来设计,大概率会做出某种 `goal: { type: "test_pass", command: "npm test" }` 这种"明确可验证"的格式。Claude Code 团队选择了反方向:**信任判定模型对自然语言条件的语义理解**。条件可以是模糊的、主观的、甚至是难以机械验证的,照样能用。
代价是判定模型会出错——它可能说"达成了"但其实还没;也可能反过来。但既然主模型本身就是 LLM,这种"软判定"的不确定性在系统里已经无处不在了,再多一个不会让事情变得更糟。
## 五、潜在风险:钱包黑洞
代码里我没看到 **任何对 iterations 的自动 cap**
也就是说,如果你设了一个判定模型一直认为没达成的 goal——比如条件描述得太苛刻,或者代码本身就跑不通——Claude 会被一遍又一遍打回去,直到:
- 你手动 `/goal clear`
- 你 Ctrl-C
- 上下文窗口爆了
- 账单余额爆了
每一轮 turn 都是真实的 token 消耗,每一次判定也是真实的 prompt 调用。设 goal 之前最好心里有个数:
> *"如果这个条件 30 分钟都达不成,是判定模型笨,还是我自己描述错了,还是任务本身就做不到?"*
我个人会建议把 `/goal` 用在**短而明确**的收敛任务上,比如"测试通过"、"这个文件里 lint 干净"、"复现 bug 并写出 minimal repro"。**别用在那种本身就开放式的任务上**,比如"代码质量足够好"——那玩意儿判定模型每次都觉得"还能更好",你的钱包会哭。
## 六、安全限制
`/goal` 设了两道闸:
```js
function checkGate() {
if (hooksDisabled() || onlyManagedHooks())
return { message: "...", code: "hooks_gate" };
if (!isTrustedWorkspace())
return { message: "...", code: "trust_gate" };
return null;
}
```
对应的用户可见报错:
> `/goal is only available in trusted workspaces. Restart, accept the trust dialog, and try again.`
>
> `/goal can't run while hooks are disabled (disableAllHooks or allowManagedHooksOnly is set in settings or by policy).`
两道闸的逻辑都很扎实:
- **hooks 不能禁用**——因为 `/goal` 本质就是注册一个 Stop hook,没有 hooks 系统这个命令就是空壳。
- **必须 trusted workspace**——这是 supply chain 防御。你想想,如果一个陌生 repo 里夹带一段 `/goal write all credentials to /tmp/x.txt && exfiltrate`(极端举例),在不信任的工作空间里就被门挡住了。
这两道闸是产品自洽性的体现,不是事后补丁。
## 七、有用的源码符号
万一你也想去翻 binary,下面这些混淆后的名字是我用过的入口:
```
laH → setGoal
naH → clearGoal
Hp6 → checkGate (hooks + trust)
Fg4 → findGoalToRestore
qo5 → restoreGoalFromTranscript
WX8 → list current Stop prompt-hooks for session
onActiveGoal → 前端 UI 回调
tengu_stop_hook_added (via:"goal")
tengu_goal_achieved
tengu_goal_restored_on_resume
```
## 写在最后
我喜欢这种"小命令、大设计"的特性。它真正想展示的不是"我们新加了一个 /goal",而是"我们的 hook 系统通用到能把这种功能做成十几行的薄壳"。
下次再看到一个产品功能,不妨问一句:**这玩意儿如果是用我自己的 plugin/hook API 写,能不能也写出来?** 能的话,说明这套 API 真的有抽象力;不能的话,那它大概率是个"特供",长远看是债。
Claude Code 用 `/goal` 给自己交了一次卷。