
013、Hooks 系统入门事件驱动自动化与 BeforeToolUse、AfterToolUse 等事件详解从一次凌晨的CI失败说起上周四凌晨两点我被PagerDuty的告警吵醒。CI流水线里Claude Agent在调用一个数据库迁移工具时莫名其妙地传入了生产环境的连接字符串——明明我在prompt里写了“仅限staging”。排查了三个小时发现问题的根源不是prompt写得不够好而是Agent在调用工具前后没有任何拦截机制去校验或修正它即将执行的操作。那一刻我意识到Claude Code的Hooks系统不是锦上添花的功能而是生产环境安全性的最后一道防线。Hooks到底是什么别想复杂了如果你写过Express中间件或者用过Git hooks那理解这个就很简单。Claude Code的Hooks本质上是一组事件监听器在Agent执行特定动作的前后触发。你可以注册自己的回调函数在这些时间窗口里做三件事拦截阻止某个操作执行修改篡改即将传入的参数或返回的结果增强注入额外的上下文或日志我习惯把Hooks看作Agent的“安检通道”——每个工具调用都要过一遍X光机。核心事件BeforeToolUse 和 AfterToolUse这两个事件是整个Hooks系统的基石也是我日常用得最多的。BeforeToolUse在工具被调用前动手脚// 别这样写直接返回不检查任何东西hooks.on(beforeToolUse,async(toolUse){returntoolUse;// 等于没写hook})// 正确的写法校验参数必要时直接拒绝hooks.on(beforeToolUse,async(toolUse){// 这里踩过坑toolUse.input 可能是深层嵌套对象直接修改引用不会生效constinputJSON.parse(JSON.stringify(toolUse.input))if(toolUse.nameexecute_sqlinput.databaseproduction){// 直接抛异常比返回修改后的对象更安全thrownewError( 禁止在生产环境执行SQL当前环境: process.env.NODE_ENV)}// 如果只是修改参数记得返回新对象if(toolUse.namedeploy_service){input.timeoutMath.min(input.timeout||300,60)// 强制超时不超过60秒return{...toolUse,input}}returntoolUse})一个血的教训toolUse.input在某些版本里是只读的Proxy对象直接修改属性不会报错但也不会生效。深拷贝后再修改这是最稳妥的做法。AfterToolUse在工具返回后做后处理这个事件我主要用来做两件事结果校验和日志记录。hooks.on(afterToolUse,async(result){// 这里踩过坑result.error 可能是字符串也可能是Error对象if(result.error){consterrorMsgtypeofresult.errorstring?result.error:result.error.message// 记录到自定义的监控系统awaitreportToolFailure(result.toolName,errorMsg,{duration:result.duration,input:sanitizeInput(result.input)// 脱敏后再记录})// 如果错误是已知的临时故障可以自动重试if(isRetryableError(errorMsg)result.retryCount3){return{...result,shouldRetry:true}}}// 校验返回数据格式if(result.toolNameread_filetypeofresult.outputstring){// 检查是否包含敏感信息if(containsSecrets(result.output)){// 别这样写直接返回原始结果等于没做校验// return result// 正确的做法脱敏并告警console.warn(⚠️ 检测到敏感信息泄露风险已自动脱敏)return{...result,output:maskSecrets(result.output)}}}returnresult})其他实用事件不止于工具调用除了Before/AfterToolUse还有几个事件值得关注OnToolError错误处理的最后防线hooks.on(toolError,async(error){// 这里踩过坑不要在这里做耗时操作会阻塞Agent的响应consterrorIdgenerateErrorId()// 异步上报不阻塞主流程setImmediate((){reportError(errorId,error)})// 返回友好的错误信息给Agentreturn{message:操作遇到临时问题错误ID:${errorId}已自动记录请重试或联系管理员,recoverable:true}})OnSessionStart / OnSessionEnd会话生命周期管理这两个事件适合做资源初始化和清理hooks.on(sessionStart,async(context){// 初始化数据库连接池context.dbPoolcreatePool(DB_CONFIG)// 注入会话级别的上下文context.sessionIduuid()context.startTimeDate.now()console.log([Session${context.sessionId}] 开始)})hooks.on(sessionEnd,async(context){constdurationDate.now()-context.startTimeconsole.log([Session${context.sessionId}] 结束耗时${duration}ms)// 清理资源awaitcontext.dbPool.end()// 生成会话报告awaitgenerateSessionReport(context.sessionId,duration)})实战构建一个完整的审计日志系统把上面的知识点串起来这是我目前在用的生产级Hooks配置classAuditHookSystem{constructor(){this.auditLog[]this.setupHooks()}setupHooks(){// 请求级别的追踪IDconsttraceIdcrypto.randomUUID()hooks.on(beforeToolUse,async(toolUse){// 记录原始请求this.auditLog.push({type:request,traceId,toolName:toolUse.name,input:this.sanitize(toolUse.input),timestamp:newDate().toISOString()})// 环境隔离防止跨环境操作if(this.isCrossEnvironment(toolUse)){thrownewError(环境不匹配: 当前环境${process.env.ENV}目标环境${toolUse.input.env})}returntoolUse})hooks.on(afterToolUse,async(result){// 记录响应this.auditLog.push({type:response,traceId,toolName:result.toolName,duration:result.duration,success:!result.error,timestamp:newDate().toISOString()})// 如果审计日志超过阈值异步刷盘if(this.auditLog.length100){setImmediate(()this.flushAuditLog())}returnresult})hooks.on(sessionEnd,async(){// 会话结束时强制刷盘awaitthis.flushAuditLog()})}sanitize(input){// 脱敏逻辑替换密码、token等字段constsensitiveKeys[password,token,secret,key]constcloneJSON.parse(JSON.stringify(input))for(constkeyofsensitiveKeys){if(clone[key]){clone[key]***REDACTED***}}returnclone}isCrossEnvironment(toolUse){// 检查工具调用是否试图操作不同环境constenvtoolUse.input?.environment||toolUse.input?.envreturnenvenv!process.env.ENV}asyncflushAuditLog(){if(this.auditLog.length0)return// 批量写入到日志存储awaitwriteAuditLogs(this.auditLog)this.auditLog[]}}个人经验Hooks系统的五个“不要”不要在Hook里做同步I/O。Hook的执行是阻塞Agent主流程的一个慢速的数据库查询会让整个Agent卡住。用异步非阻塞的方式或者把耗时操作放到setImmediate里。不要依赖Hook来修复prompt的缺陷。Hook是安全网不是拐杖。如果你的Agent经常传错参数先回去修prompt而不是靠Hook来擦屁股。不要在BeforeToolUse里修改toolUse.name。我见过有人试图把delete_production_db重命名为dry_run——这只会让日志系统混乱实际执行的操作并不会改变。Hook只能修改参数不能改变工具本身。不要忘记处理Hook自身的异常。Hook里抛出的未捕获异常会导致Agent崩溃。用try-catch包裹所有可能出错的逻辑至少保证Hook挂了不影响Agent继续运行。不要在AfterToolUse里做重试决策。重试逻辑应该放在调用方而不是Hook里。Hook只负责记录和校验重试策略会让Hook变得臃肿且难以调试。写在最后Hooks系统是我在Claude Code工程化实践中投入最多精力的模块。不是因为它复杂而是因为它太容易被忽视——大多数人在写Agent的时候只关注prompt怎么写、工具怎么配却忘了在Agent和外部世界之间加一道安检。如果你现在正在把Claude Code接入生产环境我建议你从这三个Hook开始beforeToolUse做参数校验afterToolUse做结果审计sessionEnd做资源清理。这三个Hook覆盖了80%的安全和运维需求。剩下的20%等你踩坑了自然会回来补上。