症状:Claude Code Hooks 在远程 Mac 不触发,先核对 Claude Code 实际读取的设置文件,以及事件和 matcher 是否匹配;再查运行账户、脚本环境,最后检查返回结果。不要先重装工具或清空配置。
适用条件:你已经能在远程 Mac 启动 Claude Code,但格式化、测试、命令拦截等 Hook 效果没有出现。

负责远程 Mac 开发的你,可以用这套顺序定位配置或触发问题。
维护 CI 的你,可以据此判断 Hook 结果能否支持后续 Xcode 构建或测试验收。
维护共享节点的你,需要特别核对设置作用域、运行账户和组织策略。

先把故障分层:Claude Code Hooks 为什么没有按预期运行?

不要把“没看到效果”直接等同于“Hook 没启动”。它可能在不同环节失效:事件根本没发生、matcher 没匹配、处理脚本未启动,或脚本运行了但没有返回该事件认可的决策。先用一个低风险动作做最小复现,再根据日志确定停在哪一层。

最小复现操作:

  1. 选一个无副作用的测试动作,例如让 Claude Code 调用一次 Write 写入临时文件。
  2. 暂时只保留一个 Hook 事件、一个 matcher 和一个处理脚本,避免多条规则互相干扰。
  3. 在脚本开始处输出一条不含凭据的标记,例如 hook-start,并记录当前工作目录和事件名。
  4. 用 claude --debug-file /tmp/claude-hook-debug.log 指定调试日志位置;也可启动 claude --debug,再检查对应会话日志。Anthropic 文档说明,调试日志记录 Hook 执行细节;需要更细的 matcher 记录时,可设置 CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose。Hooks 调试与日志说明
  5. 先确认 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 上运行于自己的会话中,不应假设它拥有一个可交互的终端。

按这个顺序检查:

  1. 确认账户和目录。 在 Hook 脚本中记录 whoami、pwd、HOME 和 CLAUDE_PROJECT_DIR;必要时检查项目目录是否与 Claude Code 会话的工作区一致。
  2. 检查脚本路径。 尽量使用绝对路径,或按文档使用 ${CLAUDE_PROJECT_DIR} 定位项目脚本。相对路径依赖当前工作目录,换成 Runner 启动后可能指向别处。
  3. 检查执行权限与解释器。 核对脚本文件是否可执行、首行解释器路径是否存在,并单独运行脚本验证语法。
  4. 核对依赖与 PATH。 不要只在交互式终端里确认 jq、xcodebuild 或格式化器可用;在 Hook 自身环境里打印经筛选的 PATH,再使用明确的可执行文件路径复测。
  5. 检查文件权限。 确认账户能读取配置和输入文件,也能写入需要的临时目录、日志目录或测试结果目录。
  6. 避免记录秘密。 诊断日志只保留事件、工作目录、工具名和错误信息,不要输出令牌、签名凭据或 .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 问题的替代品。