Skip to main content
扩展能力

Hooks

Hooks 让你可以在 Qoder IDE 和 JetBrains 插件执行的关键节点插入自定义逻辑,而无需修改任何代码。你只需编辑 JSON 配置文件,就能实现诸如:
  • 在工具执行前拦截危险操作
  • 写文件后自动跑 lint,保持代码风格一致
  • Agent 完成任务时弹出桌面通知,不用一直盯着 IDE
与 Prompt 指令不同,Hooks 是确定性的——只要事件触发,脚本就一定执行,不受模型理解偏差的影响。
各入口的 Hooks 能力不同:本页对应 Qoder IDE / JetBrains 插件(12 个事件,支持 commandhttp 两种类型)。Qoder CLI Hooks 支持 25 个事件与 command / http / prompt / agent 四种类型;QoderWork Hooks 另见单独文档。IDE 与 CLI 共用同一份配置文件,但各入口只会执行自身支持的事件。

支持的事件

当前 IDE / JB 插件支持以下十二种 Hook 事件:
事件名称触发时机可阻断
SessionStart会话启动或恢复时
UserPromptSubmit用户提交 Prompt 后、Agent 处理前
PreToolUse工具调用执行前
PermissionRequest工具需要用户授权时
PostToolUse工具调用成功后
PostToolUseFailure工具调用失败后
SubagentStart子代理启动时
SubagentStop子代理停止时
StopAgent 完成响应时
SessionEnd会话结束时
PreCompact上下文压缩前
Notification发出面向用户的通知时

快速开始

下面用一个例子,演示如何拦截 rm -rf 这类危险命令。
1

创建脚本

mkdir -p ~/.qoder/hooks
cat > ~/.qoder/hooks/block-rm.sh << 'EOF'
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')

if echo "$command" | grep -q 'rm -rf'; then
  echo "危险命令已被阻止: $command" >&2
  exit 2
fi

exit 0
EOF
chmod +x ~/.qoder/hooks/block-rm.sh
2

添加配置

~/.qoder/settings.json 中添加:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
3

验证效果

打开 IDE,在 Qoder 插件面板中让 Agent 执行包含 rm -rf 的命令。Hook 会阻止执行,并把错误信息反馈给 Agent。

适用场景

场景对应事件说明
拦截危险命令PreToolUse在 Agent 执行 rm -rfDROP TABLE 等命令前阻止
文件路径校验PreToolUse限制 Agent 只能在指定目录下创建 / 编辑文件
自动 Lint / 格式化PostToolUse每次写文件后自动执行 ESLint / Prettier
日志审计PostToolUse记录 Agent 的所有工具调用,用于安全审计
失败监控告警PostToolUseFailure工具调用失败时发送告警或记录错误日志
Prompt 内容审查UserPromptSubmit检测用户输入中是否包含敏感信息(密码、密钥等)
自动注入上下文UserPromptSubmit自动在 Prompt 末尾追加项目规范或编码约定
桌面通知StopAgent 完成后弹出系统通知提醒用户

工作原理

Hook 的使用流程可以概括为三步:编写脚本 → 注册配置 → 自动生效 当 Agent 执行到某个生命周期节点时(如工具调用前),插件会检查配置中是否有对应的 Hook:
  1. 插件在启动时加载所有 Hook 配置
  2. Agent 运行过程中,到达某个事件节点(如 PreToolUse
  3. 插件遍历该事件下所有 Hook 分组,用 matcher 匹配当前上下文
  4. 匹配成功的 Hook 按顺序执行对应的 shell 脚本
  5. 脚本通过 stdin 接收事件上下文(JSON),通过 exit codestdout 返回决策
  6. 插件根据返回结果决定后续行为(放行 / 阻断)

前置条件

  • jq:示例脚本依赖 jq 解析 JSON。macOS 下执行 brew install jq,Linux 下执行 apt install jq
  • 脚本权限:所有 Hook 脚本需要有可执行权限(chmod +x)。

创建 Hooks

1. 确定需求:选择事件和匹配范围

首先明确你要在哪个节点介入,以及匹配什么条件:
我想在「什么时候」拦截/处理「什么操作」?
   ↓                        ↓
 选择事件                 编写 matcher
需求事件matcher
Agent 执行任何 Shell 命令前检查PreToolUse"Bash"
Agent 写入或编辑文件后做处理PostToolUse"Write|Edit"
Agent 工具调用失败时记录PostToolUseFailure"Bash" 或不填
用户提交的所有 Prompt 做审查UserPromptSubmit不填(匹配所有)
Agent 停止时触发通知Stop不填(匹配所有)
只拦截 MCP 工具PreToolUse"mcp__.*"

2. 编写 Hook 脚本

Hook 脚本是一个标准的 shell 脚本,遵循以下协议: 输入:通过 stdin 接收 JSON 格式的事件上下文 输出:通过 exit code 控制行为
exit 0   →  放行(继续执行)
exit 2   →  阻断(停止操作,stderr 注入对话)
其他值   →  错误(继续执行,stderr 展示给用户)
脚本模板:
#!/bin/bash

# 1. 读取 stdin 的 JSON 输入
input=$(cat)

# 2. 用 jq 提取你关心的字段
#    不同事件有不同的字段,具体见「事件参考」章节
tool_name=$(echo "$input" | jq -r '.tool_name')
tool_input=$(echo "$input" | jq -r '.tool_input')

# 3. 编写你的判断逻辑
if [ "$tool_name" = "Bash" ]; then
  command=$(echo "$input" | jq -r '.tool_input.command')

  # 检查是否包含危险操作
  if echo "$command" | grep -qE 'rm\s+-rf|DROP\s+TABLE'; then
    # 阻断:exit 2 + stderr 消息会反馈给 Agent
    echo "操作被拒绝: $command" >&2
    exit 2
  fi
fi

# 4. 放行
exit 0
除了 exit code,你还可以在 exit 0 时输出 JSON 来提供更精细的控制:
#!/bin/bash
input=$(cat)

# 输出 JSON 实现精细控制(仅 exit 0 时生效)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"该操作不被允许"}}'
exit 0

3. 注册到配置文件

将脚本路径写入配置文件的对应事件下:
{
  "hooks": {
    "事件名": [
      {
        "matcher": "匹配条件(可选)",
        "hooks": [
          {
            "type": "command",
            "command": "脚本路径"
          }
        ]
      }
    ]
  }
}

4. 测试与调试

可以直接在终端用管道模拟测试:
# 模拟 PreToolUse 事件
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"hook_event_name":"PreToolUse"}' \
  | ~/.qoder/hooks/block-rm.sh
echo "Exit code: $?"
检查 stderr 输出(阻断消息):
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
  | ~/.qoder/hooks/block-rm.sh 2>&1

配置 Hooks

配置文件位置

Hook 配置从以下文件加载,多级配置会被合并执行(优先级从低到高):
位置作用域优先级可共享说明
~/.qoder/settings.json用户级1(最低)用户个人配置,对所有项目生效
.qoder/settings.json项目级2可提交到 Git,团队共享
.qoder/settings.local.json项目级(本地)3gitignore,个人开发配置
IDE / JB 插件与 CLI 共享同一套配置文件。当前版本暂不支持热加载,修改配置文件后需要重启 IDE 才能生效。

配置格式

{
  "hooks": {
    "事件名": [
      {
        "matcher": "匹配条件",
        "hooks": [
          {
            "type": "command",
            "command": "要执行的命令"
          }
        ]
      }
    ]
  }
}
字段必填说明
type"command""http"
command要执行的 shell 命令或脚本路径
timeout超时时间(秒),默认 30
matcher匹配条件,不填则匹配该事件的所有触发
if更细粒度的单条 Hook 过滤条件,形如 "ToolName""ToolName(arg_pattern)"
asynctrue 时 Hook 在后台执行,不阻塞当前操作
asyncRewaketrue 时在后台执行,并可用结果唤醒模型——适合耗时检查
statusMessageHook 运行时在状态栏显示的自定义描述
一个事件下可以配置多个 matcher 分组,每个分组可以包含多个 Hook 命令。

matcher 匹配规则

matcher 用于过滤 Hook 的触发范围,不同事件匹配不同的字段(见各事件说明)。
写法含义示例
不填或 "*"匹配所有所有工具都会触发
精确值精确匹配"Bash" 只在 Bash 工具时触发
| 分隔匹配多个值"Write|Edit" 在 Write 或 Edit 时触发
正则表达式正则匹配"mcp__.*" 匹配所有 MCP 工具

工具名映射

Qoder 支持两套工具名——原生工具名与 Claude Code 兼容工具名。配置 Hook 时可使用任意一套,插件内部会统一映射后执行匹配。例如 matcher: "Bash"matcher: "run_in_terminal" 等价。
Qoder 原生名兼容名说明
run_in_terminalBash执行 shell 命令
read_fileRead读取文件内容
create_fileWrite创建 / 写入文件
search_replaceEdit编辑文件
edit_file-基于 diff / 代码块的编辑
get_terminal_output-获取后台终端输出
delete_file-删除文件
grep_codeGrep搜索文件内容
search_fileGlob文件名匹配
list_dirLS列出目录
AgentTask启动子代理(旧称 task
Skill-调用技能
search_webWebSearch网页搜索
fetch_contentWebFetch获取网页内容
todo_writeTodoWrite写入 TODO
ask_user_question-向用户提问
search_memory-搜索记忆
update_memory-更新记忆
switch_mode-切换模式
create_plan-创建计划
run_preview-预览 Web 应用
get_problems-读取 IDE 诊断信息
fetch_rules-加载规则
ImageGen-生成图片
mcp__<server>__<tool>同左MCP 工具

编写 Hook 脚本

Hook 脚本通过 stdin 接收 JSON 输入,通过 exit code 和 stdout 输出来控制行为。本节说明所有事件通用的输入输出格式,各事件的额外字段见 Hooks 事件。

输入

Hook 脚本通过 stdin 接收 JSON 数据。所有事件都包含以下通用字段:
字段说明是否必有
session_id当前会话 ID
cwd当前工作目录
hook_event_name触发的事件名称
transcript_path会话上下文 JSON 文件路径
request_set_id当前 IDE 请求轮次的 ID
tool_name工具名(仅工具相关事件)
tool_input工具输入参数
tool_response工具执行结果(仅 PostToolUse)。IDE 当前通常为字符串
extra.email用户的 Git 邮箱
extra.repo仓库路径(group/repo 格式)
extra.branch当前分支
extra.request_time请求时间(RFC3339)
extra.response_time响应时间(RFC3339)
extra.full_diff_text本次变更的完整 diff(仅编辑类工具的 PostToolUse)
不同事件会在此基础上附加额外字段(见各 Hooks 事件)。所有字段都应按可选消费——即使某字段已声明,特定执行路径下也可能不填充。 jq 解析输入:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')

输出

Hook 通过 exit code 和 stdout 控制行为。 exit code 决定基本行为:
Exit Code含义行为
0成功继续执行,尝试解析 stdout JSON
2阻断停止操作,stderr 内容注入对话(仅对支持阻断的事件生效)
其他错误非阻断错误,stderr 显示给用户,继续执行
stdout JSON 可为部分事件提供精细控制,具体支持的字段见各事件说明。IDE 对退出码 0 和 2 都会尝试解析 stdout JSON——不要假设 exit 2 的 stdout 只会被当作纯文本。

公共 stdout 字段

除各事件在 hookSpecificOutput 内的专属字段外,以下顶层字段适用于所有事件:
字段说明
systemMessage展示给用户的消息
continueWithPromptAgent 是否继续执行
decision"block" 表示通用阻断决策。权限决策请改用 hookSpecificOutput.permissionDecision
reason决策原因(部分事件定义了自己的 reason 字段)
updatedToolOutput替换工具输出
hookSpecificOutput事件专属字段的容器(见各事件说明)

环境变量

Hook 脚本执行时,插件会注入以下环境变量供脚本使用:
变量说明
QODER_SESSION_ID会话 ID
QODER_TOOL_NAME当前工具名
QODER_CWD工作目录
QODER_TRANSCRIPT_PATHTranscript 文件路径
QODER_TOOL_INPUT_FILE_PATH工具操作的文件路径(如适用)

Hooks 事件

SessionStart

会话启动或恢复时触发。适用于在会话开始时注入启动上下文(项目规范、环境信息等)。 matcher 匹配: 无。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "SessionStart",
  "type": "startup",
  "model": "Auto"
}
字段类型说明
typestring会话启动类型。当前 IDE 传入 "startup"
modelstring会话的模型设置
stdout JSON 输出字段(成功时):
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "## Environment\n..."
  }
}

UserPromptSubmit

用户在 IDE 插件面板中提交 Prompt 后、Agent 开始处理前触发。可用于 Prompt 审查、内容过滤、自动补充上下文等场景。 matcher 匹配: 无匹配字段,该事件对所有用户输入统一触发。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "帮我写一个排序函数"
}
阻止 Prompt 提交: exit code 2,stderr 内容作为错误信息反馈给用户,Agent 不会处理该 Prompt。 stdout JSON 输出字段(exit 0 时):
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "## 当前 Git 状态\n..."
  }
}
字段类型说明
hookSpecificOutput.hookEventNamestring固定为 "UserPromptSubmit"
hookSpecificOutput.additionalContextstring附加上下文信息,会注入到 Agent 的对话中
示例:自动给 Prompt 补充项目规范
#!/bin/bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')

# 自动追加编码规范提醒
echo '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"请遵循项目 .editorconfig 中的编码规范。"}}'
exit 0

PreToolUse

工具执行前触发。可以阻止工具执行。 这是最常用的 Hook 事件,适用于危险命令拦截、文件路径校验、权限检查等场景。 matcher 匹配: 工具名(如 BashWriteEditReadGlobGrep,MCP 工具名如 mcp__server__tool 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf /tmp/build" }
}
阻止工具执行: exit code 2,stderr 内容作为错误返回给 Agent。完整示例见快速开始 stdout JSON 输出字段(exit 0 时):
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Safe read operation",
    "updatedInput": { "command": "npm test --coverage" },
    "additionalContext": "Added coverage flag"
  }
}
字段类型说明
hookSpecificOutput.hookEventNamestring固定为 "PreToolUse"
hookSpecificOutput.permissionDecisionstring"allow"(放行)、"deny"(拒绝)或 "ask"(询问用户)
hookSpecificOutput.permissionDecisionReasonstring决策原因,denyask 时展示给 Agent / 用户
hookSpecificOutput.updatedInputobject修改后的工具输入参数(可选,用于改写工具调用)
hookSpecificOutput.additionalContextstring附加上下文信息(可选)

PermissionRequest

工具调用需要用户授权时触发。可以自动做出授权决策——适用于自动放行安全操作,或自动拒绝违反策略的操作,无需弹窗询问用户。 matcher 匹配: 工具名 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf node_modules" },
  "tool_use_id": "call_01ABC123"
}
stdout JSON 输出字段(成功时):
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Safe cleanup command",
    "updatedInput": { "command": "rm -rf node_modules" }
  }
}
字段类型说明
hookSpecificOutput.permissionDecisionstring"allow"(放行)、"deny"(拒绝)或 "ask"(回退为询问用户)
hookSpecificOutput.permissionDecisionReasonstring决策原因
hookSpecificOutput.updatedInputobject修改后的工具输入参数(可选)
IDE 与 CLI 在该事件上使用不同的输出结构——CLI 期望的是嵌套的 decision.behavior 对象。请勿在未适配输出的情况下把同一脚本同时用于两个产品。

PostToolUse

工具执行成功后触发。不可阻断。适用于自动 lint、日志记录、结果分析等场景。 matcher 匹配: 工具名 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": { "file_path": "/path/to/file.ts", "file_content": "..." },
  "tool_response": "File written successfully"
}
stdout JSON 输出字段(exit 0 时):
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "feedback": "File formatted with Prettier. 3 issues auto-fixed."
  }
}
字段类型说明
hookSpecificOutput.hookEventNamestring固定为 "PostToolUse"
hookSpecificOutput.feedbackstring反馈信息,会展示给用户(如 lint 结果摘要)

PostToolUseFailure

工具调用失败后触发。不可阻断。适用于错误监控、失败重试提示、日志记录等场景。 matcher 匹配: 工具名 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "PostToolUseFailure",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" },
  "tool_use_id": "call_01ABC123",
  "error": "Command exited with non-zero status code 1",
  "is_interrupt": false
}
字段类型说明
errorstring工具执行的错误信息
is_interruptboolean失败是否由中断导致

Stop

Agent 完成响应后触发(主 Agent 无待执行的工具调用时)。可阻断 Agent 停止。适用于质量门禁、桌面通知、日志记录、任务状态汇报等场景。 matcher 匹配: 无匹配字段,该事件在 Agent 停止时统一触发。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "Stop",
  "stop_hook_active": true,
  "last_assistant_message": "我已经完成了排序函数的编写。"
}
字段类型说明
stop_hook_activeboolean当 Agent 因上一次 Stop Hook 阻断而重试时为 true脚本必须检查该字段,为 true 时 exit 0,避免死循环
last_assistant_messagestringAgent 最后一段文本回复
阻断 Agent 停止: exit 2。阻断原因作为用户消息注入对话,Agent 继续工作。 stdout JSON 输出字段(exit 0 时):
{
  "decision": "block",
  "reason": "Tests failing. Fix them before completing."
}
字段类型说明
decisionstring"block"(阻止停止,让 Agent 继续工作)
reasonstring阻止的原因,会作为消息注入对话
防止无限循环: Stop Hook 阻断 Agent(exit 2)后,Agent 会重试并再次触发 Stop 事件,此时 stop_hook_active: true。脚本必须检查该字段并在其为 trueexit 0,否则会无限阻断。

SubagentStart

子代理启动时触发。不可阻断。适用于子代理审计或注入子代理上下文。 matcher 匹配: 无。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "SubagentStart",
  "agent_id": "a1b2c3d4",
  "agent_type": "task"
}
stdout JSON 输出字段(成功时): hookSpecificOutput.additionalContext——注入到子代理对话中的上下文。

SubagentStop

子代理停止时触发。在 IDE 中不可阻断。适用于记录子代理结果。 matcher 匹配: 无。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "SubagentStop",
  "agent_id": "a1b2c3d4",
  "agent_type": "task",
  "stop_hook_active": false,
  "agent_transcript_path": "/path/to/agent-transcript.jsonl",
  "last_assistant_message": "Sub-task finished."
}
字段类型说明
agent_idstring子代理 ID
agent_typestring子代理类型
stop_hook_activeboolean是否是 Hook 阻断后的重试停止
agent_transcript_pathstring子代理的 transcript 文件路径
last_assistant_messagestring子代理的最后一条文本响应

SessionEnd

会话结束时触发。不可阻断——适用于清理或会话归档等副作用操作。 matcher 匹配: 无。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "SessionEnd",
  "reason": "exit"
}
请从输入字段 reason 中读取结束原因。(历史 matcher 元数据曾使用 exit_reason 这个名称;脚本应以 stdin 中实际的 reason 字段为准。)

PreCompact

上下文压缩前触发。在 IDE 中该事件仅用于通知和副作用——无法阻断压缩。 matcher 匹配: 触发方式(manual 为用户手动压缩,auto 为接近上下文上限时的自动压缩)。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreCompact",
  "trigger": "auto",
  "custom_instructions": ""
}

Notification

插件发出面向用户的通知时触发。不可阻断。适用于将通知转发到外部渠道(桌面、IM 等)。 matcher 匹配: 通知类型。 额外输入字段:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "Notification",
  "notification_type": "permission_prompt",
  "title": "Permission Required",
  "message": "Agent is requesting permission to run: rm -rf node_modules"
}
stdout JSON 输出字段(成功时): hookSpecificOutput.additionalContext

注意事项

  • 超时处理: Hook 脚本默认超时 30 秒,可通过 timeout 字段按 Hook 配置。超时后脚本将被终止,按放行处理。
  • 错误处理: 脚本异常退出(exit code 非 0 且非 2)时,错误信息显示给用户,Agent 流程不受影响继续执行。
  • 脚本权限: 确保脚本具有可执行权限(chmod +x)。
  • 配置合并: 多级配置文件中的同一事件 Hook 按优先级从低到高依次执行,任一 Hook 返回阻断(exit 2)则终止后续执行。
  • jq 依赖: 示例脚本依赖 jq 命令行工具来解析 JSON。请确保系统已安装 jq(macOS: brew install jq,Linux: apt install jq)。

最佳实践场景

谁适合用 Hooks

Hooks 的价值取决于你的角色,下面是一个快速对照:

个人开发者

场景说明实践
Prompt 增强自动注入项目专属的 Skill 与编码规范——不必每次手动输入场景 1
敏感数据拦截避免密码、密钥、内网 IP 被发送给模型场景 2
危险命令阻断阻断 rm -rfgit push --force 等破坏性命令场景 6
Harness 自进化会话结束时自动识别可复用经验,并触发复盘场景 8

团队 / 企业

场景说明实践
Rule/Skill 使用统计追踪团队中哪些 Rules 和 Skills 被触发,评估资产质量场景 3
文件编辑追踪记录 Agent 修改了哪些文件,用于变更审计与影响分析场景 4
全局使用统计采集对话内容、工具调用、模型回复——用于效能分析的全链路数据场景 5
安全管控落地团队统一的危险命令黑名单场景 6
质量门禁在 Agent 完成前自动执行构建 / 测试,未通过则阻断并修复场景 7
个人场景通常配置在 ~/.qoder/settings.json(用户级)或 .qoder/settings.local.json(项目级本地配置)中。团队场景应放在 .qoder/settings.json(项目级)并提交到 Git,以确保统一生效。

场景 1:Prompt 增强——自动注入 Skill

痛点:每次都要手动指定 Skill,或者忘记加载项目专属上下文。 方案:用 UserPromptSubmit Hook 自动注入 Prompt 提示,引导 Agent 使用指定的 Skill。 配置:
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/inject-skill-hint.sh"
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/inject-skill-hint.sh
#!/bin/sh
# 功能:在 Prompt 提交时,注入 Skill 使用提示(每个会话仅注入一次)
INPUT=$(cat)

# === 会话级去重:同一 session 只注入一次 ===
# 未来可使用 SessionStart 事件(即将支持)替代,届时无需去重逻辑
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
DEDUP_DIR="/tmp/hook-dedup"
mkdir -p "$DEDUP_DIR"

if [ -n "$SESSION_ID" ] && [ -f "$DEDUP_DIR/skill-hint-$SESSION_ID" ]; then
  exit 0  # 本会话已注入过,跳过
fi
# ============================================

# 读取项目约定的 skill 使用规则(可根据项目自定义)
SKILL_HINT=""

# === 请根据你的项目实际情况修改以下内容 ===
# 示例1:始终提示使用 git-commit skill
# SKILL_HINT="如果用户要求提交代码,请使用 /git-commit skill"

# ============================================

if [ -z "$SKILL_HINT" ]; then
  exit 0
fi

# 标记本会话已注入
[ -n "$SESSION_ID" ] && touch "$DEDUP_DIR/skill-hint-$SESSION_ID"

cat <<EOF
{"hookSpecificOutput": {"additionalContext": "$SKILL_HINT"}}
EOF

exit 0
关键点:
  • additionalContext 会作为 system-reminder 追加到用户 Prompt 后注入给 Agent
  • 使用 session_id + 临时文件实现会话级去重,避免每一轮都注入相同提示
  • 脚本可以读取项目配置文件,实现项目专属的 Skill 推荐
  • exit 0 放行 Prompt;exit 2 阻断 Prompt(例如用于不合规的 Prompt)

场景 2:敏感 Prompt 阻断

痛点:用户可能在 Prompt 中不小心带入密码、密钥、内网 IP 或个人数据,存在信息泄露风险。 方案:用 UserPromptSubmit Hook 检测敏感内容并阻断 Prompt。 配置:
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/block-sensitive-prompt.sh"
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/block-sensitive-prompt.sh
#!/bin/sh
# 功能:检测用户 Prompt 中的敏感信息,命中则阻断
INPUT=$(cat)

# 提取用户 Prompt 内容
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')

if [ -z "$PROMPT" ]; then
  exit 0
fi

# === 请根据团队安全规范调整敏感词规则 ===

# 1. 密钥/凭证类模式
SECRET_PATTERNS="password=|passwd=|secret_key=|access_key=|AKIA[0-9A-Z]{16}|token=[a-zA-Z0-9]{20,}"

# 2. 内部网络信息
INTERNAL_PATTERNS="10\.[0-9]+\.[0-9]+\.[0-9]+|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\." 

# 3. 自定义敏感词(团队内部术语、项目代号等)
# CUSTOM_PATTERNS="项目代号X|内部接口地址"

# 合并所有模式
ALL_PATTERNS="$SECRET_PATTERNS|$INTERNAL_PATTERNS"

# 执行检测
MATCH=$(echo "$PROMPT" | grep -oiE "$ALL_PATTERNS" | head -1)

if [ -n "$MATCH" ]; then
  echo "检测到敏感信息: $MATCH" >&2
  exit 2  # 阻断 Prompt 提交
fi

# ============================================

exit 0
关键点:
  • UserPromptSubmit可阻断事件exit 2 会直接阻止 Prompt 到达 Agent
  • 敏感模式支持正则表达式,匹配更灵活(例如 AWS AKIA 前缀、内网 IP 段)
  • 建议把模式提取到独立配置文件(如 .qoder/hooks/sensitive-patterns.txt),便于团队统一维护
  • 如需更精确的检测,可调用 gitleakstrufflehog 等外部工具
与场景 1 的区别:场景 1 用 exit 0 + additionalContext 做 Prompt 增强(注入上下文),本场景用 exit 2 做 Prompt 阻断(拒绝不合规输入)。两者可以共存于同一个 UserPromptSubmit 事件下,并按配置顺序执行。

场景 3:Rule/Skill 使用统计

痛点:配置了很多 Rules 和 Skills,但实际使用率不明。 方案:用 Transcript 体系 + Stop Hook,在每次对话结束后自动分析并记录 Rule/Skill 的触发数据。 配置:
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/analyze-rule-skill-usage.sh"
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/analyze-rule-skill-usage.sh
#!/bin/sh
# 功能:在 Agent 完成响应时,分析本次会话的 Rule/Skill 使用情况
INPUT=$(cat)

# 从 stdin 提取字段
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')

if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
  exit 0
fi

# === 📝 分析逻辑 — 请根据实际需求调整 ===

# 1. 提取 session_meta 中的 rules 信息(JSONL 逐行解析)
RULES_META=$(jq -c 'select(.data.meta_type == "rules") | .data.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -1)

# 2. 提取 slash_command 信息(用户通过 / 触发的 Skill)
SLASH_SKILL_META=$(jq -c 'select(.data.meta_type == "slash_command") | .data.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -1)

# 3. 提取 tool_name="Skill" 的调用(Agent 主动触发的 Skill 工具调用)
#    说明:除了用户手动 /skill-name 触发外,Agent 也会自行调用 Skill 工具

AUTO_SKILL_CALLS=$(jq -c 'select(.message.content[ ]? | select(.type == "tool_use" and .name == "Skill"))' "$TRANSCRIPT_PATH" 2>/dev/null)

AUTO_SKILL_COUNT=$(printf '%s' "$AUTO_SKILL_CALLS" | grep -c . 2>/dev/null || echo 0)

AUTO_SKILL_NAMES=$(printf '%s' "$AUTO_SKILL_CALLS" | jq -r '.message.content[ ] | select(.type == "tool_use" and .name == "Skill") | .input.skill' 2>/dev/null | sort -u | jq -R -s 'split("\n") | map(select(. != ""))')


# 4. 统计全部工具调用次数

TOOL_COUNT=$(jq -c 'select(.message.content[ ]?.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')


# 5. 记录到统计文件
STATS_DIR="$HOME/.qoder/stats"
mkdir -p "$STATS_DIR"
DATE=$(date +%Y-%m-%d)
STATS_FILE="$STATS_DIR/usage-${DATE}.jsonl"

jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg sid "$SESSION_ID" \
  --argjson tc "$TOOL_COUNT" \
  --argjson rules "${RULES_META:-null}" \
  --argjson slash_skill "${SLASH_SKILL_META:-null}" \
  --argjson auto_skill_count "$AUTO_SKILL_COUNT" \
  --argjson auto_skill_names "${AUTO_SKILL_NAMES:-null}" \
  '{timestamp:$ts, session_id:$sid, tool_calls:$tc, rules:$rules, slash_skill:$slash_skill, auto_skill: {count:$auto_skill_count, names:$auto_skill_names}}' >> "$STATS_FILE"

# ============================================

exit 0
Transcript 自动记录的元数据包括:
  • session_meta(rules): 本次会话加载的所有 Rules(名称、触发类型、文件路径)
  • session_meta(slash_command): 本次会话使用的 Skills(名称、类型、文件路径)
  • session_meta(session_info): 会话模式(agent/plan)与会话类型
进阶用法:编写一个定期汇总脚本,读取 ~/.qoder/stats/ 下的 JSONL 文件,生成 Rule/Skill 使用报告。

场景 4:文件编辑追踪

痛点:不清楚 Agent 在一次会话中修改了哪些文件、修改了多少次。 方案:用 PostToolUse Hook 匹配文件编辑类工具,实时记录每一次文件变更。 配置:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|search_replace|create_file",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/track-file-changes.sh"
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/track-file-changes.sh
#!/bin/sh
# 功能:追踪 Agent 的文件编辑操作
INPUT=$(cat)

SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH="$QODER_TOOL_INPUT_FILE_PATH"

if [ -z "$FILE_PATH" ]; then
  exit 0
fi

# === 📝 文件变更追踪逻辑 ===

# 记录变更日志
CHANGE_LOG="$HOME/.qoder/stats/file-changes.jsonl"
mkdir -p "$(dirname "$CHANGE_LOG")"

TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
BRANCH=$(printf '%s' "$INPUT" | jq -r '.extra.branch // empty')
REPO=$(printf '%s' "$INPUT" | jq -r '.extra.repo // empty')

echo "{\"ts\":\"$TIMESTAMP\",\"session\":\"$SESSION_ID\",\"tool\":\"$TOOL_NAME\",\"file\":\"$FILE_PATH\",\"branch\":\"$BRANCH\",\"repo\":\"$REPO\"}" >> "$CHANGE_LOG"

# ============================================

exit 0
进阶用法:
  • additionalContext 把变更统计反馈给 Agent(例如「本次会话已修改 15 个文件」)
  • 对接团队的代码分析系统,追踪 AI 辅助产生的变更量

场景 5:全局使用统计

痛点:缺乏 Agent 整体使用情况的量化数据——无法评估 AI 辅助编程的效能与质量。具体包括:用户问了什么、模型返回了什么文本、调用了哪些工具以及结果如何。 方案:用多事件 Hook 组合构建全链路的使用数据采集。UserPromptSubmit 采集用户提问,PostToolUse 采集工具调用结果,Stop 通过 Transcript 分析完整的会话摘要(模型回复、工具调用分布等)。 配置:
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/usage-tracker.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/usage-tracker.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/usage-tracker.sh"
          }
        ]
      }
    ]
  }
}
脚本 ~/.qoder/hooks/usage-tracker.sh
#!/bin/sh
# 功能:全局使用情况追踪(统一入口,按事件类型分发)
INPUT=$(cat)

EVENT=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# 获取额外上下文
EMAIL=$(printf '%s' "$INPUT" | jq -r '.extra.email // empty')
REPO=$(printf '%s' "$INPUT" | jq -r '.extra.repo // empty')
BRANCH=$(printf '%s' "$INPUT" | jq -r '.extra.branch // empty')

# === 📝 数据采集逻辑 ===

STATS_DIR="$HOME/.qoder/stats"
mkdir -p "$STATS_DIR"
DATE=$(date +%Y-%m-%d)

case "$EVENT" in
  UserPromptSubmit)
    # 采集用户提问内容(截取前 200 字符避免日志过大)
    PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')
    PROMPT_PREVIEW=$(printf '%.200s' "$PROMPT")
    echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"prompt\",\"session\":\"$SESSION_ID\",\"email\":\"$EMAIL\",\"repo\":\"$REPO\",\"branch\":\"$BRANCH\",\"prompt_preview\":\"$PROMPT_PREVIEW\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
    ;;
  PostToolUse)
    TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
    # 采集工具调用结果(截取前 500 字符)
    TOOL_RESPONSE=$(printf '%s' "$INPUT" | jq -r '.tool_response // empty')
    TOOL_RESPONSE_PREVIEW=$(printf '%.500s' "$TOOL_RESPONSE")
    echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"tool_use\",\"session\":\"$SESSION_ID\",\"tool\":\"$TOOL_NAME\",\"repo\":\"$REPO\",\"tool_response_preview\":\"$TOOL_RESPONSE_PREVIEW\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
    ;;
  Stop)
    # 📊 通过 Transcript 采集完整会话摘要
    TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
    SUMMARY=""
    if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
      USER_MSG_COUNT=$(jq -c 'select(.type == "user" and (.message.content | type == "string"))' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
      USER_PROMPTS=$(jq -r 'select(.type == "user" and (.message.content | type == "string")) | .message.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -20)

      ASSISTANT_TEXT_COUNT=$(jq -c 'select(.type == "assistant" and (.message.content[ ]? | select(.type == "text")))' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      ASSISTANT_TEXTS=$(jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "text") | .text' "$TRANSCRIPT_PATH" 2>/dev/null | head -20)

      TOOL_CALL_COUNT=$(jq -c 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      TOOL_NAMES=$(jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | .name' "$TRANSCRIPT_PATH" 2>/dev/null | sort | uniq -c | sort -rn | head -10)

      TOOL_SUCCESS=$(jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == false)' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      TOOL_ERROR=$(jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == true)' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      SUMMARY="user_msgs:${USER_MSG_COUNT},assistant_texts:${ASSISTANT_TEXT_COUNT},tool_calls:${TOOL_CALL_COUNT},tool_success:${TOOL_SUCCESS},tool_error:${TOOL_ERROR}"
    fi
    echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"stop\",\"session\":\"$SESSION_ID\",\"repo\":\"$REPO\",\"summary\":\"$SUMMARY\"}" >> "$STATS_DIR/events-${DATE}.jsonl"

    if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
      SUMMARY_DIR="$STATS_DIR/sessions"
      mkdir -p "$SUMMARY_DIR"
      cat <<SUMMARY_EOF > "$SUMMARY_DIR/${SESSION_ID}.json"
{
  "session_id": "$SESSION_ID",
  "timestamp": "$TIMESTAMP",
  "repo": "$REPO",
  "branch": "$BRANCH",
  "email": "$EMAIL",
  "user_message_count": $USER_MSG_COUNT,
  "assistant_text_count": $ASSISTANT_TEXT_COUNT,
  "tool_call_count": $TOOL_CALL_COUNT,
  "tool_success": $TOOL_SUCCESS,
  "tool_error": $TOOL_ERROR,
  "tool_distribution": "$TOOL_NAMES",
  "user_prompts_preview": $(printf '%s' "$USER_PROMPTS" | head -c 2000 | jq -Rs .),
  "assistant_texts_preview": $(printf '%s' "$ASSISTANT_TEXTS" | head -c 2000 | jq -Rs .),
  "transcript_path": "$TRANSCRIPT_PATH"
}
SUMMARY_EOF
    fi
    ;;
esac

# ============================================

exit 0
数据采集维度:
事件采集的数据数据来源
UserPromptSubmit用户提问(前 200 字符)stdin JSON 中的 prompt 字段
PostToolUse工具名 + 调用结果(前 500 字符)stdin JSON 中的 tool_nametool_response 字段
Stop完整会话摘要:用户提问、模型回复、工具调用分布、成功 / 失败率transcript_path 指向的 JSONL 文件
为何在 Stop 通过 Transcript 分析? UserPromptSubmitPostToolUse 只能采集当前一次交互的数据,而 Stop 时刻的 Transcript 文件已包含完整的会话历史——可以一次性提取模型回复文本、工具调用分布以及成功 / 失败率。详见Transcript 文件格式

场景 6:安全管控——危险命令阻断

痛点:Agent 可能执行 rm -rfgit push --force 等危险命令。 方案:用 PreToolUse Hook 匹配 Bash|run_in_terminal,在命令执行前拦截。 配置:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|run_in_terminal",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/block-dangerous-commands.sh"
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/block-dangerous-commands.sh
#!/bin/sh
# 功能:拦截危险 Shell 命令
INPUT=$(cat)

# 提取要执行的命令(从 tool_input.command 中读取)
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')

# === 📝 危险命令黑名单 — 请根据团队规范调整 ===
DANGEROUS_PATTERNS="rm -rf|git push --force|git push -f|DROP TABLE|DROP DATABASE|format |mkfs"

if echo "$COMMAND" | grep -qiE "$DANGEROUS_PATTERNS"; then
  echo "检测到危险命令: $COMMAND" >&2
  exit 2  # 阻断执行
fi

# ============================================

exit 0
关键点:
  • exit 2 会立即阻断执行,stderr 内容作为阻断原因反馈给 Agent
  • Agent 被阻断后会尝试替代方案(例如换一个更安全的命令)
  • 如需更丰富的反馈,可在 stdout 返回 JSON:
{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"Command contains rm -rf, blocked for safety"}}

场景 7:Agent 完成前的质量门禁

痛点:Agent 声称任务已完成,但测试仍在失败或还有遗留问题。 方案:用 Stop Hook(可阻断)在 Agent 完成前执行质量检查;检查未通过则阻断,强制 Agent 继续处理。 配置:
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/quality-gate.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/quality-gate.sh
#!/bin/sh
# 功能:Agent 完成前的质量门禁检查
INPUT=$(cat)

# 检查是否已经是 Stop Hook 触发的循环(防止无限循环)
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0  # 已经是 Stop Hook 触发的重试,放行
fi

# === 📝 质量检查逻辑 — 请根据项目实际情况调整 ===
ERRORS=""

# 1. 检查是否有编译错误
# if ! make build 2>/dev/null; then
#   ERRORS="${ERRORS}\n- 编译失败,请修复编译错误"
# fi

# 2. 检查是否有测试失败
# if ! make test 2>/dev/null; then
#   ERRORS="${ERRORS}\n- 测试未通过,请修复失败的测试"
# fi

# 3. 检查是否有未提交的 TODO 标记
# TODO_COUNT=$(grep -r "TODO(agent)" . --include="*.go" 2>/dev/null | wc -l | tr -d ' ')
# if [ "$TODO_COUNT" -gt 0 ]; then
#   ERRORS="${ERRORS}\n- 发现 $TODO_COUNT 个未完成的 TODO 标记"
# fi

# ============================================

if [ -n "$ERRORS" ]; then
  cat <<EOF
{"decision":"block","reason":"质量检查未通过:$ERRORS\n请修复以上问题后再完成。"}
EOF
  exit 2
fi

exit 0
关键点:
  • stop_hook_active 用于防止无限循环(Agent 被阻断后重试时该字段为 true
  • Stop Hook 阻断后,阻断原因会作为用户消息注入,Agent 继续工作
  • 由于构建和测试可能较耗时,建议设置更长的 timeout(例如 120 秒)

场景 8:Harness 自进化——自动化知识沉淀

痛点:每次任务产生的经验和决策散落在对话历史里,没有自动化的沉淀流程。 方案:用 Stop Hook 自动触发 Harness 自进化流程,分析本次对话是否产生了可复用的经验,并推动资产生命周期运转。 配置:
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/harness-evolution.sh"
          }
        ]
      }
    ]
  }
}
脚本 .qoder/hooks/harness-evolution.sh
#!/bin/sh
# 功能:Agent 完成响应时,触发 Harness 自进化流程
INPUT=$(cat)

# ⚠️ 【关键】防止无限循环:检查 stop_hook_active 标志
# 当 Stop Hook 阻断 Agent 后,Agent 会重新尝试完成,此时 stop_hook_active=true
# 必须在此处放行,否则会陷入「阻断→重试→再阻断」的死循环
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0  # 已经是 Stop Hook 触发的重试,直接放行
fi

TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')

if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
  exit 0
fi

# === 📝 Harness 自进化检测逻辑 ===

# 1. 统计本次会话的工具调用和文件变更

EDIT_COUNT=$(jq -c 'select(.message.content[ ]?.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')


# 2. 检查是否涉及架构设计、决策讨论、规范制定等关键词
# HAS_DECISION=$(jq -r 'select(.message.content | type == "string") | .message.content' "$TRANSCRIPT_PATH" 2>/dev/null | grep -c '架构\|方案\|规范\|最佳实践' || echo 0)

# 3. 记录会话摘要到 inbox,供后续批量复盘
SEDIMENTATION_DIR="$HOME/.ai/inbox"
mkdir -p "$SEDIMENTATION_DIR"
echo "{\"session_id\":\"$SESSION_ID\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"tool_calls\":$EDIT_COUNT,\"transcript\":\"$TRANSCRIPT_PATH\"}" >> "$SEDIMENTATION_DIR/pending-review.jsonl"

# 4. 可选方案 A:调用外部分析总结系统
# 通过 transcript 和会话上下文调用自己的分析服务
# curl -s -X POST http://localhost:8080/api/analyze \
#   -H 'Content-Type: application/json' \
#   -d "{\"session_id\":\"$SESSION_ID\",\"transcript\":\"$TRANSCRIPT_PATH\"}" \
#   > /dev/null 2>&1 &

# 5. 可选方案 B:通过阻断触发分析总结 Skill
# 取消下面的注释,可让 Agent 被阻断后自动进入复盘 Skill 流程
# 注意:/retro 是自定义的沉淀复盘技能,你可以替换为自己团队的复盘 Skill。
# cat <<'EOF'
# {"decision":"block","reason":"任务已完成。检测到本次对话可能有值得沉淀的经验(文件变更较多)。请运行 /retro 进行复盘,提炼可复用的经验并沉淀到资产体系。"}
# EOF
# exit 2

# ============================================

exit 0
关键点:
  • 防止无限循环(必须): Stop Hook 脚本必须检查 stop_hook_active。当 Agent 被 Stop Hook 阻断后重试时,该字段为 true,此时应立即 exit 0,避免陷入无限循环
  • exit 2 + decision:"block" 阻断 Agent 完成,强制进入复盘流程
  • pending-review.jsonl 用于记录会话,供后续批量复盘
  • 可以与 /retro Skill(自定义的复盘技能)组合,形成自动检测 → 提醒 → 沉淀的闭环
当前的实现方式:
Hooks 目前只支持 commandhttp 两种类型的处理器(不支持 prompt / agent),因此 Harness 自进化有两条实现路径:(A)调用外部分析服务,或(B)阻断 Agent 并触发复盘 Skill。
未来方向:
Hook 体系计划支持 prompt 类型与 agent 类型的处理器。届时 Harness 自进化将从「脚本驱动」升级为「Agent 驱动」,真正实现端到端的自动化知识沉淀。

场景 9:写文件后自动 Lint

每次 Agent 写入或编辑文件后,自动执行 lint 检查。 脚本 ${project}/.qoder/hooks/auto-lint.sh
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')

# 只检查 JS/TS 文件
case "$file_path" in
  *.js|*.ts|*.jsx|*.tsx)
    npx eslint "$file_path" --fix 2>/dev/null
    ;;
esac

exit 0
配置:事件 PostToolUse,matcher Write|Edit,command .qoder/hooks/auto-lint.sh

场景 10:工具失败时记录日志

当 Agent 调用的工具执行失败时,自动记录到日志文件,便于排查问题。 脚本 ~/.qoder/hooks/log-failure.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
error=$(echo "$input" | jq -r '.error')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$timestamp] $tool_name 执行失败: $error" >> ~/.qoder/hooks/failure.log

exit 0
配置:事件 PostToolUseFailure,无 matcher(匹配所有工具),command ~/.qoder/hooks/log-failure.sh

场景 11:Agent 完成后桌面通知

Agent 完成任务后弹出系统通知,适合长时间运行的任务。 脚本 ~/.qoder/hooks/notify-done.sh(macOS):
#!/bin/bash
input=$(cat)
message=$(echo "$input" | jq -r '.last_assistant_message // "任务已完成"' | head -c 100)

osascript -e "display notification \"$message\" with title \"Qoder Agent\""

exit 0
配置:事件 Stop,无 matcher,command ~/.qoder/hooks/notify-done.sh

Transcript 文件格式

Transcript 是 Qoder 自动生成的会话日志文件,位于 transcript_path 指向的路径(如 ~/.qoder/projects/<project>/transcript/<session-id>.jsonl)。文件中每一行都是一个独立的 JSON 对象,按时间顺序追加,完整记录会话的交互过程。

每行的公共字段

字段类型说明
typestring记录类型:session_meta / user / assistant / progress
sessionIdstring会话 ID
uuidstring本条记录的唯一 ID
timestampstringISO 8601 时间戳
cwdstring当前工作目录
messageobject消息内容(user/assistant 类型具有该字段)
dataobject元数据(session_meta/progress 类型具有该字段)

记录类型

1. session_meta — 会话元数据(第一行) 每个 Transcript 文件的第一行记录会话的基本信息:
{
  "type": "session_meta",
  "sessionId": "86379a0e-...",
  "data": {
    "meta_type": "session_info",
    "content": {
      "mode": "agent",
      "session_type": "assistant"
    }
  }
}
  • data.content.mode:会话模式(agent / plan / ask / debug
  • data.content.session_type:会话类型(assistant / inline_chat 等)
2. user — 用户消息 用户消息有两种形式: 用户提问message.content 为字符串):
{
  "type": "user",
  "message": {
    "role": "user",
    "content": "Delete comments from test.py"
  }
}
工具结果message.content 为包含 tool_result 的数组):
{
  "type": "user",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "tool_result",
        "tool_use_id": "call_d498c5988a...",
        "content": "Contents of /path/to/file.py, from line 1-23 ...",
        "is_error": false
      }
    ]
  },
  "toolUseResult": "Contents of /path/to/file.py ..."
}
  • is_error:工具执行是否失败
  • toolUseResult:工具结果的快捷字段(内容同 content[0].content
3. assistant — 模型回复 模型回复的 message.content 始终是一个数组,包含两种元素类型: 文本回复type: "text"):
{
  "type": "assistant",
  "message": {
    "role": "assistant",
    "content": [
      {
        "type": "text",
        "text": "I'll help you delete comments from test.py. Let me read the file first.\n\n"
      }
    ]
  }
}
工具调用type: "tool_use"):
{
  "type": "assistant",
  "message": {
    "role": "assistant",
    "content": [
      {
        "type": "tool_use",
        "id": "call_d498c5988a...",
        "name": "read_file",
        "input": {
          "file_path": "/path/to/file.py"
        }
      }
    ]
  }
}
  • name:工具名(如 read_filesearch_replacerun_in_terminalSkill
  • input:工具调用参数
  • id:与后续 tool_result 中的 tool_use_id 对应
4. progress — Hook 触发记录 记录 Hook 脚本何时触发、执行了哪些命令:
{
  "type": "progress",
  "data": {
    "type": "hook_progress",
    "hookEvent": "UserPromptSubmit",
    "hookName": "UserPromptSubmit",
    "command": ".qoder/hooks/inject-skill-hint.sh"
  }
}

会话时间线示例

一个典型会话的 Transcript 记录顺序:
L1  session_meta          ← Session start, record mode and type
L2  progress              ← UserPromptSubmit Hook fires
L3  user (string)         ← User question: "Delete comments from test.py"
L4  assistant (text)      ← Model reply: "I'll help you..."
L5  assistant (tool_use)  ← Model calls read_file
L6  user (tool_result)    ← read_file returns file content
L7  assistant (text)      ← Model reply: "Now I'll delete..."
L8  assistant (tool_use)  ← Model calls search_replace
L9  user (tool_result)    ← search_replace succeeds
L10 assistant (text)      ← Model reply: "Successfully deleted..."
L11 progress              ← Stop Hook fires
L12 assistant (text)      ← Final reply after Stop Hook

常用的 jq 提取命令

TRANSCRIPT="$TRANSCRIPT_PATH"

# Extract all user questions (filter out tool results)
jq -r 'select(.type == "user" and (.message.content | type == "string")) | .message.content' "$TRANSCRIPT"

# Extract all model text replies
jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "text") | .text' "$TRANSCRIPT"

# Extract all tool calls (tool name + parameters)
jq -c 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | {name, input}' "$TRANSCRIPT"

# Count tool call distribution (tool name + count)
jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | .name' "$TRANSCRIPT" | sort | uniq -c | sort -rn

# Extract failed tool calls
jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == true)' "$TRANSCRIPT"

# Get session mode
jq -r 'select(.type == "session_meta") | .data.content.mode' "$TRANSCRIPT"

# Get Hook trigger records
jq -c 'select(.type == "progress" and .data.type == "hook_progress") | {event: .data.hookEvent, command: .data.command}' "$TRANSCRIPT"

设计原则

原则说明
快速失败Hook 脚本应该轻量,避免阻塞 Agent 主流程
优雅降级异常退出码(非 0 也非 2)不阻断 Agent,保证容错能力
单一职责每个脚本只做一件事,复杂逻辑用多个 Hook 组合实现
幂等设计同一事件可能多次触发,脚本应该具备幂等性
防止循环Stop Hook 必须检查 stop_hook_active,避免无限重试

推荐的 Hook 组合

目标推荐组合
安全管控PreToolUse(Bash) 危险命令阻断
质量保障Stop 质量门禁
数据驱动洞察UserPromptSubmit + PostToolUse + Stop 全链路采集
Harness 自进化Stop 沉淀检测 + UserPromptSubmit Skill 引导
团队協作项目级 .qoder/settings.json 统一配置,通过 Git 共享

调试指南

第 1 步:确认 Hook 已触发

当 Hook 没有按预期触发时,在脚本的最开头加上调试日志:
#!/bin/sh
# ===== Debug mode: confirm hook fires =====
INPUT=$(cat)
printf '[HOOK DEBUG] %s hook triggered, event=%s\n' "$(date '+%H:%M:%S')" "$(printf '%s' "$INPUT" | jq -r '.hook_event_name')" >> /tmp/hook-debug.log
printf '%s' "$INPUT" | jq . >> /tmp/hook-debug.log
echo "---" >> /tmp/hook-debug.log
# ===== Remove debug code when done =====

# ... your actual logic ...
exit 0
然后触发一次 Agent 操作,查看日志:
$ tail -f /tmp/hook-debug.log
[HOOK DEBUG] 14:32:01 hook triggered, event=PreToolUse
{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "run_in_terminal",
  "tool_input": {
    "command": "ls -la"
  },
  ...
}
排查思路:如果没有任何输出,说明 Hook 没有触发。请检查配置文件位置、事件名、matcher 匹配模式以及脚本路径。

第 2 步:用真实输入在本地复现

用第 1 步捕获到的 JSON,在 Qoder 之外测试脚本:
# Pipe the captured input to simulate stdin
$ cat /tmp/hook-debug.json | sh .qoder/hooks/pre-tool-check.sh
$ echo $?   # Check exit code: 0=allow, 2=block

# Or manually construct test input
$ echo '{"hook_event_name":"PreToolUse","tool_name":"run_in_terminal","tool_input":{"command":"rm -rf /"}}' | sh .qoder/hooks/pre-tool-check.sh
$ echo $?   # Expected: 2 (block dangerous command)
小技巧:把常用的测试用例保存为文件,方便反复验证。

第 3 步:其他调试手段

  1. 查看 Transcript: ~/.qoder/projects/<encoded-path>/transcript/<session>.jsonl,用 jq 逐行解析
  2. 查看 Hook 日志: 在 Qoder 日志中搜索 [hook] 前缀,可看到执行结果与耗时
  3. 从简单开始: 先用一个只有 exit 0 的脚本验证 Hook 能触发,再逐步加入业务逻辑
  4. 及时清理: 调试完成后删除调试代码(写 /tmp/hook-debug.log 的部分),避免影响性能

快速开始模板

最小配置

将以下内容保存到 ~/.qoder/settings.json(用户级)或 <project>/.qoder/settings.json(项目级):
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/pre-tool-check.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/post-edit-track.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/on-stop.sh"
          }
        ]
      }
    ]
  }
}

项目目录结构

<project>/
├── .qoder/
│   ├── settings.json        ← Hook config (Git-shared)
│   ├── settings.local.json  ← Local override config (.gitignore)
│   └── hooks/               ← Hook scripts directory
│       ├── pre-tool-check.sh
│       ├── post-edit-track.sh
│       └── on-stop.sh
└── ...

完整配置示例

以下是一个包含五种常用事件的完整配置:
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/check-prompt.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/block-dangerous.sh"
          }
        ]
      },
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/validate-file-path.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/auto-lint.sh"
          }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/log-failure.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/notify-done.sh"
          }
        ]
      }
    ]
  }
}
Hooks - Qoder