症状:Claude Code Hooks 在远程 Mac 不触发,先核对 Claude Code 实际读取的设置文件,以及事件和 matcher 是否匹配;再查运行账户、脚本环境,最后检查返回结果。不要先重装工具或清空配置。
适用条件:你已经能在远程 Mac 启动 Claude Code,但格式化、测试、命令拦截等 Hook 效果没有出现。
负责远程 Mac 开发的你,可以用这套顺序定位配置或触发问题。
维护 CI 的你,可以据此判断 Hook 结果能否支持后续 Xcode 构建或测试验收。
维护共享节点的你,需要特别核对设置作用域、运行账户和组织策略。
先把故障分层:Claude Code Hooks 为什么没有按预期运行?
不要把“没看到效果”直接等同于“Hook 没启动”。它可能在不同环节失效:事件根本没发生、matcher 没匹配、处理脚本未启动,或脚本运行了但没有返回该事件认可的决策。先用一个低风险动作做最小复现,再根据日志确定停在哪一层。
最小复现操作:
- 选一个无副作用的测试动作,例如让 Claude Code 调用一次
Write写入临时文件。 - 暂时只保留一个 Hook 事件、一个 matcher 和一个处理脚本,避免多条规则互相干扰。
- 在脚本开始处输出一条不含凭据的标记,例如
hook-start,并记录当前工作目录和事件名。 - 用
claude --debug-file /tmp/claude-hook-debug.log指定调试日志位置;也可启动claude --debug,再检查对应会话日志。Anthropic 文档说明,调试日志记录 Hook 执行细节;需要更细的 matcher 记录时,可设置CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose。Hooks 调试与日志说明 - 先确认 Hook 是否被识别,再查脚本输出与最终决策。一次只改一个条件,保留每次复测结果。
| 观察到的现象 | 更可能卡在哪一层 | 下一步检查 |
|---|---|---|
| 日志里没有对应事件 | 预期事件没有发生,或配置未加载 | 核对事件类型、会话记录和 /hooks 显示的配置来源 |
| 事件出现,但没有处理器记录 | matcher 或 if 条件未命中 |
对照实际工具名、事件字段与过滤表达式 |
| 有启动记录,却出现找不到文件或命令 | 脚本路径、解释器或账户环境异常 | 核对绝对路径、权限、PATH 和工作目录 |
| 脚本执行成功,但命令没被拦截 | 输出格式、退出码或事件决策字段不对 | 对照该事件的官方输入输出契约 |
| Hook 阻断了工具,但 Xcode 测试仍失败 | Hook 检查与测试验收是两件事 | 查看 xcodebuild 退出状态和测试结果包 |
Claude Code 也提供 /hooks 浏览器,可查看已配置的事件、matcher、处理器和来源文件。先用它确认配置有没有进入当前会话,再把调试日志与实际操作对应起来。
第一步:核对远程会话实际读取的设置文件
远程 Mac 开发环境经常同时有本地终端、SSH 登录、Runner 服务账户和项目工作区。它们看起来操作的是同一份代码,却未必使用同一个用户目录、工作目录或 Claude Code 设置来源。尤其不要假设你在个人账户写入的用户设置,会自动出现在 CI 账户下。
| 设置来源 | 作用范围 | 远程排查重点 |
|---|---|---|
~/.claude/settings.json |
当前账户在这台机器上的项目 | 确认 Claude Code 启动账户的主目录,以及该文件是否存在 |
.claude/settings.json |
项目共享设置 | 确认文件在实际工作区内;需要共享时检查是否已提交 |
.claude/settings.local.json |
当前项目的本地设置 | 检查文件是否只存在于交互账户,或被版本控制忽略 |
| 受管设置 | 组织策略范围 | 向节点或组织管理员确认策略来源及是否限制 Hook |
Anthropic 将用户、项目、项目本地和受管设置区分为不同作用域;项目设置和用户设置的可见范围并不相同。远程运行时实际读到什么,还要看 Claude Code 是由哪个账户、在哪个目录启动。用 /hooks 查看 Hook 来源,再在会话中运行 /status 核对已加载的设置来源;不要只检查你日常登录的那个账户。Claude Code 设置文件作用域
在同一节点分别从交互式终端和 CI 任务执行环境启动时,记录 whoami、pwd、printf '%s\n' "$HOME",并检查目标设置文件。若交互终端能触发、Runner 不行,先对比账户与配置来源,不要把差异归因于 Mac 硬件。
第二步:确认事件与 matcher 对应的是同一次操作
PreToolUse、PostToolUse、Stop 等事件发生在不同生命周期位置,不能互相替代。比如,拦截工具调用应从工具调用前的事件开始验证;在会话结束时运行的 Stop,不能替代针对某次文件写入或命令调用的检查。Anthropic 的事件说明分别列出了触发时机和 matcher 所检查的字段,见前面的 Hooks 文档。
| 目标效果 | 先核对的事件 | matcher 检查内容 | 常见误判 |
|---|---|---|---|
| 工具执行前拦截 | PreToolUse |
工具名,例如 Bash、Edit 或 Write |
把事件名当作工具名来匹配 |
| 工具成功后检查 | PostToolUse |
已成功调用的工具名 | 期待它阻止已经执行的工具 |
| 一轮响应结束后检查 | Stop |
此事件不支持 matcher | 用它匹配 Bash 或某个文件名 |
| 工具失败后处理 | PostToolUseFailure |
失败的工具名 | 误以为成功调用也会触发 |
以 PreToolUse 为例,matcher 应针对工具名,而不是你的脚本名称或命令内容。表达式只包含常见字母、数字、下划线、连字符、空格、逗号或竖线时,可能按精确名称或列表匹配;包含其他字符则会进入正则匹配路径。因此,像 Edit.* 这样的表达式可能匹配到比单一 Edit 更广的工具名;要精确限定时,按官方规则写边界并用实际工具调用复测。
逐步缩小条件:先让单一事件使用空 matcher 或 "*" 验证处理器会不会启动;确认启动后,再加上单一工具名;最后才加 if 等命令条件。若泛匹配能触发、具体 matcher 不触发,问题就在匹配条件,不在远程脚本权限。
⚠️
Stop是轮次结束事件,不是 Xcode 测试事件。要在结束时启动检查,可以让 Hook 调用测试命令;但测试是否通过,仍需读取测试命令本身的退出状态和结果材料。
第三步:对比 SSH 与 Runner 的脚本运行环境
Hook 已进入处理器,但格式化器或测试命令没找到,通常要对比运行上下文,而不是反复改 matcher。远程 SSH 登录、交互式 shell 与 Runner 服务账户可能具有不同的 PATH、启动目录、解释器和文件权限。Claude Code 的命令 Hook 在 macOS 和 Linux 上运行于自己的会话中,不应假设它拥有一个可交互的终端。
按这个顺序检查:
- 确认账户和目录。 在 Hook 脚本中记录
whoami、pwd、HOME和CLAUDE_PROJECT_DIR;必要时检查项目目录是否与 Claude Code 会话的工作区一致。 - 检查脚本路径。 尽量使用绝对路径,或按文档使用
${CLAUDE_PROJECT_DIR}定位项目脚本。相对路径依赖当前工作目录,换成 Runner 启动后可能指向别处。 - 检查执行权限与解释器。 核对脚本文件是否可执行、首行解释器路径是否存在,并单独运行脚本验证语法。
- 核对依赖与
PATH。 不要只在交互式终端里确认jq、xcodebuild或格式化器可用;在 Hook 自身环境里打印经筛选的PATH,再使用明确的可执行文件路径复测。 - 检查文件权限。 确认账户能读取配置和输入文件,也能写入需要的临时目录、日志目录或测试结果目录。
- 避免记录秘密。 诊断日志只保留事件、工作目录、工具名和错误信息,不要输出令牌、签名凭据或
.env内容。
Anthropic 的安全建议也明确要求校验输入、给 shell 变量加引号、使用绝对路径,并避开 .env、密钥等敏感文件。远程执行时应把这些要求作为脚本审查项,而不是把日志越写越详细来代替权限控制。
如果 Hook 要启动 xcodebuild test,还要把“命令是否存在”和“测试能否在该会话环境运行”分开。Apple 的归档测试指南提到,SSH 下的 Xcode 测试可能受图形会话环境影响;这份资料较早,不能直接视为所有当前节点的保证,但足以提醒你在目标账户和实际执行方式下验证模拟器测试,而不是仅凭交互终端里的成功结果推断 Runner 也可运行。Apple 关于 SSH 与 Xcode 测试的归档指南
第四步:检查脚本输出是否真的形成了决策
脚本启动、脚本退出和 Claude Code 采取行动,是三个不同结果。命令 Hook 从标准输入接收 JSON;Claude Code 根据事件契约读取退出码、标准输出和 JSON 字段。输出了普通文本,不代表工具会被阻断;返回成功退出码但没有决策,也不等于明确批准或拒绝。相关输入输出规则见前面的 Hooks 文档。
| 验收目标 | 应核对的返回 | 容易踩的坑 |
|---|---|---|
| 只记录或检查,不作阻断决定 | 成功退出;按需写入诊断信息 | 把“没有 JSON 决策”理解成“已批准” |
PreToolUse 明确阻止工具 |
使用该事件认可的阻断方式,例如有效决策 JSON 或阻断退出码 | 使用普通非零退出码,以为它必然阻断 |
| 提醒模型继续检查 | 返回该事件允许的 JSON 字段 | JSON 字段拼写、事件名或结构不符合契约 |
| 任务结束后验证测试 | Stop 中启动检查,并把测试结果独立留档 |
把 Stop Hook 执行成功当成测试通过 |
对于多数事件,退出码 2 表示阻断;退出码 1 并不自动等同于阻断。对 PreToolUse,也可以按官方格式返回 permissionDecision 等字段。不同事件的处理规则有差异,因此要针对具体事件核对决策字段,而不是把某一种退出码或 JSON 结构套到所有 Hook 上。权限配置本身另有独立规则;Hook 不能替代操作系统权限、人工审批或 CI 的强制验收。需要核对工具权限时,参照 Claude Code 权限配置说明。
调试时分别记录四个状态:Hook 是否匹配、进程是否启动、进程退出码与 JSON 是否有效、Claude Code 是否应用决策。这样可以区分 Hook 错误、Hook 未匹配、脚本失败,以及 Hook 之后的工具或 CI 步骤失败。
第五步:把 Xcode 测试证据与 Hook 验收分开
Stop Hook 可以在 Claude Code 一轮响应结束时启动 xcodebuild test,但它本身并不证明测试成功。应检查测试命令退出状态,并保留 Xcode 生成的结果包;Apple 文档说明,终端执行 xcodebuild 测试会生成包含测试会话结果及相关日志的 .xcresult 文件。需要在持续集成中正式验收时,应由 CI 步骤检查测试结果,并保存可追溯的构建日志和结果材料。Apple:运行测试并解读结果
| 验收层 | 验证动作 | 通过证据 | 不能据此推出什么 |
|---|---|---|---|
| Hook 触发 | 执行能触发目标事件的最小操作 | 调试日志有匹配与处理记录 | 不能证明测试通过 |
| Hook 决策 | 使用预期通过、预期阻断两类输入复测 | 退出码、JSON 与最终工具行为一致 | 不能证明宿主机权限受到隔离 |
| Xcode 测试 | 在目标账户执行对应测试方案 | 命令状态、测试日志和 .xcresult |
不能由 Hook 的启动成功替代 |
| CI 验收 | 在受控流水线执行正式构建与测试 | CI 任务状态及归档结果 | 不能假定交互式 SSH 环境等于 Runner |
若 xcodebuild 命令本身不可用,先确认 Xcode 命令行工具已安装且活动开发者目录指向目标 Xcode;Apple 的命令行工具参考列出了 xcodebuild 和读取 .xcresult 的 xcresulttool。Apple:Xcode 命令行工具参考
正式验收时,分别验证预期通过和预期失败的测试,再在节点重启后复测一次。CI 中也要确认测试失败会让对应任务失败,而不是只把 Hook 的日志归档后仍标记为成功。构建、测试与结果归档应在对应的 CI 步骤中分别验收,不能用 Hook 的启动记录代替这些证据。
如果 SSH 和 Runner 账户的配置难以保持一致,先把共享 Hook 放进项目设置,把账户私有覆盖留在本地设置,并由 CI 对必须通过的 Xcode 检查作最终判定。你也可以先阅读远程 Mac 开发环境与节点配置,确认目标环境是否适合复现真实 macOS 工具链。
对于只需临时复现、验证远程账户或运行 Xcode 工具链的项目,远程 Mac 可省去本地机器长期占用,也避免把 macOS 专属验证硬塞进 Linux 环境;但若你需要长期、稳定的高负载节点,或依赖必须直连的物理设备,自购 Mac 往往更合适。确定需要真实远程 macOS 环境再复现时,可先了解 MACCOME 远程 Mac 使用方案,按项目周期评估,不要把租用当成所有 CI 问题的替代品。