TL;DR:编辑 ~/.claude/settings.json(或项目中的 .claude/settings.json),添加一个 hooks 字段,将事件(PreToolUse、UserPromptSubmit、SessionStart、Stop)映射到 shell 命令。运行命令的是框架本身,而非 Agent——适合用来设置守护规则和自动化流程。
钩子是 LingCode 在 Agent 生命周期特定时刻执行的 shell 命令——工具调用之前、提交提示词之后、会话结束时。钩子负责执行策略:不是写在 CLAUDE.md 里的"请不要做 X",而是真正说"不"的代码。
影响 Agent 行为的方式有两种。一种是告诉它:写在 CLAUDE.md 里、写成技能、或直接写进提示词。这种方式大多数时候都管用,但偶尔会失效——因为 Agent 是模型,而模型是概率性的。对于那些绝对必须发生、或绝对不能发生的事情,"告诉"就不够了。你写下的规则可能在压力下被忽略,在新的对话里被完全遗忘,或者当 Agent 判断当前情况与你描述的不同时被悄悄绕过。
另一种方式是强制执行:在 Agent 工作时、围绕其动作运行的代码。PreToolUse 钩子在任何工具调用之前触发,可以否决它。UserPromptSubmit 钩子在你发送消息时触发,可以记录、修改或拦截消息。PostToolUse 钩子在工具调用成功后触发。SessionEnd 钩子在对话关闭时触发。每个钩子都是一条 shell 命令——你的代码——通过环境变量获取上下文,并通过退出码和输出决定后续行为。
当错误结果的代价高到概率性保障无法接受时,就应该使用钩子。"绝对不能让 Agent 推送到 main 分支。""提交前必须运行 linter。""记录今天 Agent 执行的每条 shell 命令以供审计。"这些是策略问题,不是感觉问题——钩子就是将它们变成代码的方式。
shell 命令。这些事件在 Mac 应用的对话界面和 CLI 中保持一致。你编写的钩子在两种环境中都能生效。
在项目中创建 .claude/hooks/hooks.json(或在用户全局目录创建 ~/.claude/hooks/hooks.json):
{
"PreToolUse": [
{
"matcher": "shell",
"command": "./.claude/hooks/block-dangerous-shell.sh"
}
],
"UserPromptSubmit": [
{
"command": "./.claude/hooks/log-prompt.sh"
}
]
}
每个事件对应一个钩子条目数组。每个条目至少包含一个 command(要运行的 shell 命令),以及可选的 matcher(仅在工具调用匹配该模式时触发)。命令可以是脚本路径、内联表达式,或任何你的 shell 能接受的内容。
钩子运行时,LingCode 会设置一组以 LINGCODE_ 为前缀的环境变量(同时以 CLAUDE_ 为别名,兼容 Claude Code)。常用变量如下:
LINGCODE_EVENT — 事件名称(如 PreToolUse 等)LINGCODE_TOOL_NAME — 对于工具相关事件,为工具名称LINGCODE_TOOL_ARGS — 工具参数的 JSON 格式LINGCODE_PROMPT — 对于 UserPromptSubmit,为原始提示词文本LINGCODE_SESSION_ID — 当前对话的稳定 ID,可用于日志关联LINGCODE_CWD — 项目的工作目录钩子读取这些变量、执行操作后退出。关于标准流:stdin 为空,stdout 被捕获用于上下文,stderr 在钩子阻止事件时会显示在对话面板中。
钩子以 0 退出表示允许事件继续,以非零退出表示阻止。如果 PreToolUse 钩子以 1 退出,工具调用将被拒绝,Agent 会看到拒绝信息并调整策略。如果 UserPromptSubmit 钩子以 1 退出,消息将不会发送。阻止时,钩子向 stderr 写入的内容会作为拒绝原因传给 Agent——请写一条清晰的单行说明。
对于修改场景(重写提示词、脱敏参数),钩子将新值写入 stdout 并以 0 退出。LingCode 会用钩子的 stdout 替换原始内容。"脱敏敏感信息"钩子就是这样工作的。
git push --force创建 .claude/hooks/block-force-push.sh:
#!/usr/bin/env bash
set -euo pipefail
if [[ "${LINGCODE_TOOL_NAME}" == "shell" ]]; then
cmd=$(echo "$LINGCODE_TOOL_ARGS" | jq -r '.command // ""')
if echo "$cmd" | grep -qE 'git push (-f|--force).*main\b'; then
echo "Refused: --force push to main is not allowed. Open a PR instead." >&2
exit 1
fi
fi
exit 0
然后在 .claude/hooks/hooks.json 中添加:
{
"PreToolUse": [
{
"matcher": "shell",
"command": "./.claude/hooks/block-force-push.sh"
}
]
}
为脚本添加可执行权限(chmod +x)。从此,Agent 任何向 main 强制推送的尝试都会在工具边界被拒绝——Agent 会看到 stderr 中的拒绝原因并改换策略。这条规则无法被"说服"绕过,因为它现在是代码。
acceptEdits 并不能绕过钩子。这是有意为之——钩子是策略,模式是便利。
.claude/hooks/。纳入 git 管理,团队所有成员共享同一规则。~/.claude/hooks/。适合个人习惯(例如"在我所有项目中,提交前始终运行 lint")。hooks/ 目录中,可复用地分发钩子——但安装此类插件时会弹出授权提示,因为钩子可以执行 shell。.sh 文件)。一个有缺陷的钩子比没有钩子更糟糕。