第 2 课:Hook 引擎 — AI 是怎么获得超能力的

3 阅读7分钟

核心命题: 一段启动脚本决定了 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。

clearcompact 会清除或压缩上下文,所以需要重新注入。

决策 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)看到的:

  1. : << 'CMDBLOCK': 在 cmd.exe 中是标签前缀,这行被当作标签忽略
  2. @echo off — 关闭回显,正常执行
  3. 后续的 ifsetwhere 都是标准 cmd.exe 语法
  4. CMDBLOCK — 又一个标签,忽略
  5. # 开头的行 — cmd.exe 不认识,但前面已经 exit /b 了,不会执行到这里

Unix(bash)看到的:

  1. : << 'CMDBLOCK': 在 bash 中是 no-op(什么都不做),<< 'CMDBLOCK' 是 heredoc 语法,把从这里到 CMDBLOCK 之间的所有内容当作输入丢弃
  2. Windows 的整个 @echo off ... exit /b 0 块被 heredoc "吃掉"了
  3. CMDBLOCK — heredoc 结束标记
  4. # Unix 部分 — 注释
  5. 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_ROOTCLAUDE_PLUGIN_ROOT。如果先检测 Claude Code,Cursor 会收到错误格式的 JSON。

三种平台期望三种不同的 JSON 字段:

平台JSON 字段原因
Cursoradditional_context(snake_case)Cursor hooks 的约定
Claude CodehookSpecificOutput.additionalContext(嵌套)Claude Code 的 SDK 规范
Copilot CLI 等additionalContext(顶层 camelCase)SDK 标准格式

Claude Code 以前会同时读 additional_contexthookSpecificOutput,如果两个都有就会注入两次。所以脚本必须根据平台只输出一个。


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 在收到用户请求后会:

  1. 检查是否有技能适用
  2. 用 Skill 工具加载对应技能
  3. 按技能指导行动

这是一种"懒加载"策略:启动时只注入调度器(约 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 CodeCursor
顶层结构{ "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.xWindows 路径空格导致失败hooks.json 中用了单引号改为转义双引号永远用双引号包裹路径
v4.2.0Claude Code 2.1.x 破坏执行.sh 自动检测加了 bash 前缀去掉文件扩展名文件名本身会影响执行方式
v4.3.0首条消息缺少技能async: true 导致竞态改为 async: false顺序很重要,快不如对
v5.0.3bash 5.3+ heredoc 挂死bash 回归 bugprintf 替代不要假设 bash 版本
v5.0.3Ubuntu 上报错dash 不支持 BASH_SOURCE改用 $0POSIX 兼容比 bash 专属更安全
v5.0.3resume 时重复注入matcher 匹配了所有事件只匹配 startup/clear/compact想清楚什么时候该触发
v5.0.7Copilot 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 乖乖遵守规则。