Cline Hooks 系统实战指南:文件钩子与运行时钩子拦截 Agent 全生命周期
【免费下载链接】clineAutonomous coding agent as an SDK, IDE extension, or CLI assistant.项目地址: https://gitcode.com/GitHub_Trending/cl/cline
Cline 的钩子(Hooks)系统是拦截和增强自主编码代理行为的两大扩展点:文件钩子(File Hooks)通过.cline/hooks/目录下的外部脚本(Bash/Python/TypeScript)以 JSON 负载通信,运行时钩子(Runtime Hooks)则以类型化的进程内回调(beforeRun、beforeModel、afterTool等)供插件直接操作运行时状态。读完本文,你将能够:按事件命名放置钩子脚本实现工具调用审计、破坏性操作拦截、上下文注入与参数改写;读懂钩子的 stdin/stdout JSON 协议;并理解文件钩子如何在源码层映射到运行时钩子回调,以及何时该改用beforeModel或registerMessageBuilder()插件方案。
术语定义:先分清三种"Hook"
参考文档 sdk/examples/hooks/README.md 要求在整个生态中统一使用以下术语:
- 运行时钩子(Runtime hooks):类型化的进程内插件/代理生命周期回调,如
beforeRun、beforeModel、afterTool; - 文件钩子(File hooks):从钩子配置目录中被发现、以序列化 JSON 负载运行的外部脚本;
- 钩子事件(Hook events):文件钩子使用的序列化负载名称,如
agent_end、tool_call、prompt_submit。
从源码结构看,文件钩子本质上是运行时钩子层之上的适配器:核心运行时发现钩子文件、把事件名映射到运行时钩子回调,然后把匹配的脚本当作子进程执行并向其 stdin 写入 JSON 负载。这一关系由 hook-file-hooks.ts 中的createHookConfigFileHooks()实现——它返回一个标准的AgentHooks对象(含beforeRun/beforeTool/afterTool/afterRun/onEvent),再通过createHookConfigFileExtension()注册为名为core.hook_config_files的运行时扩展,从而与插件钩子共用同一套合并逻辑(mergeAgentHooks)。
选型建议(原文档明确):
- 想要工作区或用户级配置的 shell/Python 脚本 →用文件钩子;
- 编写插件、需要类型化地访问运行时状态或影响模型/工具执行 →用运行时钩子。
另外注意执行粒度:beforeRun与afterRun包裹一次运行时run()或continue()调用,在交互式会话中即"一次提交的用户轮次"。afterRun对 completed、aborted、failed 三种结果都会触发;如果你只关心成功完成,请检查result.status。文件钩子侧,成功完成对应agent_end事件;插件侧则使用afterRun并判断result.status === "completed"。
文件钩子事件与运行时钩子的映射表
完整继承原文档的映射关系(与 hook-file-config.ts 中HOOK_CONFIG_FILE_EVENT_MAP常量一致):
| 文件钩子文件名 | 文件钩子事件 | 背后的运行时钩子 |
|---|---|---|
TaskStart | agent_start | beforeRun |
TaskResume | agent_resume | beforeRun(带 resume 上下文) |
UserPromptSubmit | prompt_submit | beforeRun加提交的 prompt 上下文 |
PreToolUse | tool_call | beforeTool |
PostToolUse | tool_result | afterTool |
TaskComplete | agent_end | 完成时的afterRun |
TaskError | agent_error | 失败时的afterRun |
TaskCancel | agent_abort | 带取消/中止原因的afterRun或会话关闭 |
SessionShutdown | session_shutdown | 会话清理/运行时关闭 |
PreCompact | 当前未为文件钩子接线 | 无 |
PreCompact在映射表中被显式置为undefined(见 hook-file-config.ts#L41),即该文件会被识别但不会触发任何事件——压缩场景请走运行时钩子或插件方案。
源码中几个值得注意的行为细节:
agent_resume的判定:beforeRun触发时检查环境变量CLINE_HOOK_AGENT_RESUME === "1",是则派发agent_resume负载,否则派发agent_start(hook-file-hooks.ts#L927-L934);agent_end仅在result.status === "completed"时派发(hook-file-hooks.ts#L962-L976),abort(错误消息含 cancel/abort/interrupt 字样)走agent_abort,其余错误走agent_error;- 工具类钩子(
tool_call/tool_result)是阻塞执行,超时默认 120 秒(toolCallTimeoutMs ?? 120000);而生命周期类钩子(agent_start、prompt_submit、agent_end等)以 detached 方式异步派发,不阻塞主流程; - 多个同事件钩子的输出会合并(
mergeHookControls):cancel/review任一为true即生效,context以换行拼接,overrideInput后写的优先。
钩子发现机制:目录、命名与解释器推断
搜索目录
paths.ts#L487-L501 中resolveHooksConfigSearchPaths()定义了钩子目录的发现顺序(去重后):
- 用户级:
Documents下的 Cline/Hooks 目录(resolveDocumentsExtensionPath("Hooks")); - 用户级:
~/.cline/hooks; - 工作区级(旧路径,标记 deprecated):
<workspace>/.config/cline/hooks; - 工作区级(当前路径):
<workspace>/.cline/hooks。
此外,CLI 还提供--hooks-dir <path>选项用于从其他目录加载钩子(program.ts#L72-L75,默认~/.cline/hooks),例如 CI 场景可用cline --hooks-dir ./ci/hooks -i "test prompt"。
文件命名与扩展名
钩子文件必须按所处理的事件命名,文件名匹配不区分大小写,支持的扩展名在 hook-file-config.ts#L49-L62 中枚举:无扩展名(legacy)、.sh、.bash、.zsh、.js、.mjs、.cjs、.ts、.mts、.cts、.py、.ps1。合法的事件基名即上表中的TaskStart、PreToolUse等十个名字。
解释器推断
inferHookCommand()(hook-file-hooks.ts#L327-L370)负责为每个钩子文件构造执行命令,优先级为:
- Shebang 优先:解析首行
#!,并归一化解释器(如python3在 Windows 上改写为py -3,且当py命令缺失时会回退到python,见getWindowsPythonFallbackCommand); - 按扩展名回退:
.sh/.bash/.zsh→bash;.js/.mjs/.cjs→node;.ts/.mts/.cts→bun run;.py→python3(Windows 为py -3);.ps1→pwsh/powershell -File; - 无扩展名默认
bash。
也就是说,文档示例中chmod +x与 shebang 主要服务于可移植性;即使脚本不可执行,核心也会用推断出的解释器命令数组来 spawn。若确实遇到EACCES,subprocess-runner.ts#L60-L76 会给出明确的错误提示。
钩子输入负载:stdin 上的 JSON
所有钩子都会从 stdin 收到一份详细 JSON 事件,核心字段由basePayload()/createPayloadBase()构造(subprocess.ts#L251-L280):clineVersion、hookName、timestamp、taskId、workspaceRoots、userId,以及从源码可确认的额外字段sessionContext(含rootSessionId)、workspaceInfo(会话启动时生成的结构化 git/路径元数据,让钩子不必自己跑git命令)、agent_id、parent_agent_id。
PreToolUse(tool_call)事件:
{ "hookName": "tool_call", "clineVersion": "1.0.0", "timestamp": "2026-01-15T10:30:00Z", "taskId": "conv-123", "workspaceRoots": ["/path/to/repo"], "userId": "user", "iteration": 1, "tool_call": { "id": "call-456", "name": "read_files", "input": {"filePath": "/path/to/file.ts"} } }除tool_call嵌套字段外,负载还带有兼容性的preToolUse段(toolName+ 字符串化的parameters),见 hook-file-hooks.ts#L787-L801。
PostToolUse(tool_result)事件:
{ "hookName": "tool_result", "clineVersion": "1.0.0", "timestamp": "2026-01-15T10:30:00Z", "tool_result": { "id": "call-456", "name": "read_files", "input": {"filePath": "/path/to/file.ts"}, "output": "file contents here", "error": null, "durationMs": 45 } }TaskStart 等其他生命周期事件:
{ "hookName": "agent_start", "clineVersion": "1.0.0", "timestamp": "2026-01-15T10:30:00Z", "taskId": "conv-123", "workspaceRoots": ["/path/to/repo"], "userId": "user" }agent_end负载会额外包含iteration计数与turn(outputText、status);agent_error包含error(name/message/stack);agent_abort与session_shutdown携带reason;prompt_submit携带userPromptSubmit.prompt。这些字段结构均可在 hook-file-hooks.ts 的各runXxx函数中对照核实。
钩子输出:stdout 上的控制 JSON
钩子必须在 stdout 返回 JSON 对象,空{}表示"什么都不做"。可用控制字段:
| 字段 | 类型 | 效果 | 生效事件 |
|---|---|---|---|
cancel | boolean | 取消待执行的工具调用 | PreToolUse |
review | boolean | 暂停并请求用户审查 | PreToolUse |
context | string | 向 Agent 下一轮注入上下文 | PreToolUse、PostToolUse |
errorMessage | string | 向 Agent 暴露一条错误 | PreToolUse |
overrideInput | object | 执行前替换工具输入 | PreToolUse |
这些字段的解析逻辑值得展开(subprocess.ts#L74-L83 的HookOutputSchema与toHookControl):
context兼容旧字段contextModification:两者都是字符串时,优先取context;context有 50,000 字符上限(MAX_HOOK_CONTEXT_SIZE),超长会被截断并附加[hook context truncated]标记,防止钩子撑爆 prompt(subprocess.ts#L52-L65);cancel: true时消息不会被注入为对话上下文:errorMessage(或兜底的context)会作为取消原因(cancelReason)单独传递,避免一个钩子的注入上下文泄漏进另一个钩子的取消原因;- stdout 解析容错:runner 会先查找
HOOK_CONTROL\t前缀的行(取最后一条)作为控制 JSON,否则把整个 trim 后的 stdout 当 JSON 解析;解析失败会记录parseError并告警,但不会让 Agent 崩溃(subprocess-runner.ts#L29-L58)。这也是"日志请走 stderr、stdout 只放 JSON"这一约束的底层原因。 tool_call/tool_result钩子默认120 秒超时,超时进程被SIGKILL,并记录hook command timed out(DEFAULT_TOOL_HOOK_TIMEOUT_MS,subprocess.ts#L128-L131)。
cancel/context/overrideInput最终如何影响运行时,可在 hook-file-hooks.ts#L536-L584 的beforeToolResultFromControl/afterToolResultFromControl中对照:cancel→stop(+reason),context→appendContext,overrideInput→input。
官方示例清单:Bash / Python / TypeScript
sdk/examples/hooks/ 目录提供了覆盖各场景的可运行示例。文档中的复制命令以sdk/为基准目录,从仓库根目录执行时需把路径写成sdk/examples/hooks/...。
Bash 示例
PreToolUse.sh—— 记录每次工具调用及其输入,适合审计 Agent 将要做什么(参考实现见 PreToolUse.sh:读 stdin、jq提取tool_call.name与参数,写 stderr,返回{}):
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse.sh .cline/hooks/ chmod +x .cline/hooks/PreToolUse.sh cline -i "do something" # 在 stderr 中看到工具调用日志PostToolUse.sh—— 检查工具结果并追加补充上下文:
mkdir -p .cline/hooks cp sdk/examples/hooks/PostToolUse.sh .cline/hooks/ chmod +x .cline/hooks/PostToolUse.sh cline -i "do something" # 看到工具结果被记录并增强PreToolUse_BlockDestructive.sh—— 拦截 force push、批量删除等破坏性操作:
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse_BlockDestructive.sh .cline/hooks/PreToolUse.sh chmod +x .cline/hooks/PreToolUse.sh cline -i "clean up the repo" # 破坏性操作将被拦截PreToolUse_RequireReview.sh—— 对关键文件的写入强制人工审查:
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse_RequireReview.sh .cline/hooks/PreToolUse.sh chmod +x .cline/hooks/PreToolUse.sh cline -i "update dependencies" # 关键文件写入会暂停等待审查PreToolUse_InjectFileContext.sh—— 在执行前抽取并注入文件上下文(相关测试文件、lock 文件、环境信息):
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse_InjectFileContext.sh .cline/hooks/PreToolUse.sh chmod +x .cline/hooks/PreToolUse.sh cline -i "review the configuration" # 相关文件会被自动提及TaskStart.sh/TaskComplete.sh/SessionShutdown.sh—— 跟踪 Agent 会话生命周期(开始、结束、关闭):
mkdir -p .cline/hooks cp sdk/examples/hooks/TaskStart.sh .cline/hooks/ cp sdk/examples/hooks/TaskComplete.sh .cline/hooks/ cp sdk/examples/hooks/SessionShutdown.sh .cline/hooks/ chmod +x .cline/hooks/Task*.sh .cline/hooks/SessionShutdown.sh cline -i "do something" # 会话生命周期被记录Python 示例
PreToolUse.py—— Python 版工具调用日志与过滤:
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse.py .cline/hooks/ chmod +x .cline/hooks/PreToolUse.py cline -i "do something" # Python 钩子记录工具调用PostToolUse.py—— Python 版后置结果增强:
mkdir -p .cline/hooks cp sdk/examples/hooks/PostToolUse.py .cline/hooks/ chmod +x .cline/hooks/PostToolUse.py cline -i "do something" # Python 钩子增强工具结果PreToolUse_InjectContext.py—— Python 版上下文注入,含文件分析(测试文件、配置文件、lock 文件、Node.js 版本、git 分支):
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse_InjectContext.py .cline/hooks/PreToolUse.py chmod +x .cline/hooks/PreToolUse.py cline -i "add a new feature" # 相关文件与环境信息被注入TypeScript 示例
PreToolUse.ts—— TypeScript 钩子,用于进阶的工具调用过滤与日志:
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse.ts .cline/hooks/ chmod +x .cline/hooks/PreToolUse.ts cline -i "do something" # TypeScript 钩子通过 bun 执行PostToolUse.ts—— TypeScript 后置执行钩子:
mkdir -p .cline/hooks cp sdk/examples/hooks/PostToolUse.ts .cline/hooks/ chmod +x .cline/hooks/PostToolUse.ts cline -i "do something" # TypeScript 钩子通过 bun 执行PreToolUse_ModifyInput.ts—— 执行前改写工具输入(路径归一化、补默认值、清洗):
mkdir -p .cline/hooks cp sdk/examples/hooks/PreToolUse_ModifyInput.ts .cline/hooks/PreToolUse.ts chmod +x .cline/hooks/PreToolUse.ts cline -i "install dependencies" # npm install 自动加上 --save-exact快速上手三步
- 把钩子拷到项目:文件钩子放入
.cline/hooks/(或~/.cline/hooks、--hooks-dir指定目录),且文件名必须等于事件名; - 赋予执行权限:
chmod +x .cline/hooks/PreToolUse.*; - 测试:
cline -i "test prompt",或指定目录cline --hooks-dir ./my-hooks -i "test prompt"。
常见钩子模式(可直接复制)
以下模式完整继承自原文档,均只依赖jq/标准库,与上文协议一一对应。
1. 记录并放行(Bash)
#!/usr/bin/env bash input=$(cat) tool=$(echo "$input" | jq -r '.tool_call.name') echo "Action: $tool" >&2 echo '{}'2. 向下一轮注入上下文
#!/usr/bin/env bash input=$(cat) tool=$(echo "$input" | jq -r '.tool_call.name') if [ "$tool" = "run_commands" ]; then branch=$(git branch --show-current 2>/dev/null) echo "{\"context\": \"Current branch: $branch\"}" else echo '{}' fi3. 执行前修改工具输入
#!/usr/bin/env bash input=$(cat) tool=$(echo "$input" | jq -r '.tool_call.name') file=$(echo "$input" | jq -r '.tool_call.input.filePath') if [ "$tool" = "read_files" ] && [[ $file == ~/* ]]; then normalized="${file/#\~/$HOME}" echo "{\"overrideInput\": {\"filePath\": \"$normalized\"}}" else echo '{}' fi4. 拦截特定工具或命令
#!/usr/bin/env bash input=$(cat) tool=$(echo "$input" | jq -r '.tool_call.name') cmd=$(echo "$input" | jq -r '.tool_call.input.command // empty') if [ "$tool" = "run_commands" ] && [[ $cmd =~ git\ push\ --force ]]; then echo '{"cancel": true, "errorMessage": "Force push is blocked."}' else echo '{}' fi5. 对敏感文件要求审查
#!/usr/bin/env bash input=$(cat) tool=$(echo "$input" | jq -r '.tool_call.name') file=$(echo "$input" | jq -r '.tool_call.input.filePath // empty') if ([ "$tool" = "editor" ] || [ "$tool" = "write_file" ]) && \ [[ $file =~ (package\.json|\.env|secrets|tsconfig) ]]; then echo '{"review": true, "context": "This will modify a critical file"}' else echo '{}' fi6. Python:解析并操作 JSON
#!/usr/bin/env python3 import sys import json event = json.load(sys.stdin) tool_name = event.get("tool_call", {}).get("name", "") tool_input = event.get("tool_call", {}).get("input", {}) if tool_name == "read_files": file_path = tool_input.get("filePath", "") if file_path.endswith(".test.ts"): print(json.dumps({"context": "This is a test file"})) else: print(json.dumps({})) else: print(json.dumps({}))7. TypeScript:类型安全 + 异步操作
#!/usr/bin/env bun interface HookEvent { tool_call: { name: string; input: Record<string, unknown> }; } const event: HookEvent = JSON.parse(await Bun.stdin.text()); const toolName = event.tool_call.name; if (toolName === "run_commands") { const branch = await getGitBranch(); console.log(JSON.stringify({ context: `Branch: ${branch}` })); } else { console.log(JSON.stringify({})); } async function getGitBranch(): Promise<string> { return "main"; }运行时钩子进阶:自定义压缩(Custom Compaction)
文件钩子只能"观察"生命周期事件;对消息压缩这类需要直接改写请求的高级场景,应使用 TypeScript运行时钩子插件。仓库提供了完整示例 custom-compaction-hook.example.ts,它通过hooks.beforeModel估算请求体积,并在向模型供应商发起请求前把较旧的中间历史替换为一条摘要消息。原文档的安装与验证流程为:
cline plugin install <示例文件位置> --cwd . cline -i "Search the codebase for dispatcher usage, then summarize it"(原文档以仓库 URL 安装该示例;本地开发时对应文件即sdk/examples/hooks/custom-compaction-hook.example.ts。)
两种压缩方案的取舍
| 示例 | 扩展点 | 消息形态 | 适用场景 |
|---|---|---|---|
custom-compaction-hook.example.ts(位于.cline/plugins/) | hooks.beforeModel运行时钩子 | Agent 运行时请求消息,含tool-call、tool-result、reasoning、image、file等运行时 part | 需要运行时钩子上下文、当前运行时快照或直接改写请求对象的场景 |
plugins/custom-compaction.ts(参考 sdk/examples/plugins/ 下的custom-compaction.ts) | api.registerMessageBuilder() | 运行时消息转换为 SDK/供应商绑定Message[]之后的形态 | 大多数可复用的、插件自有的消息改写与压缩策略 |
原文档的结论:普通插件自有的供应商消息改写优先用registerMessageBuilder()——它在核心消息管线中运行、且先于内置的 provider-safety builder;只有当压缩逻辑需要运行时钩子上下文、或必须检查精确的运行时请求对象时,才使用beforeModel。
调试钩子的三种手段
1. 打印钩子调用轨迹:
cline --verbose "your prompt"2. 手动喂 JSON 测试单个钩子(无需启动 Agent):
echo '{"tool_call": {"name": "read_files", "input": {"filePath": "test.ts"}}}' | .cline/hooks/PreToolUse.sh3. 检查钩子输出的 JSON 结构:
.cline/hooks/PreToolUse.sh < input.json | jq .从源码看,手动测试之所以可行,是因为钩子子进程的全部契约就是"stdin 收 JSON、stdout 回 JSON"(subprocess-runner.ts#L126-L220)。另外,核心还会把审计负载以 JSONL 追加写入钩子日志(CLINE_HOOKS_LOG_PATH或~/.cline数据目录下的hooks.jsonl,见 hook-file-hooks.ts#L598-L607),可用它回溯每一次事件的完整负载。
实战注意事项(原文档 Tips 全量整理)
--yolo模式下钩子被禁用——需使用--act或--plan模式启用钩子;- 日志一律写 stderr——stdout 被控制 JSON 独占;
- 保持钩子快——它们在任何一次工具调用前后都会执行,性能直接影响 Agent 吞吐(且工具钩子有 120 秒硬超时);
- 用
jq做 JSON 提取——JSON 解析容易出错,jq是最安全的提取方式; - 允许多钩子共存——不同事件文件可同时放在
.cline/hooks/,同一事件的多个文件其输出按上文合并规则叠加; - 自定义目录加载——
--hooks-dir ./ci/hooks可把钩子集中放在仓库内的 CI 专用目录,便于团队统一审计策略。
小结
Cline 钩子系统的设计可以概括为一句话:用统一的运行时钩子回调(beforeRun/beforeTool/afterTool/afterRun/onEvent)作为单一扩展内核,文件钩子只是把其中五个回调桥接成"按事件命名的外部脚本 + stdin/stdout JSON"的适配层。掌握 sdk/examples/hooks/README.md 中的事件映射表与输入输出协议,配合 sdk/packages/core/src/hooks/ 下的hook-file-config.ts(发现与命名)、hook-file-hooks.ts(事件桥接与控制合并)、subprocess.ts(负载与控制字段)、subprocess-runner.ts(子进程执行与容错)四个文件,你就能从"照着示例拷脚本"进阶到"按团队审计规范定制拦截策略",并在需要改写模型请求时平滑切换到beforeModel/registerMessageBuilder()插件方案。
【免费下载链接】clineAutonomous coding agent as an SDK, IDE extension, or CLI assistant.项目地址: https://gitcode.com/GitHub_Trending/cl/cline
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考