面向 Claude Code 的跨平台多语言钩子¶
Claude Code 插件需要能在 Windows、macOS 和 Linux 上工作的钩子。本文档描述了 hooks/run-hook.cmd 中使用的单一通用调度器模式。
权威来源:
hooks/run-hook.cmd是规范实现。本文档与代码不一致时,以代码为准。
问题¶
Claude Code 通过 shell 运行钩子命令: - macOS/Linux:bash 或 sh - 安装了 Git Bash 的 Windows:Git Bash - 未安装 Git Bash 的 Windows:PowerShell(旧版本使用 CMD.exe)
这两种 Windows 回退 shell 都无法解析我们的命令字符串:PowerShell 将开头带引号的路径视为字符串表达式,并在下一个裸词处报错;而 CMD.exe 的 /c 引用规则会在路径包含元字符(如 ()时剥掉外层引号。因此,我们的钩子声明了 "shell": "bash"(自 Claude Code 2.1.81 起支持;旧版本会忽略该键),这会强制走 Git Bash 路径;当没有 Git Bash 时,会产生一条可操作的“为 Windows 安装 Git”错误,而不是 shell 解析器失败。
这带来了几个挑战:
- 脚本执行:Windows CMD 无法直接执行
.sh文件 - 路径格式:Windows 使用反斜杠(
C:\path),Unix 使用正斜杠(/path) - 环境变量:
$VAR语法在 CMD 中不生效 .sh自动前置:Windows 上的 Claude Code 会自动为路径中包含.sh的任何命令前置bash—— 如果脚本带有扩展名,就会干扰调度器。
解决方案:无扩展名脚本 + 单一通用调度器¶
仓库使用一个通用的 run-hook.cmd 调度器来处理所有钩子。钩子脚本是无扩展名的(session-start,而不是 session-start.sh)。这是有意为之:它可以防止 Claude Code 在 Windows 上的自动检测机制在调度器命令前前置 bash 从而导致其损坏。
文件结构¶
hooks/
├── hooks.json # Points to run-hook.cmd with extensionless script name
├── run-hook.cmd # Cross-platform dispatcher (the polyglot wrapper)
└── session-start # Actual hook logic — extensionless bash script
hooks.json¶
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"shell": "bash",
"async": false
}
]
}
]
}
}
路径被加上引号,是因为 ${CLAUDE_PLUGIN_ROOT} 可能包含空格。
run-hook.cmd 的高层工作原理¶
run-hook.cmd 是一个多语言脚本:Windows 将第一个块视为批处理命令,而 Unix shell 将该块视为无操作(no-op)的 heredoc,并在其后继续执行。
不要从本文档复制实现。修改调度器时,请直接阅读 hooks/run-hook.cmd,然后运行 tests/hooks/test-session-start.sh。
Windows(CMD.exe)上的工作原理¶
- 批处理部分验证脚本名称,并根据调度器自身的位置解析钩子目录。
- 它依次在四个位置查找 bash:
C:\Program Files\Git\bin\bash.exeC:\Program Files (x86)\Git\bin\bash.exe%LOCALAPPDATA%\Programs\Git\bin\bash.exe(适用于 Windows 的 Git 的按用户安装),仅在设置了LOCALAPPDATA时使用PATH上的bash(MSYS2、Cygwin 或非默认 Git 安装),通过运行%SystemRoot%\System32\where.exe $PATH:bash找到,因此where和bash都不会从当前目录获取。WSL 启动器(System32、Sysnative或%LOCALAPPDATA%\Microsoft\WindowsApps中的bash.exe)以及无扩展名的匹配会被跳过,因为当未安装 Linux 发行版时,这些启动器会失败。- 如果找到 bash,它会从钩子目录运行指定的无扩展名钩子脚本,并以该脚本的退出码退出。
- 如果未找到 bash,调度器会静默地以
0退出——插件继续工作,只是跳过钩子。 exit /b使 CMD 在到达 Unix 部分之前停止。
Unix(bash/sh)上的工作原理¶
: << 'CMDBLOCK'在无操作命令上打开一个 heredoc。- 整个 CMD 批处理块被 heredoc 消费并忽略。
- 在
CMDBLOCK之后,bash 解析脚本目录,并直接exec指定的无扩展名脚本。
关键设计决策¶
| 决策 | 原因 |
|---|---|
| 无扩展名脚本 | 防止 Claude Code 在 Windows 上的 .sh 自动前置机制干扰调度器命令 |
不使用 -l(登录 shell) |
不需要;钩子脚本应当自包含,不依赖登录 shell 的 PATH 设置 |
不使用 cygpath |
bash 直接接收 Windows 路径并正确处理;旧的 -c "..." 调用模式需要 cygpath,直接 exec 则不需要 |
| 未找到 bash 时静默退出 | 避免为没有安装 Git for Windows 的用户破坏插件;钩子上下文注入会被优雅地跳过 |
编写跨平台钩子脚本¶
你的钩子逻辑放在无扩展名的脚本文件中。一些可移植的模式:
应该这样做¶
- 尽可能使用纯 bash 内置命令
- 使用
$(command)而不是反引号 - 引用所有变量展开:
"$VAR"
避免这样做¶
- 在没有回退方案的情况下依赖与 PATH 相关的工具(钩子运行时没有
-l,因此不会设置登录 shell 的 PATH) - 给脚本添加
.sh扩展名——这会触发 Claude Code 在 Windows 上的自动前置机制
示例:不使用外部工具进行 JSON 转义¶
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"
}
故障排查¶
“bash is not recognized”¶
CMD 在调度器尝试的四个位置中都找不到 bash。调度器以静默方式退出(0)而不是报错,因此该 hook 被跳过。请安装适用于 Windows 的 Git(系统范围或按用户),或确保非 WSL 的 bash 位于 PATH 中。
Hook 在 Unix 上运行,但在 Windows 上不执行任何操作¶
检查 hooks.json 中的脚本文件名是否没有扩展名。像 run-hook.cmd session-start.sh 这样的命令可能会触发 Claude Code 的 .sh 自动检测,从而绕过预期的 CMD 调度器路径,或者只是尝试运行一个不存在的 session-start.sh 脚本。
Hook 完全不触发¶
验证 hooks.json 中的 matcher 是否与你的宿主程序所发出的事件类型匹配。Claude Code 使用 startup|clear|compact;Cursor 使用 sessionStart。请查看 hooks-cursor.json 以获取 Cursor 变体。
相关问题¶
- anthropics/claude-code#9758 —
.sh脚本在 Windows 上会在编辑器中打开 - anthropics/claude-code#3417 — Hook 在 Windows 上不工作
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 su/superpowers