在 CLI Agent 里挂 MCP server
在终端 Agent 里挂 MCP server 的两种方式 —— claude mcp add 命令 vs settings.json 配置文件,含 stdio 与远程 server 的踩坑要点
理解 Claude Code 的生命周期 hook(PreToolUse / PostToolUse / Stop / Notification 等),何时该用、典型用例与避坑
内容摘要
Claude Code 的 **Lifecycle Hooks(生命周期钩子)**,可以理解成在 Agent 跑的过程中预埋的若干个"开关位"。每当 Agent 走到某个关键节点——比如"准备调一个工具"、"工具刚跑完"、"用户输入还没进 LLM"、"会话即将结束"——Claude Code 这个壳就会去检查 `settings.json`,看你有没有为这个节点注册脚本。注册了,它就把当前节点的全部上下文(工具名、参数、cwd、session_id 等)按 JSON 喂到脚本的标准输入;脚本怎么处理、要不要拦截,由你写的逻辑决定。
Claude Code 的 Lifecycle Hooks(生命周期钩子),可以理解成在 Agent 跑的过程中预埋的若干个"开关位"。每当 Agent 走到某个关键节点——比如"准备调一个工具"、"工具刚跑完"、"用户输入还没进 LLM"、"会话即将结束"——Claude Code 这个壳就会去检查 settings.json,看你有没有为这个节点注册脚本。注册了,它就把当前节点的全部上下文(工具名、参数、cwd、session_id 等)按 JSON 喂到脚本的标准输入;脚本怎么处理、要不要拦截,由你写的逻辑决定。
为什么要单独做"hooks",而不是让 Claude 自己在 prompt 里"记得"在某些时刻去做某事?两个原因:
一句话说人话:hook = "Claude 不参与的、由 CLI 这一侧自动触发的脚本"。它不是给 Claude 用的工具,而是 Claude Code 这个外壳给你提供的"自动化插槽"。
| 要素 | 作用 |
|---|---|
| 事件(event) | 一个生命周期触发点,比如 PreToolUse、PostToolUse、Stop、UserPromptSubmit、SessionStart |
| 匹配器(matcher) | 缩小触发范围,例如 "Bash" 只在调 Bash 工具时触发,"Write|Edit" 同时匹配两种文件写入 |
| 处理器(hook handler) | 真正被调起的脚本(type: "command")或 HTTP 端点(type: "http") |
| stdin(输入) | Claude Code 通过标准输入传入完整事件 JSON:tool_name、tool_input、cwd、session_id 等 |
| 退出码 / stdout | exit 0 + 可选 JSON = 正常;exit 2 = 阻断(仅对部分事件生效);其它非零 = 报警但不阻断 |
| 作用域 | ~/.claude/settings.json(用户全局)、.claude/settings.json(项目共享)、.claude/settings.local.json(项目私有,不入库) |
下图画出一次"用户提问 → Claude 决定调 Bash → 工具执行 → Claude 输出 → 结束"完整 turn 中各 hook 的触发位置:
注意几个细节:PreToolUse 在工具真正跑之前,所以它是唯一能"拦下危险命令"的位置;PostToolUse 在工具已经成功执行之后,它能给 Claude 反馈但已经无法撤销操作——这是新手最常踩的坑(详见"常见误区")。
写一个 hook 一共三步:① 在 settings.json 里登记事件 + 匹配器;② 写脚本读 stdin 里的事件 JSON;③ 用 exit code 或 stdout JSON 控制后续行为。
在项目根目录建 .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-format.sh"
}
]
}
]
}
}
这段配置的含义:每当 Claude 调用 Write 或 Edit 工具改完文件后,自动跑 .claude/hooks/auto-format.sh。$CLAUDE_PROJECT_DIR 是 Claude Code 启动时注入的环境变量,指向项目根,路径鲁棒性比写死好。
新建 .claude/hooks/auto-format.sh,给它执行权限(chmod +x):
#!/bin/bash
# PostToolUse: 给刚刚被 Write/Edit 的文件按扩展名自动格式化
# Claude Code 通过 stdin 传入完整事件 JSON,这里用 jq 取出文件路径
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
case "$FILE_PATH" in
*.py) ruff format "$FILE_PATH" ;;
*.ts|*.tsx|*.js|*.jsx) npx prettier --write "$FILE_PATH" ;;
*.go) gofmt -w "$FILE_PATH" ;;
*) exit 0 ;; # 其他扩展名直接跳过
esac
# 把 lint 错误(如果有)回灌给 Claude
if ! ruff check "$FILE_PATH" 2>/dev/null; then
echo "ruff 发现风格问题,请修复" >&2
fi
exit 0
预期效果:你让 Claude 改一个 app.py,Edit 工具刚执行完,Claude 还没开始下一句话,hook 已经跑完 ruff format,文件就已经是规范风格了;如果 lint 有问题,stderr 会被自动注入 Claude 的下文,让模型在下一步回复时主动修复。
PreToolUse 是少数几个能"真的阻止 Claude 做某事"的事件之一。下面这个脚本会拦截一切 rm -rf 命令:
#!/bin/bash
# .claude/hooks/block-dangerous-bash.sh
# 注册到 PreToolUse + matcher: "Bash"
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# 黑名单:rm -rf、sudo、curl | sh 等
if echo "$COMMAND" | grep -qE '(rm\s+-rf|sudo|curl\s+[^|]+\|\s*sh)'; then
# 用 hookSpecificOutput 给 PreToolUse 显式 deny
jq -n --arg cmd "$COMMAND" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: ("拦截高危命令:" + $cmd + "。请改用更安全的方案。")
}
}'
exit 0
fi
exit 0
对应 settings.json 片段:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-bash.sh" }
]
}
]
}
}
效果:模型若生成了 rm -rf dist/,命令根本不会跑到 shell,原因会回灌给模型,让它换一种方式(比如逐文件删)。这比依赖"我让 Claude 别用 rm -rf"的提示词靠谱得多。
速查:PreToolUse 控制工具执行的标准做法是
hookSpecificOutput.permissionDecision,可选值"allow" | "deny" | "ask" | "defer",多个 hook 同时触发时优先级deny > defer > ask > allow。
| 事件 | 触发时机 | 能否阻断 | 典型用途 |
|---|---|---|---|
SessionStart | 启动 / resume / 清屏 / 压缩后 | 否 | 注入项目背景、最近 issue、环境变量 |
UserPromptSubmit | 用户按下回车后、prompt 进 LLM 前 | ✅ | 敏感词拦截、自动补充上下文 |
PreToolUse | 模型生成完工具参数、工具执行前 | ✅ | 拦截危险命令、改写参数、按规则放行 |
PostToolUse | 工具成功返回后 | 否(无法撤销) | 自动格式化、跑 lint、写审计日志 |
PostToolUseFailure | 工具执行失败后 | 否 | 补充错误诊断上下文 |
Stop | Claude 结束本轮回复时 | ✅(可让它继续说) | 强制跑测试再收尾、待办未完成时阻止退出 |
SubagentStop | subagent 任务结束 | ✅ | 验收 subagent 产出 |
PreCompact / PostCompact | 上下文压缩前 / 后 | PreCompact 可阻断 | 压缩前转储重要状态、压缩后注入摘要 |
Notification | Claude Code 弹通知(如等权限) | 否 | 推到桌面、机器人、企业微信 |
SessionEnd | 会话结束 | 否 | 上传日志、写 token 计费、清理临时文件 |
CLI Agent 周边有不少"自动化"机制,初学者常分不清。下面这张表把 hooks 和最容易混的几个邻居放一起对比:
| 维度 | Lifecycle Hooks | Skills | MCP 工具 | Cursor Rules / CLAUDE.md |
|---|---|---|---|---|
| 触发方 | CLI 这一侧自动触发(事件驱动) | Claude 自己根据情境显式调用 | Claude 自己显式调用(Tool Use) | 在每次推理时作为提示注入 |
| 是否进入 LLM 推理 | ❌ 不进 | ✅ Skill 描述 + 内容会进上下文 | ✅ Tool 描述会进上下文 | ✅ 全文注入 |
| 是否消耗 token | ❌ | ✅ | ✅ | ✅ |
| 能否阻断模型行为 | ✅(PreToolUse / Stop 等可硬拦) | ❌ 只能引导 | ❌ 只是提供工具 | ❌ 提示性 |
| 谁决定执行时机 | CLI(确定性) | 模型(概率性) | 模型(概率性) | 模型(概率性) |
| 适合做什么 | 强制 lint、安全拦截、审计、通知 | 复杂工作流的可重用"剧本" | 接外部数据源、调远程服务 | 写编码规范、风格约定 |
| 不适合做什么 | "让模型在这里做点决策"(hook 里没 LLM) | 强约束(模型可能忽略) | 同上 | 任何需要确定性的动作 |
核心区别一句话:hooks 是 CLI 外壳的事件回调,确定性、零 token、能硬拦;Skills/MCP/Rules 是 喂给模型的上下文或工具,灵活但不可靠。要"必须执行"用 hooks,要"灵活执行"用 Skills/MCP,要"风格约定"用 CLAUDE.md。互相不能替代。
互链:MCP 和 hooks 的关系详见 /mcp/cards/what-is-mcp;Skills 和 hooks 怎么搭配见 /skills/cards 下相关卡。
| 误区 | 准确理解 |
|---|---|
| 以为 hook 能阻止 Claude "想"做某事 | hook 拦的是工具调用,不是模型推理。模型仍然会想,hook 只是不让对应动作发生在你机器上。想限制模型思考,靠的是 prompt 和权限规则,不是 hook |
| 以为可以在 hook 里调 Claude 模型 | hook 跑在 Claude Code 进程之外、不在 LLM 推理上下文。脚本里没有"反过来问 Claude"的官方通道;要让模型介入只能写 decision: "block" 把控制权交回去、并附 reason 让模型在下一步处理 |
| 以为 PostToolUse 能撤销文件修改 | PostToolUse 触发时工具已经执行成功。文件已经被写、命令已经跑完。你最多能给模型"反馈"让它下一步去 revert,但 hook 本身不会回滚。要真拦改动只能用 PreToolUse |
| 以为 exit 2 在所有事件都能阻断 | 只有部分事件能被 exit 2 阻断(PreToolUse、UserPromptSubmit、Stop、PreCompact、SubagentStop、PermissionRequest 等)。在 PostToolUse / SessionStart / Notification 上 exit 2 只是把 stderr 当反馈喂回去,不会撤销已经发生的事 |
| 以为多个 hook 顺序执行 | 同一事件命中的多个 hook 是并行执行的,相同的 command 字符串还会被去重。如果你希望 A 跑完再跑 B,要么写在同一个脚本里串起来,要么靠各自退出码触发后续逻辑 |
在 Stop hook 里无脑 exit 2 想"让 Claude 一直说" | Stop hook exit 2 会把 Claude 拉回继续生成,但可能死循环——上次 stop 的原因若没解决,它会一直 stop、一直被拉回。务必加终止条件(达到次数、某文件存在等) |
| 把秘钥硬编码进 hook 脚本 | hook 是 plain shell,提交进 .claude/settings.json 的内容会被同事看到。秘钥放环境变量,HTTP hook 用 allowedEnvVars 白名单注入 header,不要写在脚本里 |
| 优势 | 劣势 |
|---|---|
| 确定性:到点必跑,不依赖模型记性,特别适合"强制规范"场景(lint、format、审计) | 调试链路长:同一事件可能命中多条 hook、还可能并行跑,出问题时要翻 hook 日志、stdin JSON、stderr 三处定位 |
| 零 token 成本:跑在 LLM 之外,不占上下文窗口,长会话特别划算 | 作用域有限:只能在 Claude Code 预定义的 12+ 个事件里挂,遇到没钩子的节点(比如"模型 thinking 中途")插不进去 |
| 能硬拦危险动作:PreToolUse + permissionDecision: deny 是目前防"AI 一时兴起 rm -rf"最可靠的护栏 | PreToolUse 是双刃剑:拦得太宽 Claude 寸步难行,拦得太松形同虚设。需要持续根据 false positive / negative 调整规则 |
| 跨语言、跨工具:脚本是 shell/python/任意可执行文件,hook 也支持 HTTP,不绑定具体语言生态 | 配置散乱风险:用户级、项目级、local 级三层 settings 合并,加上插件 hooks,团队协作时容易出现"我机器拦了你机器没拦" |
| 可与 Skills / MCP 互补:把"必须发生的副作用"放 hook,把"灵活的工具调用"放 MCP/Skills,分工清晰 | 跨平台兼容性:默认 shell 是 bash/powershell,Windows 团队和 macOS/Linux 团队混跑时,要么写两套脚本要么统一走 Node/Python |
参考答案:
Claude Code 把 hook 设计成进程外、单向的事件回调:CLI 把上下文 JSON 通过 stdin 推给你的脚本,等你拿 exit code 和 stdout 回报结论,整个交互就结束了。这个设计是刻意的,原因有三:
要让模型介入,正确做法是 hook 写 decision: "block" + reason: "...",把控制权交回 Claude Code,由它在下一轮推理时把 reason 注入上下文,让模型自己处理。
参考答案:
应该用 PostToolUse + matcher 为 Write|Edit。原因:
decision: "block" + reason 或者直接 stderr + exit 2(在 PostToolUse 上 exit 2 会把 stderr 注入 Claude 的下文),模型下一步就会看到"ruff 报了 E501,请修复"。完整脚本骨架:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
[[ "$FILE" == *.py ]] || exit 0
if ! OUTPUT=$(ruff check "$FILE" 2>&1); then
echo "ruff 报错,请修复后重试:" >&2
echo "$OUTPUT" >&2
exit 2 # PostToolUse 上 exit 2 = 把 stderr 注入下一轮上下文
fi
参考答案:
利用 Claude Code 的三层 settings 合并机制:
| 文件 | 入库 | 放什么 |
|---|---|---|
.claude/settings.json | ✅ git 跟踪 | 团队统一:PostToolUse 跑 lint / format、SessionStart 注入项目上下文 |
.claude/settings.local.json | ❌ .gitignore | 个人私有:自己机器上的 rm 拦截、个人通知(推到自己的 Telegram 等) |
~/.claude/settings.json | n/a | 跨项目通用:全局生效的拦截、自己的审计日志路径 |
要点:
.claude/settings.json,不带 local,行为更可预测。bypassPermissions),用 managed-settings.json 配合 allowManagedHooksOnly: true,普通 settings 无法覆盖。优先展示同分类且标签更接近的内容,方便继续串联学习。
在终端 Agent 里挂 MCP server 的两种方式 —— claude mcp add 命令 vs settings.json 配置文件,含 stdio 与远程 server 的踩坑要点
理解 Claude Code 的 settings 三层模型(用户级 / 项目级 / local)的合并规则、覆盖优先级与最常踩的坑
理解 Claude Code 等 CLI Agent 的工具白名单 / 危险权限 / sandbox 边界,知道 bypassPermissions 何时该开