hooks 对照:Claude Code 与 Cursor 各自在哪些时点开了口子

2026-08-18

要给 AI 编程工具挂一段自己的脚本——提交前扫一遍密钥、写完文件跑 formatter、危险命令直接拦下——真正卡住人的往往不是脚本怎么写,而是「我想插手的那个时点,它到底开没开口子」。这个问题只有翻文档能回答,而两家文档的组织方式差别很大:一家把事件铺成一张长表,一家按触发面分成三类。

这篇把两边的事件点清单摆在一起数一遍。数字都是回源数过的:Claude Code 官方文档《Hooks reference》页(code.claude.com/docs/en/hooks)的生命周期表列了 31 个事件,且该页下方正好有 31 个同名的事件小节;Cursor 官方文档《Hooks》页(cursor.com/docs/hooks)在「Hook categories」一节把事件分成三类,Agent hooks 18 个、Tab hooks 2 个、App lifecycle hooks 1 个,合计 21 个。

先说能对上号的那一层

抛开命名风格(一边大驼峰,一边小驼峰),两边有一批事件在语义上是能对读的:

你想插手的时点Claude CodeCursor
会话开始 / 结束SessionStart / SessionEndsessionStart / sessionEnd
用户提交提示词之后、请求发出之前UserPromptSubmitbeforeSubmitPrompt
任一工具调用之前PreToolUsepreToolUse
工具调用成功之后 / 失败之后PostToolUse / PostToolUseFailurepostToolUse / postToolUseFailure
subagent 启动 / 结束SubagentStart / SubagentStopsubagentStart / subagentStop
上下文压缩之前PreCompactpreCompact
agent 这一轮结束Stopstop

Cursor 文档在「Troubleshooting」一节里写了一句话,能解释这种相似为什么不是巧合:命令 hook 用退出码 2 阻断动作,「这与 Claude Code 的行为一致,以便兼容」。同一页的环境变量表里还列了 CLAUDE_PROJECT_DIR,说明写的是「项目目录的别名(Claude 兼容)」。Cursor 另有一页《Third Party Hooks》(cursor.com/docs/reference/third-party-hooks)讲从 Claude Code 之类第三方工具加载 hooks 的兼容性与配置——本文没有展开读那一页,只指出它存在。

再说只有一边有的那一层

这才是选型时会咬到你的地方。

Cursor 有、我们在 Claude Code 文档里没找到对应说明的:

  • beforeTabFileRead / afterTabFileEdit。这是 Tab(内联补全)自己的读文件与改文件事件。Cursor 文档明说它「只由 Tab 触发,不由 Agent 触发」,用途是「对自动进行的 Tab 操作套用另一套策略」。Claude Code 文档里我们没有找到针对内联补全的独立事件,这一点不比。
  • afterAgentThought。Cursor 把它描述为「agent 完成一个 thinking block 之后」触发。Claude Code 有一个 MessageDisplay,文档写的是「助手消息文本显示期间」,落点是消息文本而不是思考块,两者不是一回事,也不比。
  • workspaceOpen。Cursor 文档写它「在 Cursor 打开工作区时以及每次工作区文件夹变化时触发」,且「在任何 agent 会话之外」,输出可以返回 pluginPaths 给当前工作区加载额外的 plugin 目录。

Claude Code 有、我们在 Cursor 文档里没找到对应说明的(挑影响面大的几个):

  • PermissionRequestPermissionDenied。前者在「一次工具调用需要一个权限决定时」触发,后者在「auto 模式拒绝了一次工具调用时」触发。Cursor 侧的权限判定是写在 preToolUsebeforeShellExecution 这些事件的返回值里的(permission 字段),文档里没有单独的权限事件,这一层不比。
  • WorktreeCreate / WorktreeRemove。Claude Code 文档写明,配置了 WorktreeCreate hook 会替换掉默认的 git 行为,可以改用 SVN、Perforce、Mercurial 这类别的版本控制系统;同时提醒因为默认行为被整个替换,.worktreeinclude 不再被处理,要把 .env 之类本地配置拷进新 worktree 得在自己脚本里做。
  • FileChanged。它的 matcher 身兼两职:值按 | 切开,每一段注册成工作目录下的字面文件名(文档举的例子是 ".envrc|.env" 正好监听这两个文件),同时又按常规 matcher 规则过滤哪些 hook 组会跑。文档特意警告这里写正则没意义——^\.env 会去监听一个真叫 ^\.env 的文件。
  • Elicitation / ElicitationResult(MCP 服务器在工具调用中途要用户输入时)、InstructionsLoadedCLAUDE.md.claude/rules/*.md 被载入上下文时)、ConfigChangeCwdChangedDirectoryAddedSetupUserPromptExpansionPostToolBatchStopFailurePostCompactTeammateIdleTaskCreated / TaskCompleted

反过来还有一处结构性差异值得单独拎出来:Cursor 为 shell、MCP、读文件、改文件各配了专用事件(beforeShellExecution / afterShellExecutionbeforeMCPExecution / afterMCPExecutionbeforeReadFileafterFileEdit),Claude Code 则统一走 PreToolUse / PostToolUse 再靠 matcher 和 if 收窄。这不是谁多谁少的问题,是同一件事写法不同——下面这段决策路径就从这儿开始。

从你的处境倒推:怎么挑挂载点

处境一:我要拦住危险的 shell 命令。

Cursor 侧挂 beforeShellExecution,它的 matcher 直接匹配命令字符串本身。文档原样给的例子:

{
  "hooks": {
    "beforeShellExecution": [
      {
        "command": "./scripts/approve-network.sh",
        "timeout": 30,
        "matcher": "curl|wget|nc"
      }
    ]
  }
}

Claude Code 侧挂 PreToolUse,matcher 匹配的是工具名,要收窄到具体命令得再加一个 if,用的是权限规则语法:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

以上两段均原样引自各自官方文档,未经实测,以官方文档与实际运行结果为准。

这里有个必须读到的限定:Claude Code 文档写明 if 只在五个工具事件(PreToolUsePostToolUsePostToolUseFailurePermissionRequestPermissionDenied)上求值,在其它事件上,设了 if 的 hook 永远不会跑;而且 if 对 Bash 命令的匹配是尽力而为的,命令解析不了时会 fail open 照跑不误,文档因此直说:要硬性的允许或拒绝,用权限系统,别用 hook。

处境二:hook 自己挂了怎么办。

这一维两边都有明确说法,而且都偏向「放行」。Cursor 文档写命令 hook 的退出码语义是:0 用输出、2 阻断(等价于返回 permission: "deny")、其它退出码算 hook 失败,动作继续,默认 fail-open;要反过来就在 hook 定义上加 failClosed: true,文档说这对安全关键的 beforeMCPExecution 尤其推荐。Claude Code 侧的结论一致但拆得更细:非 2 的退出码在多数事件上是「非阻断错误」,动作照常进行;超时的 command / http / mcp_tool hook 会被取消、不产生任何决定,文档明写「别指望一个卡住的 hook 能当闸门」。

顺带一个容易翻车的点:Claude Code 文档专门提醒,脚本路径写错、文件不可执行时,shell 会以 127 之类的码退出,文档写明这时给出的是同一种非阻断错误提示,动作照常放行。也就是说一条打错路径的策略 hook 会静默失效,第一次跑的时候得盯着这条提示看。

处境三:我要让 agent 自动接着跑下一轮。

两边都能做,做法不一样。Cursor 的 stop 事件输出一个 followup_message 字符串,非空时会被自动当成下一条用户消息提交;配套有个 loop_limit 选项限制自动跟进次数,文档写明 Cursor 自家 hook 有一个默认上限、而按 Claude Code 兼容方式加载的 hook 默认是 null(不限)——这是文档写明的默认值,随版本可能变动。Claude Code 侧走的是另一条路:Stop 是「能阻断」的事件之一,退出码 2 的效果是「阻止 Claude 停下、让对话继续」。

处境四:我要在云端跑的 agent 上也套同一套策略。

Cursor 文档给了一张明确的支持表:云端 agent 会跑仓库里的 .cursor/hooks.json,表里逐条列出 14 个可用的 hook;不可用的另有一张表并附了原因——sessionStart / sessionEndbeforeMCPExecution / afterMCPExecutionbeforeTabFileRead / afterTabFileEditworkspaceOpen。此外云端只跑 command 型 hook,prompt 型不跑,文档说的理由是 prompt hook 需要 hook 与 agent 循环之间的认证连线,云端执行环境里没有。还有一句很实在的提醒:云端 agent 早期的探索性回合有时跑在只读环境里,那些回合不跑 hook。用户级的 ~/.cursor/hooks.json 在云端也不可用。

Claude Code 侧对应的说法在配置一节:Claude Code on the web 的云会话不读你本地的 ~/.claude/settings.json,那边的 hook 来自仓库和组织的服务端托管设置。两边都是「哪些配置源不生效」的清单,但 Cursor 那张逐事件的支持表在 Claude Code 文档里没有对应物,这一层的粒度不比。

配置源与优先级:都是多层合并,合并规则不同

Cursor 的层级是四级,文档写明优先级从高到低是 Enterprise → Team → Project → User,并说「所有来源里匹配的 hook 都会跑,响应冲突时按优先级合并」。Enterprise 那一级是 MDM 管理的系统级文件,Windows 上的路径文档写的是 C:\ProgramData\Cursor\hooks.json。工作目录随来源变:项目 hook 从项目根目录跑,用户 hook 从 ~/.cursor/ 跑——文档反复强调这一点,因为路径写成 ./hooks/x.sh 还是 .cursor/hooks/x.sh 取决于它。

Claude Code 的配置源列了七处:~/.claude/settings.json.claude/settings.json.claude/settings.local.json、托管策略设置、plugin 的 hooks/hooks.json、skill frontmatter、subagent frontmatter。文档写明这些条目是合并而不是相互替换的,且 disableAllHooks 从用户、项目、本地这三级设都关不掉托管策略里的 hook。企业管理员还有个 allowManagedHooksOnly 可以把用户、项目、本地和 plugin 的 hook 一并挡掉。

处理器类型上,Claude Code 列了五种:commandhttpmcp_toolpromptagent,其中文档明确标注 agent hooks 是 experimental、可能变动。Cursor 列了两种:command(默认)与 prompt(用 LLM 判断自然语言条件,可用 model 字段覆盖默认模型)。

Windows 侧

这一维两边文档给的东西不对等,得分开说。

Claude Code 文档有专门的「Windows PowerShell tool」一节:可以在单个 command hook 上设 "shell": "powershell",会自动探测 pwsh.exe 并回落到 powershell.exe。引用项目根目录时要写 ${CLAUDE_PROJECT_DIR}$env:CLAUDE_PROJECT_DIR;文档明确警告不要写裸的 $CLAUDE_PROJECT_DIR,PowerShell 会把它当未定义局部变量解析成 $null,脚本路径就丢了项目根前缀。占位符改写到 ${env:NAME} 形式这件事,文档写明自 v2.1.198 起对 settings.json 里的 hook 也生效,更早的版本上只对 plugin hook 生效。另有一条 Windows 专属的坑:exec 形式(设了 args)要求 command 解析到真正的可执行文件,npm、npx、eslint 装在 node_modules/.bin 下的 .cmd / .bat 垫片不是可执行文件、没有 shell 就 spawn 不起来,文档给的做法是直接用 node 调底层脚本,或者干脆改用 shell 形式。

Cursor 文档在 Windows 上给到的是企业分发路径(C:\ProgramData\Cursor\hooks.json),示例脚本是 bash、Python 和 TypeScript。我们在 Cursor 的 hooks 文档里没有找到「在 Windows 上为 hook 指定 shell」的配置项说明,这一点不比。

落到一句话

如果你要挂的是「工具调用前后 + 会话首尾 + 结束续跑」这类通用位置,两边都够用,剩下的差别主要在写法:一个靠专用事件名,一个靠通用事件加 matcher 与 if。真正会把你逼到某一边的是边角:内联补全的读写策略只在 Cursor 文档里有,权限判定层的独立事件、worktree 创建的接管、CLAUDE.md 载入与文件监听这些只在 Claude Code 文档里有。两边都白纸黑字写了 hook 失败时默认放行,所以无论选哪边,都别把 hook 当成安全闸门用。

两个产品的迭代都很快,本文涉及的事件名、字段名与默认值随版本变动,请以各自官方文档最新内容为准。


本文依据 Claude Code 官方文档(code.claude.com/docs)于 2026-08-17 的公开内容整理。 该产品闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观、操作手感与运行速度的任何描述。 该产品迭代频繁,文中涉及的命令、配置项与默认值随版本变动,请以官方文档最新内容为准。 本文不涉及价格、额度与限流的具体数值,相关信息请以官方定价与用量说明页为准。

本文涉及的另一方内容依据其官方文档整理(Cursor:cursor.com/docs)。 双方均为闭源商业产品,本文只对照各方公开写明的机制,不推断实现,也不对产品做优劣排名

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。