Hooks
介绍
Tuanjie AI 支持 Hooks(钩子):在 Agent 运行的关键节点自动调用外部命令。通过 Hooks,您可以在不修改核心代码的前提下,实现自定义的拦截、审计和增强逻辑。
什么是 Hooks?
Hooks 是一种生命周期钩子机制,允许您在 Tuanjie AI Agent 运行的各个关键节点(如工具调用前后、模型请求前后、会话开始/结束等)自动执行外部脚本或命令。
Hooks 与 Gemini CLI 的配置格式兼容。本文只说明如何配置和使用,不涉及内部实现。
Hooks 的核心能力
- 观察:记录日志、采集遥测、审计 Agent 行为
- 拦截:按策略阻止危险操作(如
rm -rf) - 终止:停止当前运行
- 修改:调整工具参数、注入上下文、改写模型请求或响应
推荐脚本语言
Hook 以子进程方式运行。建议使用 node 编写脚本(而非 Python 或 Bash),因为 Node 与 Codely CLI 同栈,在 macOS / Linux / Windows 上行为一致,无需额外解释器或 shebang 差异。下文示例均使用 Node.js。
快速开始
1. 启用 Hooks
在 settings.json 中开启:
{
"hooks": {
"enabled": true
}
}
2. 添加一个 BeforeTool Hook(拦截危险 shell 命令)
{
"hooks": {
"enabled": true,
"BeforeTool": [
{
"matcher": "shell",
"sequential": true,
"hooks": [
{
"type": "command",
"name": "block-rm-rf",
"command": "node .codely-cli/hooks/block-rm-rf.js",
"timeout": 2000
}
]
}
]
}
}
示例脚本 .codely-cli/hooks/block-rm-rf.js:
#!/usr/bin/env node
const fs = require('fs');
let input = '';
process.stdin.on('data', (chunk) => {
input += chunk;
});
process.stdin.on('end', () => {
const data = JSON.parse(input);
const cmd = data.tool_input?.command ?? '';
if (/\brm\s+-rf\b/.test(cmd)) {
process.stdout.write(
JSON.stringify({
decision: 'deny',
reason: '禁止执行 rm -rf',
}),
);
process.exit(2);
}
process.stdout.write(JSON.stringify({ decision: 'allow' }));
process.exit(0);
});
配置文件位置
Hooks 写在 settings.json 中,支持三个作用域(会合并):
| 作用域 | 路径 |
|---|---|
| 用户(全局) | ~/.codely-cli/settings.json |
| 项目(工作区) | <项目根>/.codely-cli/settings.json |
| 系统 | 见 CLI 配置 |
此外,已激活的扩展(extensions) 也可以通过其配置中的 hooks 字段提供 Hook,优先级最低(排在系统/用户/项目之后)。
合并规则:
- 配置项(如
enabled、mode):后者覆盖前者(工作区 > 用户 > 系统) disabled列表:各作用域取并集- 各事件的 Hook 数组:按 系统 → 用户 → 工作区 → 扩展 拼接(同名+同命令的 Hook 会去重,项目级优先)
全局配置项
hooks 对象除事件名外,还支持以下字段:
| 字段 | 说明 |
|---|---|
enabled | 是否启用 Hook 系统(默认 false) |
enableUI | 是否启用 /hooks 命令 UI;省略时继承 enabled |
disabled | 要禁用的 Hook 名称列表(按 name 匹配;无 name 时用 command) |
notifications | 是否在 UI 中显示 Hook 开始/结束事件(不影响 Notification 事件本身) |
maxTotalDurationPerTurn | 单轮 Hook 总耗时上限(毫秒,0 = 不限) |
mode | 见下表 |
environmentSanitization | 传给 Hook 子进程的环境变量过滤策略(见「环境变量与安全」) |
mode 取值:
| 值 | 行为 |
|---|---|
permissive(默认) | Hook 执行失败仅警告,主流程继续 |
strict | 对控制类事件,Hook 失败视为致命错误(CI 护栏推荐) |
disabled | 跳过所有 Hook(即使 enabled: true,便于排查或 CI 临时关闭) |
单个 Hook 的定义格式
每个事件(如 BeforeTool)对应一个 HookDefinition 数组:
{
"matcher": "可选",
"sequential": true,
"hooks": [
{
"type": "command",
"name": "建议填写,便于 /hooks 管理",
"command": "node path/to/hook.js",
"timeout": 60000,
"env": { "MY_FLAG": "1" }
}
]
}
字段说明:
matcher(可选)- 工具相关事件(
BeforeTool/AfterTool/AfterToolFailure/PermissionRequest/PermissionDenied):匹配工具名;若可编译为正则则用正则,否则精确匹配 - 触发字符串类事件(精确匹配):
SessionStart/SessionEnd/PreCompress/PostCompress/Notification/SubagentStart/SubagentStop,分别匹配source、reason、trigger、notification_type、agent_type FileChanged:匹配工具名或文件 basename(任一命中即可)- 通过 Claude Code 别名事件加载的 Hook 使用 CC 的 matcher 语义(见下文「Claude Code 兼容」)
- 工具相关事件(
sequential- 任一匹配项为
true时,该事件下所有 Hook 串行执行 - 串行时,前面的 Hook 可修改输入供后续 Hook 使用(适用于
BeforeTool、BeforeAgent、BeforeModel)
- 任一匹配项为
hooks:该匹配组内要执行的命令列表timeout:单次 Hook 超时(毫秒),默认 60000
全部 Hook 事件一览
Tuanjie AI 共支持 18 种 Hook 事件:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
SessionStart | 会话开始(启动 / 恢复 / 清空 / 压缩) | 注入项目规范、环境说明 |
SessionEnd | 会话结束 | 清理、归档、统计 |
BeforeAgent | 每轮 Agent 开始前 | 注入本轮约束或上下文 |
AfterAgent | 每轮 Agent 结束后 | 校验输出 、请求重试或终止 |
BeforeModel | 调用模型前 | 调整温度、token 上限等请求参数 |
AfterModel | 模型返回后 | 脱敏、过滤敏感内容 |
BeforeToolSelection | 选择可用工具前 | 限制本轮可调用的工具 |
BeforeTool | 每次工具调用前 | 拦截或改写工具参数 |
AfterTool | 每次工具调用后 | 补充说明、链式触发后续工具 |
AfterToolFailure | 工具执行失败后 | 失败审计、告警(仅建议性) |
Notification | 工具需用户确认时 | 记录或提示(仅建议性) |
PermissionRequest | 工具等待用户审批时 | 审计(仅建议性) |
PermissionDenied | 用户拒绝工具调用时 | 审计(仅建议性) |
PreCompress | 对话压缩前 | 备份、日志 |
PostCompress | 对话压缩后 | 日志、统计(仅建议性) |
SubagentStart | 子 Agent 启动时 | 审计(仅建议性) |
SubagentStop | 子 Agent 结束时 | 审计(仅建议性) |
FileChanged | 文件被成功修改后 | 审计、通知(仅建议性) |
下面按事件说明你会收到什么以及可以做什么。
SessionStart — 会话开始
何时触发: 新会话启动、从 checkpoint 恢复、清空会话后,以及每次对话压缩后。
输入要点: source 为 startup | resume | clear | compact。
注意:无 matcher 的 SessionStart Hook 在每次压缩后也会触发;如不需要,可用 matcher: "startup|resume|clear" 排除。
可执行操作:
- 注入
additionalContext:内容会包在<hook_context>...</hook_context>中,作为后续对话的上下文(非交互模式下也会拼进 prompt) - 通用:
stopReason终止、decision: deny拦截、systemMessage提示用户
示例输出(注入项目规范):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "本项目禁止直接修改 main 分支。"
}
}
SessionEnd — 会话结束
何时触发: 退出、清空、登出等会话结束时(尽力触发,不保证 100%)。
输入要点: reason 为 exit | clear | logout | prompt_input_exit | other。
可执行操作: 主要用于日志与副作用;可返回 systemMessage 或 suppressOutput。此事件不能注入 prompt 上下文。
BeforeAgent — Agent 轮次开始前
何时触发: 每轮 Agent 向模型发请求之前。
输入要点: 当前轮次的 prompt 文本。
可执行操作:
- 注入
additionalContext(绑定到当前轮,会进入本轮用户侧内容) - 拦截:
decision: deny/block+reason - 终止:
continue: false+stopReason
AfterAgent — Agent 轮次结束后
何时触发: 一轮 Agent 完成(已有模型回复)之后。
输入要点: prompt、prompt_response、stop_hook_active。
可执行操作:
- 终止运行:
continue: false+stopReason - 拦截 / 要求重试:
decision: deny或hookSpecificOutput.impact: "retry"(可选retryPrompt作为下一轮输入) - 强制停止:
hookSpecificOutput.impact: "halt"(可选haltReason) - 清空上下文:
hookSpecificOutput.clearContext: true
注意:此事件主要用于控制流(停止、重试),不要依赖 additionalContext 做持久化注入;若需注入上下文 ,请用 BeforeAgent 或 SessionStart。
BeforeModel — 模型请求前
何时触发: 每次向 LLM 发请求之前。
输入要点: llm_request(含 model、messages、config 等)。
可执行操作:
- 修改
llm_request中的config(如temperature、maxOutputTokens) - 修改
messages(谨慎:可能丢失非文本部分;若只改配置,请省略messages) - 拦截或终止(同上)
示例(仅改温度):
{
"hookSpecificOutput": {
"hookEventName": "BeforeModel",
"llm_request": {
"config": { "temperature": 0.2 }
}
}
}
注意:BeforeModel 对消息内容的修改在运行时生效,但不保证恢复会话后与历史请求逐字节一致。若需可复现的注入,优先使用 SessionStart / BeforeAgent / AfterTool 的 additionalContext。
AfterModel — 模型响应后
何时触发: 每次收到模型响应之后(流式与非流式均会触发;流式模式下可能按块调用,务必保持脚本轻量)。
输入要点: llm_request、llm_response。
可执行操作:
- 修改
llm_response(如脱敏、截断) - 拦截或终止
BeforeToolSelection — 工具选择前
何时触发: 决定本轮模型可使用哪些工具之前。
输入要点: llm_request。
可执行操作: 通过 toolConfig 限制工具:
{
"hookSpecificOutput": {
"hookEventName": "BeforeToolSelection",
"toolConfig": {
"mode": "ANY",
"allowedFunctionNames": ["read_file", "grep"]
}
}
}
mode 可选:AUTO | ANY | NONE。
BeforeTool — 工具调用前
何时触发: 每次工具即将执行之前。
输入要点: tool_name、tool_input;MCP 工具另有 mcp_context。
可执行操作:
- 拦截:
decision: deny或block+reason;进程退出码2等同拦截 - 改写参数:在
hookSpecificOutput.tool_input中返回新参数(Tuanjie AI 会用新参数重新做策略检查,不能绕过内置安全策略)
示例(把 shell 命令替换为安全命令):
{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeTool",
"tool_input": { "command": "echo safe" }
}
}
matcher 示例: "shell" 只匹配 shell 工具;"read_.*" 用正则匹配以 read_ 开头的工具。
AfterTool — 工具调用后
何时触发: 工具执行完成并产生结果之后。
输入要点: tool_name、tool_input、tool_response。
可执行操作:
additionalContext:附加到该工具结果上下文中(不是单独的用户消息)tailToolCallRequest:立即再执行一次指定工具(高级用法;结果会替换原工具响应)
{
"hookSpecificOutput": {
"hookEventName": "AfterTool",
"additionalContext": "注意:该文件含自动生成代码,请勿手动编辑标记区域。"
}
}
Notification — 工具需确认时
何时触发: 某工具需要用户确认权限时(如危险操作)。
输入要点: notification_type(如 ToolPermission)、message、details。
可执行操作: 仅建议性——可写日志或 systemMessage,不能阻止确认流程。
注意:matcher 按 notification_type 精确匹配(早期版本中 Notification 的 matcher 会被忽略,现在已生效;想保持"总是运行"请去掉 matcher 或写 "*")。
PreCompress — 对话压缩前
何时触发: 手动或自动压缩对话历史之前。
输入要点: trigger 为 manual | auto。
可执行操作: 日志、备份类副作用;可返回 systemMessage。不能注入 prompt。
PostCompress — 对话压缩后
何时触发: 对话历史压缩完成之后。
输入要点: trigger 为 manual | auto。
可执行操作: 仅建议性——日志、 统计;可返回 systemMessage。
AfterToolFailure — 工具执行失败后
何时触发: 工具执行返回错误时(在 AfterTool 之前触发)。
输入要点: tool_name、tool_input、error(错误信息),可选 tool_response。
可执行操作: 仅建议性——失败审计、告警。
PermissionRequest / PermissionDenied — 工具审批
何时触发: PermissionRequest 在工具等待用户审批时触发;PermissionDenied 在用户拒绝工具调用时触发。
输入要点: tool_name、tool_input;PermissionDenied 另有可选 reason。
可执行操作: 仅建议性——审计、通知;不会阻塞或替代审批流程。
SubagentStart / SubagentStop — 子 Agent 生命周期
何时触发: 子 Agent(如 task 工具派发的代理)启动 / 结束时。
输入要点: agent_id、agent_type;SubagentStop 另有可选 result。matcher 按 agent_type 精确匹配。
可执行操作: 仅建议性——审计、统计。
FileChanged — 文件变更后
何时触发: Agent 成功修改文件之后。
输入要点: file_path、change_type、tool_name、可选 metadata。
可执行操作: 仅建议性——审计、通知;可返回 systemMessage。不能修改文件或阻止已完成的变更。
注意:matcher 匹配工具名或文件 basename(任一命中即可)。
Hook 脚本的输入与输出约定
每个 Hook 作为独立命令运行:
| 流 | 格式 | 说明 |
|---|---|---|
| stdin | JSON | 事件输入(含 session_id、cwd、hook_event_name 等公共字段 + 事件专有字段) |
| stdout | JSON | Hook 输出(见下文「通用输出字段」) |
| stderr | 任意文本 | 日志、调试信息 |
黄金法则
stdout 只输出 JSON;所有日志写到 stderr。stdout 非 JSON 时 Tuanjie AI 会尝试按纯文本兼容解析,但不应依赖此行为。
退出码
| 退出码 | 含义 |
|---|---|
0 | 成功 |
2 | 拦截(等同 decision: deny) |
| 其他 | 非致命错误(permissive 下为警告;strict 下可能终止) |
公共输入字段(所有事件均有)
session_id— 当前会话 IDtranscript_path— transcript 路径(可能为空)cwd— 当前工作目录hook_event_name— 事件名timestamp— ISO 时间戳
可选字段(存在时才出现):
permission_mode— 当前审批模式:default|acceptEdits|bypassPermissions|planagent_id/agent_type— 当 Hook 由子 Agent(而非主循环)触发时携带,可据此区分调用来源
通用输出字段(所有事件均可使用)
{
"continue": false,
"stopReason": "Hook 请求停止运行",
"decision": "deny",
"reason": "不允许此操作",
"systemMessage": "展示给用户的消息",
"suppressOutput": true,
"hookSpecificOutput": {}
}
| 字段 | 作用 |
|---|---|
continue: false + stopReason | 停止整个运行 |
decision: "deny" 或 "block" + reason | 拦截当前动作(是否生效取决于事件) |
decision: "allow" | 显式允许继续 |
systemMessage | 在 UI 中提示用户(可被 suppressOutput 抑制) |
hookSpecificOutput | 事件专有 payload(见上文各事件说明) |
环境变量与安全
Tuanjie AI 注入的环境变量
Hook 子进程会自动获得:
| 变量 | 说明 |
|---|---|
CODELY_PROJECT_DIR / CODELY_CWD | 项目目录(当前相同) |
CODELY_SESSION_ID | 会话 ID |
CODELY_TRANSCRIPT_PATH | Transcript 路径 |
兼容别名(与 Gemini / Claude 生态对齐):GEMINI_PROJECT_DIR、GEMINI_SESSION_ID、GEMINI_CWD、CLAUDE_PROJECT_DIR。
命令中的路径占位符
在 command 字符串中可使用(会自动 shell 转义):
$CODELY_PROJECT_DIR$GEMINI_PROJECT_DIR$CLAUDE_PROJECT_DIR
示例:
"command": "node $CODELY_PROJECT_DIR/.codely-cli/hooks/my-hook.js"
环境变量脱敏
默认会对 Hook 子进程环境做脱敏,避免泄露密钥。可在 settings 中配置:
{
"hooks": {
"environmentSanitization": {
"enableEnvironmentVariableRedaction": true,
"allowedEnvironmentVariables": [],
"blockedEnvironmentVariables": []
}
}
}
在 GitHub Actions 等环境(检测到 GITHUB_SHA 或 SURFACE=Github)中,脱敏策略会强制严格,与上述配置无关。
项目级 Hook 信任机制
前置条件:若当前工作区文件夹本身未受信任(folder trust),项目级 Hook 会整体禁用,并提示先信任工作区。以下逐 Hook 的指纹信任是在工作区已受信任之上的第二层防护。
工作区内的 Hook 在首次出现或变更后,默认被阻止,直到你显式信任:
- 运行
/hooks panel查看被阻止的 Hook 及指纹 - 信任方式:
/hooks trust-project— 信任本项目全部 Hook/hooks trust <序号|指纹|名称>— 信任指定 Hook
已信任的 Hook 记录在 ~/.codely-cli/trusted_hooks.json(按项目根目录索引)。
环境变量快捷方式(开发 / CI):
| 变量 | 效果 |
|---|---|
CODELY_BYPASS_HOOK_TRUST=1 | 跳过信任检查,不读写信任文件 |
CODELY_TRUST_ALL_HOOKS=1 | 自动信任并持久化本项目所有 Hook |
调试与管理命令
启用 Hooks UI(enableUI: true 或 enabled: true)后,可在 CLI 中使用:
| 命令 | 作用 |
|---|---|
/hooks panel | 列出 Hook、状态、统计与最近执行 |
| `/hooks logs [n | hook-name]` |
/hooks test <名称或指纹> <事件> [jsonOverrides] | 用模拟数据单独跑一个 Hook |
/hooks dry-run <事件> [jsonOverrides] | 模拟跑整次事件的 Hook 计划 |
| `/hooks enable | disable |
/hooks trust-project / /hooks trust ... | 信任项目 Hook |
非交互模式
Hooks 在交互式与非交互式(如 codely -p "...")下行为一致,差异在于:
- Hook 的警告/提示会输出到 stderr
SessionStart注入的additionalContext会作为<hook_context>...</hook_context>拼进 prompt
在 CI 中作护栏时,建议:
{
"hooks": {
"enabled": true,
"mode": "strict",
"maxTotalDurationPerTurn": 5000
}
}
并配合较短的 timeout。
未配置 Hooks 时
若未配置任何 Hook:
- 不会启动 Hook 子进程,无额外开销
- 各事件直接返回安全默认值
常见场景速查
| 场景 | 推荐事件 | 做法 |
|---|---|---|
| 禁止危险 shell | BeforeTool | matcher: "shell",decision: deny |
| 项目规范注入 | SessionStart 或 BeforeAgent | additionalContext |
| 工具结果补充说明 | AfterTool | additionalContext |
| CI 只允许读操作 | BeforeToolSelection | allowedFunctionNames 白名单 |
| 模型输出脱敏 | AfterModel | 修改 llm_response(保持脚本快速) |
| 轮次质 量门禁 | AfterAgent | impact: "retry" 或 deny |
| 压缩前备份 | PreCompress | 写 stderr 日志或外部备份脚本 |
| 文件变更审计 | FileChanged | 写日志(建议性) |
Claude Code 兼容
Tuanjie AI 可直接加载 Claude Code(CC)风格的 hooks 配置:事件名、输出协议、Hook 类型与字段由兼容层(packages/core/src/hooks-system/ccCompat.ts)自动转换。完整说明见英文文档的 Claude Code compatibility 章节,要点如下:
事件别名
CC 别名事件自动映射到 Tuanjie AI 事件:PreToolUse → BeforeTool、PostToolUse → AfterTool、PostToolUseFailure → AfterToolFailure、UserPromptSubmit → BeforeAgent、Stop → AfterAgent、PreCompact → PreCompress、PostCompact → PostCompress;同名事件(SessionStart、SessionEnd、Notification、FileChanged、PermissionRequest、PermissionDenied、SubagentStart、SubagentStop)直接通用。
不支持的 CC 事件(Setup、TeammateIdle、Task* 等)会警告并跳过,不会导致加载失败。
CC 别名事件的专有语义
仅对写在 CC 别名事件名下的 Hook 生效(原生事件名保持原生语义):
timeout按秒解释(CC 约定)并自动转换为毫秒;原生 Hook 仍按毫秒- matcher:
Write|Edit为竖线分隔的精确匹配列表,简单名称为精确匹配(非子串正则);CC 工具名(Bash、Read、Write、Edit等)自动映射到 Tuanjie AI 内置工具 /hooks panel会以(CC: <事件名>)标注通过别名加载的 Hook
输出协议与 Hook 类型
- CC 输出自动归一化:
decision: "approve"→allow,permissionDecision→ 顶层decision,updatedInput→tool_input - 新增三种 Hook 类型:
http(POST 输入 JSON 到url,header 仅插值allowedEnvVars白名单内的大写环境变量)、prompt(询问模型,$ARGUMENTS替换为输入 JSON)、agent(受限验证子代理);项目级实例与 command Hook 走同一套信任流程 - 新增字段:
if(权限规则条件,如Bash(git *))、once(每会话最多成功执行 一次)、statusMessage;command Hook 另支持shell、async、asyncRewake
对既有原生配置的行为变更
升级保持原生配置兼容(信任指纹、毫秒超时、正则 matcher、输出协议均不变),但有三处行为差异:
Notification的 matcher 开始生效:早期版本会忽略它(Hook 总是运行),现在按notification_type匹配。想保持旧行为请去掉 matcher 或写"*"SessionStart在每次压缩后也会触发(source: "compact");可用matcher: "startup|resume|clear"排除FileChanged的 matcher 同时匹配文件 basename,宽泛正则可能比以前更频繁触发
迁移(CC settings.json → Tuanjie AI)
大多数 CC hooks 配置可直接复制到 Tuanjie AI settings 并设置 hooks.enabled: true。注意:
- Tuanjie AI 的总开关是
hooks.enabled(CC 为disableAllHooks) - 项目级 Hook 需先
/hooks trust-project信任 - Tuanjie AI 独有事件(
BeforeModel、AfterModel、BeforeToolSelection)无 CC 对应,照常可用
常见问题
Q: Hooks 会在非交互模式下工作吗?
A: 会的。Hooks 在交互式与非交互式(如 codely -p "...")下行为一致,差异仅在于警告/提示输出到 stderr,以及 SessionStart 注入的 additionalContext 会拼进 prompt。
Q: 未配置任何 Hook 时会有性能开销吗?
A: 不会。未配置 Hook 时不会启动 Hook 子进程,各事件直接返回安全默认值,无额外开销。
Q: Hook 脚本应该用什么语言编写?
A: 推荐使用 Node.js,因为它与 Tuanjie AI 同栈,在 macOS / Linux / Windows 上行为一致,无需额外解释器或 shebang 差异。
Q: 如何调试 Hook?
A: 使用 /hooks panel 查看 Hook 状态与最近执行,/hooks logs 查看 stdout/stderr 片段,/hooks test 用模拟数据单独测试某个 Hook,/hooks dry-run 模拟跑整次事件。
最佳实践
开发 Hook
- stdout 只输出 JSON:所有日志写到 stderr,避免污染 Hook 输出解析
- 保持脚本轻量:尤其是
AfterModel在流式模式下可能按块调用,务必快速返回 - 合理设置超时:CI 场景建议较短 timeout,并使用
mode: "strict" - 使用 Node.js:跨平台一致,避免 shebang 差异
安全建议
- 环境变量脱敏:默认开启,避免泄露密钥;在 CI 环境中会强制严格
- 项目级信任:首次使用项目 Hook 需显式信任,防止恶意 Hook 执行
- 最小权限:Hook 脚本应只请求必要的 权限和文件访问