hooks 对照:Claude Code 与 Cursor 各自在哪些时点开了口子
要给 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 Code | Cursor |
|---|---|---|
| 会话开始 / 结束 | SessionStart / SessionEnd | sessionStart / sessionEnd |
| 用户提交提示词之后、请求发出之前 | UserPromptSubmit | beforeSubmitPrompt |
| 任一工具调用之前 | PreToolUse | preToolUse |
| 工具调用成功之后 / 失败之后 | PostToolUse / PostToolUseFailure | postToolUse / postToolUseFailure |
| subagent 启动 / 结束 | SubagentStart / SubagentStop | subagentStart / subagentStop |
| 上下文压缩之前 | PreCompact | preCompact |
| agent 这一轮结束 | Stop | stop |
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 文档里没找到对应说明的(挑影响面大的几个):
PermissionRequest与PermissionDenied。前者在「一次工具调用需要一个权限决定时」触发,后者在「auto 模式拒绝了一次工具调用时」触发。Cursor 侧的权限判定是写在preToolUse、beforeShellExecution这些事件的返回值里的(permission字段),文档里没有单独的权限事件,这一层不比。WorktreeCreate/WorktreeRemove。Claude Code 文档写明,配置了WorktreeCreatehook 会替换掉默认的 git 行为,可以改用 SVN、Perforce、Mercurial 这类别的版本控制系统;同时提醒因为默认行为被整个替换,.worktreeinclude不再被处理,要把.env之类本地配置拷进新 worktree 得在自己脚本里做。FileChanged。它的matcher身兼两职:值按|切开,每一段注册成工作目录下的字面文件名(文档举的例子是".envrc|.env"正好监听这两个文件),同时又按常规 matcher 规则过滤哪些 hook 组会跑。文档特意警告这里写正则没意义——^\.env会去监听一个真叫^\.env的文件。Elicitation/ElicitationResult(MCP 服务器在工具调用中途要用户输入时)、InstructionsLoaded(CLAUDE.md或.claude/rules/*.md被载入上下文时)、ConfigChange、CwdChanged、DirectoryAdded、Setup、UserPromptExpansion、PostToolBatch、StopFailure、PostCompact、TeammateIdle、TaskCreated/TaskCompleted。
反过来还有一处结构性差异值得单独拎出来:Cursor 为 shell、MCP、读文件、改文件各配了专用事件(beforeShellExecution / afterShellExecution、beforeMCPExecution / afterMCPExecution、beforeReadFile、afterFileEdit),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 只在五个工具事件(PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied)上求值,在其它事件上,设了 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 / sessionEnd、beforeMCPExecution / afterMCPExecution、beforeTabFileRead / afterTabFileEdit、workspaceOpen。此外云端只跑 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 列了五种:command、http、mcp_tool、prompt、agent,其中文档明确标注 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)。
双方均为闭源商业产品,本文只对照各方公开写明的机制,不推断实现,也不对产品做优劣排名。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。