核心命题: 一段启动脚本决定了 AI 的"第一印象",而这个第一印象塑造了整个会话的行为。
课前回顾
第 1 课我们建立了全局认知:Superpowers 通过 14 个技能改变 AI 的行为。但有一个问题没有回答:
这些技能是怎么"装进" AI 脑子里的?
AI 不像软件那样可以"安装模块"。它的行为完全由上下文决定 — 你给它什么文本,它就按什么方式工作。所以,关键问题变成了:Superpowers 是在什么时刻、以什么方式、把什么内容注入到 AI 的上下文中的?
这就是 Hook 引擎要回答的问题。
2.1 启动链路全追踪
从用户启动到 AI 获得超能力
当你打开 Claude Code 并开始一个新会话时,以下事情在你看不见的地方发生了:
用户启动 Claude Code
│
▼
Claude Code 扫描已安装的插件
│
▼
找到 superpowers 插件,读取 .claude-plugin/plugin.json
│
▼
plugin.json 指向 hooks/hooks.json
│
▼
hooks.json 注册了 SessionStart 事件
│
▼
触发条件匹配(startup)→ 执行 hooks/run-hook.cmd session-start
│
▼
run-hook.cmd 找到 bash → 执行 hooks/session-start 脚本
│
▼
session-start 读取 skills/using-superpowers/SKILL.md
│
▼
把内容包装成 JSON,用 <EXTREMELY_IMPORTANT> 标签包裹
│
▼
JSON 输出被 Claude Code 接收
│
▼
内容注入到 AI 的上下文中
│
▼
AI 的第一条回复就已经"有超能力了"
让我们逐个文件展开。
文件 1:plugin.json — 插件的身份证
路径: .claude-plugin/plugin.json
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.0.7",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": ["skills", "tdd", "debugging", "collaboration", "best-practices", "workflows"]
}
这个文件告诉 Claude Code:"我是一个名为 superpowers 的插件"。Claude Code 会在插件目录中寻找 hooks/hooks.json 来注册生命周期事件。
文件 2:hooks.json — 事件注册表
路径: hooks/hooks.json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}
三个关键设计决策隐藏在这几行 JSON 中:
决策 1:matcher 是 startup|clear|compact,为什么不含 resume?
resume 是恢复上一次会话。恢复的会话已经有 Superpowers 的上下文了(在上次会话的历史消息中)。如果再注入一次,AI 会收到两份相同的指令,浪费上下文窗口。这个决策来自 v5.0.3 的 bug fix。
clear 和 compact 会清除或压缩上下文,所以需要重新注入。
决策 2:async: false,为什么必须同步?
v4.3.0 之前用的是 async: true。结果发现:异步时 hook 可能还没执行完,AI 就开始处理用户的第一条消息了。这意味着 AI 的第一条回复没有 Superpowers — 它可能直接开始写代码而不是先问问题。
改为 async: false 后,Claude Code 会等 hook 执行完再让 AI 回复。代价是启动稍慢(几百毫秒),收益是 AI 从第一秒就有超能力。
决策 3:路径中的 ${CLAUDE_PLUGIN_ROOT}
这是 Claude Code 提供的环境变量,指向插件的安装目录。注意路径用双引号包裹 — 这是为了处理路径中有空格的情况(Windows 上的 C:\Program Files\...)。这个细节来自 v4.0.x 的 Windows 兼容性 bug。
文件 3:run-hook.cmd — 跨平台魔法文件
路径: hooks/run-hook.cmd
这是整个项目最巧妙的工程之一 — 一个文件,既是 Windows 的 .cmd 批处理脚本,又是 Unix 的 bash 脚本。
: << 'CMDBLOCK'
@echo off
REM Windows 部分开始 ────────────────────────────
if "%~1"=="" (
echo run-hook.cmd: missing script name >&2
exit /b 1
)
set "HOOK_DIR=%~dp0"
REM 尝试位置 1:标准 Git for Windows
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM 尝试位置 2:32 位 Git for Windows
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM 尝试位置 3:PATH 中的 bash
where bash >nul 2>nul
if %ERRORLEVEL% equ 0 (
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM 找不到 bash — 静默退出而不报错
exit /b 0
CMDBLOCK
# Unix 部分 ────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
它是怎么同时工作在两个操作系统上的?
Windows(cmd.exe)看到的:
: << 'CMDBLOCK'—:在 cmd.exe 中是标签前缀,这行被当作标签忽略@echo off— 关闭回显,正常执行- 后续的
if、set、where都是标准 cmd.exe 语法 CMDBLOCK— 又一个标签,忽略#开头的行 — cmd.exe 不认识,但前面已经exit /b了,不会执行到这里
Unix(bash)看到的:
: << 'CMDBLOCK'—:在 bash 中是 no-op(什么都不做),<< 'CMDBLOCK'是 heredoc 语法,把从这里到CMDBLOCK之间的所有内容当作输入丢弃- Windows 的整个
@echo off ... exit /b 0块被 heredoc "吃掉"了 CMDBLOCK— heredoc 结束标记# Unix 部分— 注释exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"— 执行目标脚本
为什么文件没有扩展名?
session-start 脚本故意没有 .sh 扩展名。原因来自 v4.2.0 的 bug:Claude Code 2.1.x 在 Windows 上会自动检测 .sh 文件并在命令前加 bash,导致 bash "run-hook.cmd" session-start.sh — 这样 cmd.exe 会尝试把 .cmd 文件当 bash 脚本执行,自然会失败。
去掉扩展名后,自动检测不触发,polyglot 包装器正常工作。
为什么找不到 bash 就静默退出?
REM 找不到 bash — 静默退出而不报错
exit /b 0
如果 Windows 上没有安装 Git Bash 或 WSL,hook 就无法执行。但这不应该让 Claude Code 报错 — 插件的其他功能(技能文件本身)仍然可用,只是缺少了启动时的自动注入。
文件 4:session-start — 核心注入脚本
路径: hooks/session-start
这是真正的核心。它做三件事:读取技能内容、JSON 转义、按平台输出。
#!/usr/bin/env bash
set -euo pipefail
# ① 确定插件根目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# ② 检查是否有旧版技能目录需要迁移
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
warning_message="..." # 提示用户迁移
fi
# ③ 读取 using-superpowers 技能的完整内容
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 \
|| echo "Error reading using-superpowers skill")
# ④ JSON 转义函数
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}" # 反斜杠
s="${s//\"/\\\"}" # 双引号
s="${s//$'\n'/\\n}" # 换行
s="${s//$'\r'/\\r}" # 回车
s="${s//$'\t'/\\t}" # 制表符
printf '%s' "$s"
}
# ⑤ 转义内容
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
warning_escaped=$(escape_for_json "$warning_message")
# ⑥ 组装注入内容
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n..."
# ⑦ 根据平台输出不同的 JSON 格式
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
printf '{\n "hookSpecificOutput": {\n "additionalContext": "%s"\n }\n}\n' "$session_context"
else
printf '{\n "additionalContext": "%s"\n}\n' "$session_context"
fi
关键决策:escape_for_json 的性能优化
早期版本用逐字符循环做 JSON 转义:
# 旧版:逐字符处理 — O(n²)
for ((i=0; i<${#input}; i++)); do
char="${input:$i:1}"
case "$char" in
...
esac
done
这在 macOS 上还能忍受,但在 Windows Git Bash 上需要 60+ 秒。原因是 bash 的 ${input:$i:1} 子字符串操作在每次调用时都会复制整个字符串,导致 O(n²) 复杂度。
修复:改用 bash 的参数替换 ${s//old/new},这是单次 C 层面的全局替换,速度快了 7 倍以上。
关键决策:printf 替换 heredoc
v5.0.3 之前用 heredoc 输出 JSON:
cat <<EOF
{
"hookSpecificOutput": {
"additionalContext": "${session_context}"
}
}
EOF
但 macOS 上 Homebrew 安装的 bash 5.3+ 有一个回归 bug:当 heredoc 中包含大量变量展开时,会无限挂起。修复:改用 printf,绕过 heredoc 机制。
关键决策:平台检测顺序
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
# Cursor 优先检测 — 因为 Cursor 也可能设置 CLAUDE_PLUGIN_ROOT
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
# Claude Code(不是 Copilot CLI)
else
# Copilot CLI 或其他平台 — SDK 标准格式
fi
为什么 Cursor 要优先检测?因为 Cursor 可能同时设置 CURSOR_PLUGIN_ROOT 和 CLAUDE_PLUGIN_ROOT。如果先检测 Claude Code,Cursor 会收到错误格式的 JSON。
三种平台期望三种不同的 JSON 字段:
| 平台 | JSON 字段 | 原因 |
|---|---|---|
| Cursor | additional_context(snake_case) | Cursor hooks 的约定 |
| Claude Code | hookSpecificOutput.additionalContext(嵌套) | Claude Code 的 SDK 规范 |
| Copilot CLI 等 | additionalContext(顶层 camelCase) | SDK 标准格式 |
Claude Code 以前会同时读 additional_context 和 hookSpecificOutput,如果两个都有就会注入两次。所以脚本必须根据平台只输出一个。
2.2 注入的内容:using-superpowers 技能
为什么只注入这一个技能?
session-start 读取的是 skills/using-superpowers/SKILL.md — 所有 14 个技能中只注入了这一个。为什么?
因为 using-superpowers 是"调度器"。它不教 AI 怎么做 TDD 或怎么调试,它教 AI 怎么发现和调用其他技能。
核心规则:
<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing,
you ABSOLUTELY MUST invoke the skill.
</EXTREMELY-IMPORTANT>
有了这条规则,AI 在收到用户请求后会:
- 检查是否有技能适用
- 用 Skill 工具加载对应技能
- 按技能指导行动
这是一种"懒加载"策略:启动时只注入调度器(约 100 行),其他技能在需要时按需加载。如果把所有 14 个技能都在启动时注入,会占用大量上下文窗口,挤压用户实际工作的空间。
注入内容的包装
技能内容被包装在 <EXTREMELY_IMPORTANT> 标签中:
<EXTREMELY_IMPORTANT>
You have superpowers.
**Below is the full content of your 'superpowers:using-superpowers' skill...**
[using-superpowers/SKILL.md 的完整内容]
</EXTREMELY_IMPORTANT>
<EXTREMELY_IMPORTANT> 不是标准的 HTML 或 Markdown 标签 — 它是一个"注意力信号"。AI 训练数据中,类似标签通常标记高优先级内容。这确保 AI 不会忽略这段注入的文本。
2.3 Cursor 的差异化处理
Superpowers 为 Cursor 维护了单独的 hook 配置文件。
路径: hooks/hooks-cursor.json
{
"version": 1,
"sessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start"
}
]
}
]
}
和 Claude Code 的 hooks.json 对比:
| 差异 | Claude Code | Cursor |
|---|---|---|
| 顶层结构 | { "hooks": { "SessionStart": [...] } } | { "version": 1, "sessionStart": [...] } |
| 事件名大小写 | SessionStart(PascalCase) | sessionStart(camelCase) |
| version 字段 | 无 | 必须有 "version": 1 |
| async 字段 | 需要显式设为 false | 无此字段 |
这就是跨平台的现实 — 同一个概念(会话启动时执行脚本),每个平台的 API 格式都不一样。
2.4 动手实验
实验 1:手动执行 hook 并观察输出
cd /path/to/superpowers
# 模拟 Claude Code 环境
CLAUDE_PLUGIN_ROOT=. bash hooks/session-start 2>/dev/null | python3 -m json.tool
观察:
- 输出的 JSON 结构是什么样的?
additionalContext字段的内容有多长?- 能看到
<EXTREMELY_IMPORTANT>标签吗?
实验 2:模拟不同平台
# 模拟 Cursor
CURSOR_PLUGIN_ROOT=. bash hooks/session-start 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('Keys:', list(d.keys()))
print('Content length:', len(str(d)))
"
# 模拟 Copilot CLI
COPILOT_CLI=1 CLAUDE_PLUGIN_ROOT=. bash hooks/session-start 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('Keys:', list(d.keys()))
"
对比三种平台的 JSON 结构差异。
实验 3:测量注入内容大小
# 查看 using-superpowers 技能的大小
wc -w skills/using-superpowers/SKILL.md
wc -c skills/using-superpowers/SKILL.md
# 查看完整注入内容的大小
CLAUDE_PLUGIN_ROOT=. bash hooks/session-start 2>/dev/null | wc -c
思考:这个大小对 AI 的上下文窗口意味着什么?
2.5 从 Bug 中学到的设计智慧
Hook 系统的演化史是一部跨平台兼容性的教科书。每个 bug fix 都隐藏着一个教训:
| 版本 | Bug | 根因 | 修复 | 教训 |
|---|---|---|---|---|
| v4.0.x | Windows 路径空格导致失败 | hooks.json 中用了单引号 | 改为转义双引号 | 永远用双引号包裹路径 |
| v4.2.0 | Claude Code 2.1.x 破坏执行 | .sh 自动检测加了 bash 前缀 | 去掉文件扩展名 | 文件名本身会影响执行方式 |
| v4.3.0 | 首条消息缺少技能 | async: true 导致竞态 | 改为 async: false | 顺序很重要,快不如对 |
| v5.0.3 | bash 5.3+ heredoc 挂死 | bash 回归 bug | 用 printf 替代 | 不要假设 bash 版本 |
| v5.0.3 | Ubuntu 上报错 | dash 不支持 BASH_SOURCE | 改用 $0 | POSIX 兼容比 bash 专属更安全 |
| v5.0.3 | resume 时重复注入 | matcher 匹配了所有事件 | 只匹配 startup/clear/compact | 想清楚什么时候该触发 |
| v5.0.7 | Copilot CLI 格式不兼容 | 不同平台期望不同 JSON | 三路平台检测 | 永远不要假设统一的 API |
最深刻的教训:async: false 的故事
这个改动只有一个字段的变化,但它揭示了一个核心真理:AI 的"第一印象"决定了整个会话。 如果 AI 在第一条消息时还没有获得技能,它就按"默认模式"回复了 — 直接开始写代码。即使后续注入了技能,AI 已经建立了"这个会话不需要先设计"的行为模式。
本课自检清单
- 能画出从"启动会话"到"AI 获得技能"的完整链路(5 个文件的调用关系)
- 能解释 polyglot 文件
run-hook.cmd为什么能同时在 Windows 和 Unix 上工作 - 能说出
hooks.json中三个关键设计决策(matcher、async、路径引号) - 能解释为什么只注入
using-superpowers而不是所有 14 个技能 - 能说出三种平台的 JSON 输出格式差异
下节预告
第 3 课:铁律与纪律 — AI 行为控制的核心模式
Hook 让 AI 获得了超能力。但超能力的核心是什么?是 14 个技能中的"纪律三件套":TDD、验证、系统调试。下一课我们深入 AI 最擅长也最容易绕过的领域 — 工程纪律,以及 Superpowers 如何用"Iron Law + Red Flags + Rationalizations"三板斧让 AI 乖乖遵守规则。