首页
/ superpowers 跨平台 Hooks 实战:一份 run-hook.cmd 多语言脚本如何打通 Windows、macOS 与 Linux

superpowers 跨平台 Hooks 实战:一份 run-hook.cmd 多语言脚本如何打通 Windows、macOS 与 Linux

2026-09-04 18:41:39作者:滑思眉Philip

superpowers 插件需要在 Windows、macOS 和 Linux 三端运行相同的 Hook 逻辑,但三端可用的 shell 解析器各不相同(bash、Git Bash、PowerShell、CMD.exe)。本文以 docs/windows/polyglot-hooks.md 的"单一通用分发器(single generic dispatcher)"模式为主线,结合 hooks/run-hook.cmd 的逐行实现、hooks/hooks.json 的注册配置和 tests/hooks/test-session-start.sh 的测试断言,讲清楚这套无扩展名 Hook 脚本 + 多语言包装器的设计动机、双平台执行原理和可移植编写规范。读完后你将掌握:如何在 Claude Code 生态中编写三端通吃的 Hook 命令、为什么脚本必须去掉 .sh 扩展名,以及如何在无 bash 的 Windows 环境下优雅降级。

一、问题背景:Hook 命令必须经过 shell 解析

Claude Code 通过 shell 执行 Hook 命令,而不同平台实际可用的 shell 并不统一:

  • macOS / Linux:bash 或 sh
  • Windows(已安装 Git Bash):Git Bash
  • Windows(未安装 Git Bash):PowerShell(旧版本曾回退到 CMD.exe)

文档明确指出,两个 Windows 回退 shell 都无法正确解析插件的命令字符串:

  1. PowerShell 会把命令开头的引号路径当作字符串表达式,并在遇到下一个裸词(bareword)时报错;
  2. CMD.exe/c 引号规则会在路径包含元字符(如 ()时剥掉外层引号——而 C:\Program Files (x86)\... 恰好包含 (,这是真实会踩中的坑。

因此 superpowers 的 Hook 声明中显式指定了 "shell": "bash"(Claude Code 2.1.81 起支持该键,更早的版本会忽略它),强制走 Git Bash 路径;当系统没有 Git Bash 时,会得到一个可操作的"请安装 Git for Windows"错误,而不是难以定位的 shell 解析失败。

在此之上还有四个具体的跨平台障碍(均为原文档列出的挑战):

  1. 脚本执行:Windows CMD 无法直接执行 .sh 文件;
  2. 路径格式:Windows 用反斜杠(C:\path),Unix 用正斜杠(/path);
  3. 环境变量$VAR 语法在 CMD 中无效;
  4. .sh 自动前缀:Claude Code 在 Windows 上会自动给任何路径包含 .sh 的命令前面加上 bash——如果 Hook 脚本带扩展名,这一行为会干扰分发器本身。

二、解决方案:无扩展名脚本 + 单一通用分发器

superpowers 仓库对所有 Hook 共用同一个 hooks/run-hook.cmd 分发器,而真正的 Hook 逻辑放在无扩展名的脚本中(是 session-start,而不是 session-start.sh)。这一点是刻意为之:命令字符串中不含 .sh,就能阻止 Claude Code 的 Windows 自动检测把 bash 前缀注入到分发器命令里、破坏其执行路径。

2.1 文件结构

hooks/
├── hooks.json          # 指向 run-hook.cmd,且引用无扩展名脚本名
├── run-hook.cmd        # 跨平台分发器(多语言包装器)
└── session-start       # 真正的 Hook 逻辑 —— 无扩展名 bash 脚本

2.2 hooks.json 注册配置

hooks/hooks.json 的完整内容如下(见 hooks.json#L4-L16):

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|clear|compact",
        "hooks": [
          {
            "type": "command",
            "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
            "shell": "bash",
            "async": false
          }
        ]
      }
    ]
  }
}

逐字段说明:

字段 取值 作用
matcher startup|clear|compact Claude Code 的 SessionStart 事件在会话启动、/clear、自动压缩三种场景触发;Cursor 则用 sessionStart(见 hooks/hooks-cursor.json
command "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start" 路径必须加引号,因为 ${CLAUDE_PLUGIN_ROOT} 展开后可能含空格;session-start 为第一个位置参数,指认要执行的无扩展名脚本
shell bash 强制 Git Bash 路由,规避 PowerShell/CMD 的解析缺陷
async false 同步执行,保证上下文注入发生在会话可用之前

注意命令尾部是 run-hook.cmd" session-start——脚本名不带扩展名,这正是后文测试用例专门校验的注册形态。

三、run-hook.cmd 源码级解析

文档强调 hooks/run-hook.cmd 是唯一权威实现,修改分发器时应直接阅读该文件并运行 tests/hooks/test-session-start.sh,而不是从文档抄实现。该文件是一个多语言脚本(polyglot):Windows 的 CMD 把开头视为批处理命令执行,而 Unix shell 把同一段内容当作一个无害的 here-doc 整体跳过。全文仅 46 行,逐段拆解如下。

3.1 第一行:多语言分界点

: << 'CMDBLOCK'
  • Unix shell 而言,: 是 no-op 内建命令,<< 'CMDBLOCK' 开启一个以 CMDBLOCK 为定界符的 here-doc。整个 CMD 批处理块(第 2–40 行)全部作为 here-doc 的"数据"被消费并丢弃,shell 在 CMDBLOCK 标记之后继续执行真正的 Unix 逻辑。
  • CMD.exe 而言,: 开头的行是批处理的合法行(注释/标签式空操作),@echo off 之后则进入纯批处理逻辑。

一行代码同时满足两种解析器,这是整个模式的核心技巧。

3.2 Windows(CMD.exe)分支

批处理部分的执行顺序(见 run-hook.cmd#L13-L39):

  1. 校验脚本名if "%~1"=="" 时输出 run-hook.cmd: missing script name 到 stderr 并 exit /b 1。第一个参数是 hooks.json 传入的无扩展名脚本名(如 session-start)。
  2. 解析 Hook 目录set "HOOK_DIR=%~dp0" 取分发器自身所在目录,从而无论插件安装在何处都能定位到同目录下的 Hook 脚本。
  3. 按顺序尝试三个 bash 位置
    • C:\Program Files\Git\bin\bash.exe(Git for Windows 64 位默认路径)
    • C:\Program Files (x86)\Git\bin\bash.exe(32 位安装路径,注意路径里的 ( 正是 CMD 引号陷阱的受害者)
    • where bash 检测 PATH 上的 bash(覆盖 MSYS2、Cygwin 或非默认位置的 Git 安装)
  4. 找到 bash 后,以 "C:\...\bash.exe" "%HOOK_DIR%%~1" %2 %3 ... 的形式直接执行命名脚本,并把最多 8 个额外参数(%2%9)透传过去,然后 exit /b %ERRORLEVEL% 原样返回 Hook 脚本的退出码。
  5. 找不到任何 bash 时静默退出 0exit /b 0 让插件在未安装 Git for Windows 的机器上继续正常工作,只是跳过上下文注入,而不是让 Hook 报错。
  6. 每个成功路径都以 exit /b 收尾,确保 CMD 在执行到 Unix 部分之前停止。

3.3 Unix(bash/sh)分支

CMDBLOCK 标记之后只有四行(见 run-hook.cmd#L42-L46):

# Unix: run the named script directly
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"

逻辑与 CMD 分支对称:用 $0 解析出分发器自身目录,取第一个参数为脚本名,shiftexec 直接替换进程执行目标脚本,其余参数经 "$@" 完整透传。注意这里不做任何 Windows 路径转换——bash 在 Git for Windows 环境下接收 Windows 路径即可正确处理,这一点正是设计决策表中"不需要 cygpath"的依据。

3.4 关键设计决策表

原文档总结了四个核心决策及其理由,与源码逐条对应:

决策 原因 源码对应
无扩展名脚本 防止 Claude Code 的 Windows .sh-auto-prepend 干扰分发器命令 run-hook.cmd#L7-L9 注释与 hooks.json 命令串
不使用 -l(登录 shell) 无需登录 shell;Hook 脚本应自包含,不依赖登录 shell 的 PATH 配置 调用形如 bash "%HOOK_DIR%%~1",无 -l 参数
不使用 cygpath bash 直接接收 Windows 路径即可正确处理;cygpath 是旧版 -c "..." 调用模式的需求,直接 exec 不需要 Unix 分支直接 exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}"
无 bash 时静默退出 避免破坏未装 Git for Windows 用户的插件;上下文注入被优雅跳过 exit /b 0run-hook.cmd#L37-L39

四、被分发的 Hook 脚本:session-start 实例

hooks/session-start 是无扩展名分发器模式的实际受益者:Windows 上由 run-hook.cmd 找到 bash 后执行它,Unix 上由分发器的 shell 分支直接 exec 它,两端入口完全一致。它的工作是在 SessionStart 事件时把 using-superpowers 技能内容注入会话上下文,其中两个细节对理解本主题很有价值。

4.1 自定位与 JSON 转义

脚本开头用 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 推导插件根目录(session-start#L6-L8),再读取 skills/using-superpowers/SKILL.md。由于注入内容要嵌入 JSON 字符串,必须做转义。文档给出的通用可移植范例是纯 bash 内建的逐字符循环:

escape_for_json() {
    local input="$1"
    local output=""
    local i char
    for (( i=0; i<${#input}; i++ )); do
        char="${input:$i:1}"
        case "$char" in
            $'\\') output+='\\' ;;
            '"') output+='\"' ;;
            $'\n') output+='\n' ;;
            $'\r') output+='\r' ;;
            $'\t') output+='\t' ;;
            *) output+="$char" ;;
        esac
    done
    printf '%s' "$output"
}

而仓库实际采用的是更高效的参数替换版本(session-start#L16-L24)——每个 ${s//old/new} 是单次 C 层遍历,比逐字符循环快几个数量级,源码注释明确写道它"取代了逐字符循环":

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"
}

4.2 按平台输出不同 JSON 字段

session-start 的输出口径(session-start#L38-L47)按环境变量分三路:

  • 设置了 CURSOR_PLUGIN_ROOT(且 Cursor 可能同时设置 CLAUDE_PLUGIN_ROOT)→ 顶层 additional_context(snake_case);
  • CLAUDE_PLUGIN_ROOT 且无 COPILOT_CLI → 嵌套的 hookSpecificOutput.additionalContext
  • 其余(如 Copilot CLI 设置 COPILOT_CLI=1)→ SDK 标准的顶层 additionalContext

注释特别说明:Claude Code 会同时读取 additional_contexthookSpecificOutput不做去重,所以只能输出当前平台消费的那一个字段。这也是分发器必须保证 Hook 在三端都以同样的 bash 语义运行的原因——输出形状由环境变量而非平台决定。

五、测试如何验证整套分发机制

tests/hooks/test-session-start.sh 用纯 bash + node 断言覆盖了分发链路的三个关键面,修改 run-hook.cmd 后必须跑通它:

  1. 注册形态断言test-session-start.sh#L146-L165):用 node 解析 hooks/hooks.json,校验两个硬约束——shell 必须是 "bash",且 command 必须以 run-hook.cmd" session-start 结尾(即命令中不得出现 .sh 后缀,防止触发 Windows 自动前缀)。这条断言的注释复述了第一节的动机:Windows 上 PowerShell 会产生 ParserError、cmd.exe 会在含元字符的路径上剥引号。
  2. 包装器分发断言test-session-start.sh#L177-L185):直接以 bash hooks/run-hook.cmd session-start 调用包装器(模拟 Unix 分支),断言输出是合法的嵌套 hookSpecificOutput JSON 且包含预期的上下文文本——验证了第 3.3 节 shell 分支的 exec 透传路径。
  3. 平台输出形状断言:分别以 CLAUDE_PLUGIN_ROOTCURSOR_PLUGIN_ROOTCOPILOT_CLI=1 三种环境运行 Hook,验证三种 JSON 形状互斥(例如 cursor 形状下不得出现 hookSpecificOutput),并用 env -i 清空环境变量保证测试不受本机 shell 配置污染。

此外,仓库用 scripts/lint-shell.sh 对 shell 文件做 ShellCheck 检查(默认 --severity=warning--strict 模式追加 quote-safe-variables 等规则);其中 is_shell_file 的判定逻辑(lint-shell.sh#L26-L40)值得注意:.sh 扩展名是首要判据,无扩展名文件则回退到检查首行 shebang——session-start 正是靠 #!/usr/bin/env bash 首行被纳入 lint 范围的。

六、编写跨平台 Hook 脚本的规范

文档给出的可移植编写规则,全部可由分发器的执行环境解释:

应该做(Do):

  • 尽量使用纯 bash 内建命令——Hook 通过分发器以非登录、非交互方式执行,外部工具越少越稳;
  • $(command) 替代反引号;
  • 所有变量展开加引号:"$VAR"

避免(Avoid):

  • 依赖 PATH 中的工具却不给回退——Hook 不带 -l 运行,登录 shell 的 PATH 不会被设置,这正是分发器决策表中"No -l"的对应面;
  • 给脚本起 .sh 扩展名——会触发 Claude Code 的 Windows auto-prepend,绕过分发器。

docs/porting-to-a-new-harness.md(Part 7 — Cross-platform / Windows)将这两条规则上升为移植约束:Hook 脚本必须无扩展名,且不要为每个操作系统写变体脚本——一份无扩展名 bash 脚本加多语言包装器即可覆盖全部三端。

七、故障排查

原文档列出的三种典型症状,结合源码可进一步定位:

7.1 "bash is not recognized"

CMD 在分发器尝试的三个位置(两个 Program Files 路径 + PATH)都没找到 bash。此时分发器按设计静默退出 0,Hook 被跳过而非报错。修复方式:将 Git for Windows 安装到标准路径,或确保 bash 在 PATH 上。排查时可在 CMD 中手动执行 run-hook.cmd#L21-L35 对应的 if exist 检查。

7.2 Unix 上正常、Windows 上无动作

首先检查 hooks.json 中脚本名是否无扩展名。形如 run-hook.cmd session-start.sh 的命令会触发 Claude Code 的 .sh 自动检测,绕过预期的 CMD 分发路径,或者干脆去执行一个不存在的 session-start.shtests/hooks/test-session-start.sh#L157-L160 的正则断言 /run-hook\.cmd" session-start$/ 就是防止这种漂移的回归测试。

7.3 Hook 完全不触发

核对 hooks.jsonmatcher 与当前 harness 实际发出的事件类型是否匹配:Claude Code 使用 startup|clear|compactSessionStart,Cursor 使用小写的 sessionStart(其注册文件是 hooks/hooks-cursor.json,且省略了 matcher/type/async 字段,命令直接写 ./hooks/run-hook.cmd session-start)。

八、背景与后续修改指引

文档末尾引用了上游 Claude Code 的两个历史 issue 作为该模式动机的来源:.sh 脚本在 Windows 上会被编辑器打开(anthropics/claude-code#9758)、Hooks 在 Windows 上不工作(anthropics/claude-code#3417)。这两个问题正是"PowerShell/CMD 解析失败"和".sh 自动前缀"两个坑的历史注脚。

最后重申文档给出的维护纪律:hooks/run-hook.cmd 是唯一权威实现——修改分发器时直接阅读源码而非本文或文档转述,修改后运行 tests/hooks/test-session-start.sh 验证;若把本模式移植到新 harness,docs/porting-to-a-new-harness.md 的 Part 7 给出了必须遵守的两条规则(无扩展名、不写按 OS 区分的变体脚本)。

参考文件索引

文件 角色
docs/windows/polyglot-hooks.md 本文主体文档:模式动机与决策记录
hooks/run-hook.cmd 权威多语言分发器实现(46 行)
hooks/hooks.json Claude Code 侧 Hook 注册(含 shell: "bash"
hooks/hooks-cursor.json Cursor 侧注册变体(sessionStart 事件)
hooks/session-start 无扩展名 Hook 脚本:上下文注入与 JSON 转义
tests/hooks/test-session-start.sh 分发链路的形态、透传与平台输出测试
scripts/lint-shell.sh Shell 脚本静态检查(ShellCheck + 语法检查)
docs/porting-to-a-new-harness.md 移植指南 Part 7:跨平台约束
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384