ECC Hooks 系统深度解析:PreToolUse / PostToolUse / Stop 钩子机制、权限安全与自定义实战
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
Hooks 是 ECC(Everything Claude Code)中基于事件驱动的自动化机制,它们挂载在 Agent 每次工具调用的前后与会话生命周期边界上,用于强制执行代码质量、提前拦截错误并自动化重复性检查。本文以docs/ja-JP/rules/common/hooks.md的 Hook 架构规范为核心,结合仓库中的hooks/hooks.json配置图、hooks/README.md安装指南与scripts/hooks/下的真实实现,系统讲解 Hook 类型、自动接受权限的安全边界、TodoWrite 最佳实践,以及如何编写可跨平台运行的自定义 Hook。读完本文,你将掌握在 Claude Code / Codex / Opencode 等 harness 上部署、调优和扩展 ECC Hook 体系的完整方法。
Hooks 系统的工作流程
Hooks 是事件驱动的自动化,在 Claude Code 工具执行之前或之后触发。ECC 的核心执行链路可以概括为:
User request → Claude picks a tool → PreToolUse hook runs → Tool executes → PostToolUse hook runs- PreToolUsehooks 在工具执行前运行,可以阻止(exit code 2)或警告(stderr 不阻止);
- PostToolUsehooks 在工具完成后运行,可以分析输出但不能阻止;
- Stophooks 在每次 Claude 响应后运行;
- SessionStart / SessionEndhooks 在会话生命周期边界运行;
- PreCompacthooks 在上下文压缩前运行,适合保存状态。
Hook 类型详解
PreToolUse:工具执行前(验证、参数修改)
PreToolUse 是唯一可以真正"阻止"工具调用的 Hook 类型,适合做验证与参数修改。ECC 内置了多个 PreToolUse 钩子,例如 pre-bash-dev-server-block.js 会在非 tmux 环境下阻止npm run dev等开发服务器命令(退出码 2),以保证日志可访问性。从源码可以看到,它会展开$(...)命令替换与(...)子 shell 分组递归收集所有命令行分段,防止$(npm run dev)这类包装命令绕过检查。
ECC 的 PreToolUse 钩子通常经过 run-with-flags.js 这个门控执行器运行——只有当当前 Hook Profile 允许该钩子时才真正执行脚本,否则直接透传 stdin 并以退出码 0 结束(fail-open,不干预工具调用)。该执行器还实现了路径穿越防护(拒绝指向插件根目录之外的脚本)以及大输入截断处理(超过 1MB 时抑制透传,避免截断的 JSON 被 harness 判定为 hook 失败)。
PostToolUse:工具执行后(自动格式化、检查)
PostToolUse 在工具完成后运行,典型用途包括自动格式化、质量门禁、构建分析与 PR 日志。ECC 在hooks/hooks.json中通过post:dispatcher:sync(同步)与post:dispatcher:async(后台异步)两个分发器钩子,在单进程内批量执行所有 PostToolUse 钩子,同时保留每个钩子各自的 profile 门控。
Stop:会话结束时(最终验证)
Stop 钩子在每次 Agent 响应结束后运行,适合做最终验证与状态持久化。典型例子是 check-console-log.js:它遍历本次修改过的 JS/TS 文件,检查是否残留console.log并给出警告;同时遵守always-exit-0约定,通过 stderr 输出警告而不阻断流程。源码中通过EXCLUDED_PATTERNS排除了测试文件、配置文件与scripts/目录(这些场景中console.log是合理的)。
ECC 的 Stop 钩子还承担了大量会话管理职责:Plan Canvas 待交付反馈投递(stop:plan-canvas-pending)、批量格式化与类型检查(stop:format-typecheck,对本次响应编辑过的所有 JS/TS 文件统一执行 Biome/Prettier 与tsc,避免每次 Edit 后都跑一遍)、会话状态持久化(stop:session-end)、模式提取(stop:evaluate-session,支撑持续学习)、成本追踪(stop:cost-tracker)与桌面通知(stop:desktop-notify)。
会话生命周期钩子
除三大核心类型外,ECC 还实现了完整的生命周期钩子:SessionStart(加载历史上下文并探测包管理器、恢复 Plan Canvas 浏览器评审)、PreCompact(上下文压缩前保存状态)、SessionEnd(生命周期标记与清理日志)、PostToolUseFailure(跟踪失败的 MCP 工具调用并标记不健康服务器)。
退出码语义与阻止/警告机制
Hook 通过退出码与 stdout/stderr 与 harness 通信:
| 退出码 | 含义 | 适用范围 |
|---|---|---|
0 | 成功,继续执行 | 所有 Hook |
2 | 阻止该工具调用 | 仅 PreToolUse |
| 其他非零 | 错误(记录日志但不阻止) | 所有 Hook |
- 警告(warn):向 stderr 输出提示信息,工具调用照常进行;
- 阻止(block):PreToolUse 钩子以退出码 2 退出,工具调用被拦截;
- 透传约定:Hook 必须将原始 stdin 原样写回 stdout,否则 harness 可能误判为 Hook 失败。
一个需要特别注意的细节:非阻塞 PreToolUse 钩子的 stderr 只写入调试日志,不会进入模型上下文。若要向模型展示用户可见的建议,需要像 suggest-compact.js 那样,向 stdout 输出结构化 JSON 中的hookSpecificOutput.additionalContext字段。该钩子利用两个信号触发战略压缩建议:工具调用计数(默认每 50 次提醒一次)和会话 transcript 中真实的上下文 token 用量(按窗口比例缩放阈值,200k 窗口默认 160k 触发、1M 窗口 250k 触发)。
自动接受权限与安全边界
ECC 规则文档对自动接受权限(Auto-Accept Permissions)给出了明确的红线,必须谨慎使用:
- 为受信任、定义明确的计划启用——当任务的每一步都是可预期、可审计的时候;
- 为探索性工作禁用——当 Agent 的行为边界不清晰、需要逐步确认的时候;
- 切勿使用
dangerously-skip-permissions标志——这是绕过所有权限确认的"核选项",会抹掉 Agent 行为的全部审计痕迹; - 改为在
~/.claude.json中配置allowedTools——以白名单方式精确放行特定工具,而不是一刀切跳过权限。
allowedTools的典型配置方式如下:
{ "permissions": { "allow": [ "Bash(npm run test)", "Read", "Edit" ] } }这一安全立场与仓库中的安全指南一致。the-security-guide.md 明确指出:如果你无法看到 Agent 读取了什么、调用了哪个工具、尝试访问哪个网络目标,就无法对其设防;文中直接点名了在--dangerously-skip-permissions下运行多 Agent 循环并直接推送 main 分支的反面案例。ECC 的立场是:权限放行应当最小化、可审计、面向受信计划,而不是用跳过权限换取便利。
TodoWrite 最佳实践
TodoWrite 是长任务执行中的进度管理工具。ECC 规则要求使用 TodoWrite 工具来:
- 跟踪多步骤任务的进度;
- 验证对指令的理解(写出计划等于把理解显式化);
- 实现实时指导(让用户/监督者随时看到 Agent 的当前步骤);
- 展示细粒度的实现步骤。
更重要的是,Todo 列表本身是 Agent 对任务理解的"暴露面",一份糟糕的 Todo 列表会直接揭示:
- 步骤顺序错误——说明对依赖关系理解有偏差;
- 缺失的项目——说明遗漏了关键需求;
- 额外不必要的项目——说明过度设计或误读了范围;
- 粒度错误——过粗则无法跟踪,过细则淹没在噪音中;
- 被误解的需求——Todo 与指令不一致时,应尽早纠正而非埋头执行。
实践要点:在任务开始前写 Todo,随执行进度实时勾选更新,在里程碑处对照 Todo 清单做偏差审查。
在 ECC 中安装与启用 Hooks
ECC 的 Hook 安装由官方安装器完成,仓库内hooks/hooks.json是面向插件/仓库形态的配置图,不建议手工将其粘贴到~/.claude/settings.json或直接复制到~/.claude/hooks/hooks.json——因为安装器会把 Hook 命令重写为针对你实际 Claude 根目录的路径。
bash ./install.sh --target claude --modules hooks-runtime --enable-hookspwsh -File .\install.ps1 --target claude --modules hooks-runtime --enable-hooks安装后,解析好的 Hook 会写入~/.claude/hooks/hooks.json;在 Windows 上 Claude 配置根目录为%USERPROFILE%\.claude。内存持久化的生命周期定义位于 hooks/memory-persistence/ 目录,它是 SessionStart、PreCompact、观测、活动跟踪与 SessionEnd 行为的稳定契约。
运行时控制:环境变量与 Profile
ECC 推荐用环境变量在运行时控制 Hook 行为,而无需编辑 hooks.json:
# 总开关。显式环境变量值优先于插件偏好设置。 export ECC_HOOKS_ENABLED=true # minimal | standard | strict(默认:standard) export ECC_HOOK_PROFILE=standard # 按钩子 ID 禁用指定钩子(逗号分隔) export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck" # 仅在安装/恢复期间禁用 GateGuard export ECC_GATEGUARD=off # 限制 SessionStart 附加上下文(默认 8000 字符) export ECC_SESSION_START_MAX_CHARS=4000 # 完全禁用 SessionStart 附加上下文 export ECC_SESSION_START_CONTEXT=off # 保留上下文/范围/循环警告,但抑制 API 速率成本估算 export ECC_CONTEXT_MONITOR_COST_WARNINGS=offWindows PowerShell 下使用用户级环境变量:
[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User')三个运行时 Hook Profile 的含义(源码见 scripts/lib/hook-flags.js,其中VALID_PROFILES只接受minimal | standard | strict三者):
- minimal——只保留必要的生命周期与安全钩子;
- standard——默认值,均衡的质量 + 安全检查;
- strict——启用额外的提醒与更严格的护栏。
Claude 插件形态下,ecc setup --mode claude-plugin会安装或更新插件,并暴露与个人设置hooks_enabled、hook_profile相同的选项。此外,ECC_GATEGUARD=off是setup-only值(通过ecc setup关闭本地 ECC Hook 工作),它不是运行时 Hook Profile。
仓库内置钩子清单
ECC 在 hooks/hooks.json 中维护了完整的可执行 Hook 图,以下按类型汇总:
PreToolUse Hooks
| Hook | Matcher | 行为 | 退出码 |
|---|---|---|---|
| 开发服务器阻止器 | Bash | 阻止在 tmux 外运行npm run dev等——确保日志可访问 | 2(阻止) |
| tmux 提醒 | Bash | 对长时运行命令(npm test、cargo build、docker)建议使用 tmux | 0(警告) |
| git push 提醒 | Bash | 提醒在git push前审查变更 | 0(警告) |
| 提交前质量检查 | Bash | git commit前执行质量检查:lint 暂存文件、校验-m/--message提交信息格式、检测 console.log/debugger/密钥 | 2(阻止严重)/ 0(警告) |
| 文档文件警告 | Write | 对非标准.md/.txt文件给出警告(放行 README、CLAUDE、CONTRIBUTING、CHANGELOG、LICENSE、SKILL、docs/、skills/);跨平台路径处理 | 0(警告) |
| 战略压缩建议 | Edit\|Write | 在逻辑间隔(约每 50 次工具调用)建议手动/compact | 0(警告) |
hooks.json中还注册了pre:observe(持续学习观测采集,异步)、pre:governance-capture(治理事件捕获,密钥/策略违规/审批请求,需ECC_GOVERNANCE_CAPTURE=1启用)、pre:config-protection(阻止修改 linter/formatter 配置文件,引导 Agent 修代码而非放宽配置)、pre:mcp-health-check(MCP 工具执行前健康检查,拦截不健康调用)与pre:edit-write:gateguard-fact-force(事实强制门禁:对每个文件的首次 Edit/Write 要求先完成调查再放行)。
PostToolUse Hooks
| Hook | Matcher | 作用 |
|---|---|---|
| PR 日志 | Bash | gh pr create后记录 PR URL 与审查命令 |
| 构建分析 | Bash | 构建命令后的后台分析(异步、非阻塞) |
| 质量门禁 | Edit\|Write\|MultiEdit | 编辑后运行快速质量检查 |
| 设计质量检查 | Edit\|Write\|MultiEdit | 前端编辑趋向通用模板 UI 时给出警告 |
| Prettier 格式化 | Edit | 编辑后自动用 Prettier 格式化 JS/TS 文件 |
| TypeScript 检查 | Edit | 编辑.ts/.tsx文件后运行tsc --noEmit |
| console.log 警告 | Edit | 警告已编辑文件中出现的console.log |
Lifecycle Hooks
| Hook | 事件 | 作用 |
|---|---|---|
| 会话启动 | SessionStart | 加载历史上下文并探测包管理器 |
| Plan Canvas 会话 | SessionStart | 呈现打开的 Plan Canvas 浏览器评审,让新会话恢复循环 |
| 压缩前保存 | PreCompact | 上下文压缩前保存状态 |
| console.log 审计 | Stop | 每次响应后检查所有修改文件中的console.log |
| 会话摘要 | Stop | 在可获取 transcript 路径时持久化会话状态 |
| 模式提取 | Stop | 评估会话中可提取的模式(持续学习) |
| 成本追踪 | Stop | 输出轻量级运行成本遥测标记 |
| 桌面通知 | Stop | 发送 macOS 桌面通知并附带任务摘要 |
| 会话结束标记 | SessionEnd | 生命周期标记与清理日志 |
编写自定义 Hook
Hooks 本质上是 shell 命令:从 stdin 接收工具输入(JSON),并把结果输出到 stdout。跨平台(Windows / macOS / Linux)的关键是用 Node.js 实现 Hook 逻辑——ECC 的所有内置 Hook 均为此模式。
基本结构
// my-hook.js let data = ''; process.stdin.on('data', chunk => data += chunk); process.stdin.on('end', () => { const input = JSON.parse(data); // 访问工具信息 const toolName = input.tool_name; // "Edit", "Bash", "Write", 等 const toolInput = input.tool_input; // 工具特定参数 const toolOutput = input.tool_output; // 仅 PostToolUse 可用 // 警告(非阻塞):写入 stderr console.error('[Hook] 将展示给 Claude 的警告信息'); // 阻止(仅 PreToolUse):以退出码 2 退出 // process.exit(2); // 始终将原始数据输出到 stdout console.log(data); });Hook 输入 Schema
interface HookInput { tool_name: string; // "Bash", "Edit", "Write", "Read" 等 tool_input: { command?: string; // Bash:正在运行的命令 file_path?: string; // Edit/Write/Read:目标文件 old_string?: string; // Edit:被替换的文本 new_string?: string; // Edit:替换后的文本 content?: string; // Write:文件内容 }; tool_output?: { // 仅 PostToolUse output?: string; // 命令/工具输出 }; }异步 Hooks
对于不应阻塞主流程的 Hook(如后台分析),设置async: true并给出超时:
{ "type": "command", "command": "node my-slow-hook.js", "async": true, "timeout": 30 }异步 Hook 在后台运行,无法阻止工具执行。ECC 内置的观测采集、会话持久化、成本追踪与桌面通知均采用异步模式。
常见配方
警告新增 TODO 注释:
{ "matcher": "Edit", "hooks": [{ "type": "command", "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}console.log(d)})\"" }], "description": "Warn when adding TODO/FIXME comments" }阻止创建超大文件:
{ "matcher": "Write", "hooks": [{ "type": "command", "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller, focused modules');process.exit(2)}console.log(d)})\"" }], "description": "Block creation of files larger than 800 lines" }用 ruff 自动格式化 Python 文件:
{ "matcher": "Edit", "hooks": [{ "type": "command", "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.py$/.test(p)){const{execFileSync}=require('child_process');try{execFileSync('ruff',['format',p],{stdio:'pipe'})}catch(e){}}console.log(d)})\"" }], "description": "Auto-format Python files with ruff after edits" }要求新源码文件附带测试文件:
{ "matcher": "Write", "hooks": [{ "type": "command", "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/src\\/.*\\.(ts|js)$/.test(p)&&!/\\.test\\.|\\.spec\\./.test(p)){const testPath=p.replace(/\\.(ts|js)$/,'.test.$1');if(!fs.existsSync(testPath)){console.error('[Hook] No test file found for: '+p);console.error('[Hook] Expected: '+testPath);console.error('[Hook] Consider writing tests first (/tdd)')}}console.log(d)})\"" }], "description": "Remind to create tests when adding new source files" }禁用或覆盖 Hook
要禁用某个 Hook,在hooks.json中移除或注释对应条目;以插件方式安装时,可在~/.claude/settings.json中覆盖:
{ "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [], "description": "Override: allow all .md file creation" } ] } }源码与测试验证
Hook 体系的实现集中在 scripts/hooks/ 目录(50 余个脚本),核心支撑包括:
- run-with-flags.js——profile 门控执行器,同时处理截断保护、路径穿越防护与
require()直载优化(导出了run()的钩子无需再 spawn 子进程,可节省约 50–100ms/钩子); - scripts/lib/hook-flags.js——环境变量解析与 Profile 合法性校验;
- scripts/lib/utils.js——跨平台工具函数(Git 修改文件探测、stdin JSON 读取、日志与输出);
- pre-bash-dispatcher.js 与 posttooluse-dispatcher.js——Bash 前置与 PostToolUse 的分发器;
- session-start-bootstrap.js——会话启动引导。
测试方面,tests/hooks/ 下有覆盖上述机制的完整测试,例如hook-flags.test.js(环境变量与 Profile 解析)、pre-bash-reminders.test.js(tmux / push 提醒)、posttooluse-dispatcher.test.js(分发器行为)、doc-file-warning.test.js、mcp-health-check.test.js与continuous-learning-observe-runner.test.js等,可作为理解钩子契约与编写新 Hook 的参考范例。
小结
ECC 的 Hooks 系统将"工具调用前验证、工具调用后检查、会话生命周期管理"三条自动化主线统一在hooks/hooks.json的 Hook 图之下,并借助 profile 门控与运行环境变量实现细粒度启停,无需改动配置即可在minimal/standard/strict之间切换。对使用者而言,牢记三条核心纪律即可:PreToolUse 才能阻止、永远不要使用dangerously-skip-permissions、用 TodoWrite 暴露对任务的理解。在此基础上,参考scripts/hooks/下的实现与tests/hooks/下的测试,你就能写出安全、跨平台、可复用的自定义 Hook,把 ECC 的自动化护栏延伸到自己的工程实践中。
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考