Skip to Content
用户指南功能特性钩子

Qwen Code hook

概述

Qwen Code hook 提供了一种强大的机制,用于扩展和自定义 Qwen Code 应用程序的行为。hook 允许用户在应用程序生命周期的特定节点(如工具执行前、工具执行后、会话开始/结束以及其他关键事件期间)执行自定义脚本或程序。

hook 默认处于启用状态。你可以通过在设置文件(与 hooks 同级)中将 disableAllHooks 设置为 true 来临时禁用所有 hook:

{ "disableAllHooks": true, "hooks": { "PreToolUse": [...] } }

这会禁用所有 hook,但不会删除其配置。

什么是 hook?

hook 是由用户定义的脚本或程序,Qwen Code 会在应用程序流程的预定义节点自动执行它们。它们允许用户:

  • 监控和审计工具使用情况
  • 强制执行安全策略
  • 向对话中注入额外的上下文
  • 根据事件自定义应用程序行为
  • 与外部系统和服务集成
  • 以编程方式修改工具输入或响应

hook 类型

Qwen Code 支持四种 hook 执行器类型:

类型描述
command执行 shell 命令。通过 stdin 接收 JSON,通过 stdout 返回结果。
http将 JSON 作为 POST 请求体发送到指定的 URL。通过 HTTP 响应体返回结果。
function直接调用已注册的 JavaScript 函数(仅限会话级 hook)。
prompt使用 LLM 评估 hook 输入并返回决策。

命令 hook

命令 hook 通过子进程执行命令。输入 JSON 通过 stdin 传递,输出通过 stdout 返回。

配置:

字段类型必填描述
type"command"是hook 类型
commandstring是要执行的命令
namestring否hook 名称(用于日志记录)
descriptionstring否hook 描述
timeoutnumber否超时时间(秒),默认 60
asyncboolean否是否在后台异步运行
envRecord<string, string>否环境变量
shell"bash" | "powershell"否要使用的 Shell
statusMessagestring否执行期间显示的状态消息

timeout 对于 command、HTTP 和 prompt hook 以秒为单位;SDK 注册的 function hook 保留毫秒。命令 hook 的超时曾经以毫秒为单位编写,因此对于命令 hook,1000 或更大的值仍然被读取为毫秒,现有设置继续正常工作。要迁移,请查找 timeout 为 1000 或更大的命令 hook,并将该值改写为秒,例如将 10000 改写为 10。要为命令 hook 设置 1000 秒或更长的超时,继续以毫秒为单位编写,例如 30 分钟写为 1800000。不是正数的命令 hook timeout(如 "30s")会被忽略,并应用 60 秒默认值。启用调试日志(QWEN_DEBUG_LOG_FILE=1)后,每个具有毫秒或被忽略的 timeout 的命令 hook 会在该会话的调试日志中被命名一次。

示例:

{ "hooks": { "PreToolUse": [ { "matcher": "write_file", "hooks": [ { "type": "command", "command": "$QWEN_PROJECT_DIR/.qwen/hooks/security-check.sh", "name": "security-check", "timeout": 10 } ] } ] } }

HTTP hook

HTTP hook 将 hook 输入作为 POST 请求发送到指定的 URL。它们支持 URL 白名单、DNS 级 SSRF 防护、环境变量插值等安全特性。

配置:

字段类型必填描述
type"http"是hook 类型
urlstring是目标 URL
headersRecord<string, string>否请求头(支持环境变量插值)
allowedEnvVarsstring[]否URL/请求头中允许使用的环境变量白名单
timeoutnumber否超时时间(秒),默认 600
namestring否hook 名称(用于日志记录)
statusMessagestring否执行期间显示的状态消息
onceboolean否每个会话中每个事件仅执行一次(仅限 HTTP hook)

安全特性:

  • URL 白名单:通过 allowedUrls 配置允许的 URL 模式
  • SSRF 防护:拦截私有 IP(10.x.x.x、172.16-31.x.x、192.168.x.x 等),但允许环回地址(127.0.0.1、::1)
  • DNS 验证:在请求前验证域名解析,以防止 DNS 重绑定攻击
  • 环境变量插值:使用 ${VAR} 语法,仅允许 allowedEnvVars 白名单中的变量

允许私有网络 hook(仅限托管环境)

默认情况下,HTTP hook 无法指向私有或链路本地 IP 范围。在平台托管环境中,如果 hook 接收方是第一方 VPC 内部端点(例如,解析到 172.16.0.0/12 的内部 API 网关),你可以通过以下配置放宽 IP 范围检查:

{ "security": { "allowPrivateNetworkHooks": true } }
  • 此设置仅从 User、System 和 SystemDefaults 设置作用域中生效。在 Workspace(项目)设置中设置的值将被忽略并记录为警告,因此克隆的仓库永远无法自行授予此绕过权限。
  • 该标志仅放宽通用私有/CGNAT/链路本地范围检查。云元数据端点在所有配置中保持阻止:BLOCKED_HOSTS 列表会逐字匹配(metadata.google.internal、metadata.azure.internal 等),元数据 IP 169.254.169.254 和 100.100.100.200 在所有序列化形式(包括 IPv4 映射的 IPv6,如 ::ffff:a9fe:a9fe)以及 DNS 解析后均被阻止。
  • security.allowedHttpHookUrls 白名单仍然独立适用。在托管环境中,请将此标志与白名单配合使用,以确保只有预期的内部端点可达。Workspace(项目)设置中的白名单仅在没有 User、System 或 SystemDefaults 作用域设置白名单时才会生效;否则它将被忽略并记录为警告,因此仓库可以缩小其 hook 发送数据的位置,但永远无法替换你配置的白名单(空白名单表示”允许所有”)。
  • HTTP hook 永远不会跟随重定向。3xx 响应被视为任何其他非 2xx 状态一样:非阻塞的 hook 失败,并且永远不会联系重定向目标。

警告: 启用此标志允许 hook 访问你网络上的内部基础设施。仅在受信任的托管环境中启用——绝不在你无法控制的仓库中启用。

示例:

{ "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "http", "url": "http://127.0.0.1:8080/hooks/pre-tool-use", "headers": { "Authorization": "Bearer ${HOOK_API_KEY}" }, "allowedEnvVars": ["HOOK_API_KEY"], "timeout": 10, "name": "remote-security-check" } ] } ] } }

示例:外部判断服务适配器

上面的 remote-security-check 配置假定 http://127.0.0.1:8080/hooks/pre-tool-use 已经运行着一个遵循此协议的服务(POST 接收 {tool_name, tool_input, ...},返回 hookSpecificOutput.permissionDecision)。下面是一个最小的、仅使用标准库的适配器,它补全了缺失的部分,连接到一个具体的判断后端,使整个示例可运行并可端到端测试,而不仅仅是一个桩代码。只有 review() 函数是后端特定的——将其函数体以及请求/响应形状替换为你使用的任何服务;其余部分(服务器、fail-open 处理、hook 响应形状)无论后端如何都保持不变。

声明:下面使用的后端 invinoveritas  是作者关联的服务——在此使用是因为它是可以端到端验证的服务,并非推荐。任何返回 JSON 裁决的 HTTP 服务都同样适用;只需更改 review() 即可。

数据处理:使用 matcher: "*" 时,每个工具调用的完整 tool_input 都会发送到判断后端——请将该输入视为敏感信息(它可能包含文件内容、路径或密钥)。如果你只需要判断 shell 命令,请缩小 matcher 范围(例如 run_shell_command)。

#!/usr/bin/env python3 # judgment_hook.py -- run: JUDGMENT_API_KEY=... python3 judgment_hook.py import json, os, sys, urllib.request from http.server import BaseHTTPRequestHandler, HTTPServer JUDGMENT_API_KEY = os.environ["JUDGMENT_API_KEY"] JUDGMENT_URL = os.environ.get("JUDGMENT_URL", "https://api.babyblueviper.com/review") def review(tool_name, tool_input): """POST the call to the judgment backend and return its verdict. This is the one function to change for a different backend -- request/response shape below matches invinoveritas's /review; adapt both to your own backend's contract if you swap it out.""" body = json.dumps({ "artifact": json.dumps({"tool_name": tool_name, "tool_input": tool_input}), "artifact_type": "shell_command" if tool_name in ("run_shell_command", "shell") else "general", "context": f"qwen-code PreToolUse: {tool_name}", }).encode() req = urllib.request.Request( JUDGMENT_URL, data=body, headers={"Authorization": f"Bearer {JUDGMENT_API_KEY}", "Content-Type": "application/json"}, ) # Keep this below the HTTP hook's own timeout (10s in the config above), so a "deny" # verdict is always returned before the hook gives up and fails open on its own. with urllib.request.urlopen(req, timeout=8) as resp: return json.loads(resp.read()) # response includes a "verdict" field: "reject" denies, anything else allows class Handler(BaseHTTPRequestHandler): def do_POST(self): payload = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}") tool_name, tool_input = payload.get("tool_name", "unknown"), payload.get("tool_input", {}) try: verdict = review(tool_name, tool_input) decision = "deny" if verdict.get("verdict") == "reject" else "allow" reason = verdict.get("summary", f"judgment verdict: {verdict.get('verdict')}") except Exception as e: decision, reason = "allow", "judgment backend unavailable, failing open" # never block on a review-side outage print(f"judgment backend unavailable for {tool_name}, failing open: {e}", file=sys.stderr) out = {"continue": True, "decision": decision, "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": decision, "permissionDecisionReason": reason, }} body = json.dumps(out).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *a): pass if __name__ == "__main__": HTTPServer(("127.0.0.1", 8080), Handler).serve_forever()

已针对上述真实生产 API 进行了端到端实时测试:真正具有破坏性的输入({"tool_name": "run_shell_command", "tool_input": {"command": "rm -rf /important_data"}})返回了 permissionDecision: "deny" 及真实解释;良性输入(ls -la)返回了 "allow"。在判断后端出现任何网络/超时/格式错误响应问题时均 fail-open,因此服务中断永远不会阻止合法的工具调用——与上面 command hook 示例使用各自退出码遵循的相同原则。

函数 hook

函数 hook 直接调用已注册的 JavaScript/TypeScript 函数。它们由 Skill 系统在内部使用,目前尚未作为公共 API 暴露给最终用户。

注意:对于大多数用例,请改用命令 hook 或 HTTP hook,它们可以在设置文件中进行配置。

Prompt hook

Prompt hook 使用 LLM 评估 hook 输入并返回决策。这对于基于上下文做出智能决策非常有用,例如决定是否允许或阻止某个操作。

数据处理: Prompt hook 将其事件输入发送到配置的模型提供商。当启用基于文件的调试日志时,完全展开的 prompt hook 请求也会写入会话调试日志。请将hook 输入和调试日志视为可能包含敏感信息。

工作原理:

  1. hook 输入 JSON 通过 $ARGUMENTS 占位符注入到你的 prompt 中
  2. prompt 被发送到 LLM(默认使用你当前的模型)
  3. LLM 返回包含决策结果的 JSON 响应
  4. Qwen Code 处理该决策,并相应地继续或阻止执行

配置:

字段类型必填描述
type"prompt"是hook 类型
promptstring是发送到 LLM 的 prompt。使用 $ARGUMENTS 获取 hook 输入
modelstring否要使用的模型(默认使用你当前的模型)
timeoutnumber否超时时间(秒),默认 30
namestring否hook 名称(用于日志记录)
descriptionstring否hook 描述
statusMessagestring否执行期间显示的状态消息

响应格式:

LLM 必须返回具有以下结构的 JSON:

{ "ok": true, "reason": "Explanation of the decision", "additionalContext": "Optional context to inject into the conversation" }
字段描述
oktrue 表示允许/继续,false 表示阻止/停止
reason当 ok 为 false 时必填。显示给模型以解释阻止原因
additionalContext可选。允许操作时注入到对话中的额外上下文

支持的事件:

Prompt hook 可用于大多数 hook 事件,包括:

  • PreToolUse - 评估是否允许工具调用
  • PostToolUse - 评估工具结果并可能注入上下文
  • Stop - 决定是继续还是停止
  • SubagentStop - 评估子代理结果
  • UserPromptSubmit - 评估或丰富符合条件的模型绑定 prompt

示例:Stop hook

{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "You are evaluating whether Qwen Code should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.", "timeout": 30 } ] } ] } }

当 ok 为 false 时,Qwen Code 将继续工作,并使用 reason 作为下一次响应的上下文。

示例:PreToolUse hook

{ "hooks": { "PreToolUse": [ { "matcher": "run_shell_command", "hooks": [ { "type": "prompt", "prompt": "Evaluate this tool call for security concerns. Tool input: $ARGUMENTS\n\nCheck for:\n- Dangerous commands (rm -rf, curl | sh, etc.)\n- Unauthorized access attempts\n- Data exfiltration patterns\n\nRespond with {\"ok\": true} if safe, or {\"ok\": false, \"reason\": \"concern\"} if blocked.", "model": "sonnet", "timeout": 30, "name": "security-evaluator" } ] } ] } }

hook 事件

hook 在 Qwen Code 会话的特定节点触发。不同的事件支持不同的 matcher 来过滤触发条件。

事件触发时机Matcher 目标
PreToolUse工具执行前工具 id(write_file、read_file、run_shell_command 等)
PostToolUse工具成功执行后工具 id
PostToolUseFailure工具执行失败后工具 id
UserPromptSubmit受支持的模型调用之前无
SessionStart会话开始或恢复时来源(startup、resume、clear、compact)
SessionEnd会话结束时原因(clear、logout、prompt_input_exit 等)
SessionDelete显式选择的会话被删除后无
MessageDisplay回复流式传输时反复触发无(始终触发)
Stop当 Claude 准备结束响应时无(始终触发)
SubagentStart子代理启动时代理类型(Bash、Explorer、Plan 等)
SubagentStop子代理停止时代理类型
PreCompact对话压缩前触发器(manual、auto)
Notification发送通知时类型(permission_prompt、idle_prompt、auth_success)
PermissionRequest显示权限对话框时工具 id
PermissionDenied当 AUTO 模式分类拒绝工具调用时工具 id
TodoCreated创建新的 todo 项时无(始终触发)
TodoCompletedtodo 项被标记为已完成时无(始终触发)

匹配器模式

matcher 是一个用于过滤触发条件的正则表达式。

事件类型事件匹配器支持匹配目标
工具事件PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied✅ 正则表达式工具 id:write_file、read_file、run_shell_command 等
子代理事件SubagentStart, SubagentStop✅ 正则表达式代理类型:Bash、Explorer 等
会话事件SessionStart✅ 正则表达式来源:startup、resume、clear、compact
会话事件SessionEnd✅ 正则表达式原因:clear、logout、prompt_input_exit 等
会话事件SessionDelete❌ 不支持N/A
通知事件Notification✅ 精确匹配类型:permission_prompt、idle_prompt、auth_success
Compact 事件PreCompact✅ 精确匹配触发方式:manual、auto
Todo 事件TodoCreated, TodoCompleted❌ 不支持N/A
提示词事件UserPromptSubmit❌ 不支持N/A
停止事件Stop❌ 不支持N/A
消息显示MessageDisplay❌ 不支持N/A

匹配器语法:

  • 空字符串 "" 或 "*" 匹配该类型的所有事件
  • 支持标准正则表达式语法(例如,^run_shell_command$、read_.*、(write_file|edit))
  • 工具钩子在 tool_name 中接收运行时工具 id(例如 write_file)。内置显示名称如 WriteFile 和 ReadFile 也作为 matcher 别名被接受以兼容,但新配置应优先使用运行时 id。

示例:

{ "hooks": { "PreToolUse": [ { "matcher": "^run_shell_command$", "hooks": [ { "type": "command", "command": "echo 'bash check' >> /tmp/hooks.log" } ] }, { "matcher": "write_.*", "hooks": [ { "type": "command", "command": "echo 'write check' >> /tmp/hooks.log" } ] }, { "matcher": "*", "hooks": [ { "type": "command", "command": "echo 'all tools' >> /tmp/hooks.log" } ] } ], "SubagentStart": [ { "matcher": "^(Bash|Explorer)$", "hooks": [ { "type": "command", "command": "echo 'subagent check' >> /tmp/hooks.log" } ] } ] } }

输入/输出规则

Hook 输入结构

所有 hook 执行器接收标准化的事件输入。传递边界取决于执行器类型:

Hook 类型输入接收方
command子进程,通过 stdin 上的 JSON
http配置的端点,通过 JSON POST 请求体
function受信任的进程内回调
prompt配置的模型提供商,输入替换 $ARGUMENTS 后发送

函数钩子是在 Qwen 进程中运行的受信任代码。它们接收进程内对象,因此不得将字段视为对函数钩子不可变。

Qwen 不控制钩子进程、端点、回调或模型提供商是否保留或转发其输入。请审查每个配置的执行器的数据处理策略。

通用字段:

{ "session_id": "string", "transcript_path": "string", "cwd": "string", "hook_event_name": "string", "timestamp": "string" }

根据 Hook 类型添加特定于事件的字段。在子代理中运行时,还会额外包含 agent_id 和 agent_type。

Hook 输入是一个向前可扩展的 JSON 契约:可以向现有事件添加新的可选字段。消费者应忽略未知字段。拒绝未知属性的严格解码器在升级 Qwen Code 之前,必须显式允许每个新的可选字段。对于安全敏感的钩子,解码器失败可能改变 fail-open 或 fail-closed 行为,因此管理员必须在推出前根据已部署的钩子验证升级后的有效载荷。

Hook 输出结构

Hook 输出通过 stdout(command)或 HTTP 响应体(http)以 JSON 格式返回。

退出码行为(Command Hooks):

退出码行为
0成功。解析 stdout 中的 JSON 以控制行为。
2阻塞错误。忽略 stdout,将 stderr 作为错误反馈传递给模型。
其他非阻塞错误。stderr 仅在调试模式下显示,继续执行。

输出结构:

Hook 输出支持三类字段:

  1. 通用字段:continue、stopReason、suppressOutput、systemMessage
  2. 顶层决策:decision、reason(部分事件使用)
  3. 事件特定控制:hookSpecificOutput(必须包含 hookEventName)
{ "continue": true, "decision": "allow", "reason": "Operation approved", "hookSpecificOutput": { "hookEventName": "PreToolUse", "additionalContext": "Additional context information" } }

各 Hook 事件详情

PreToolUse

用途:在使用工具之前执行,用于进行权限检查、输入验证或上下文注入。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "tool_name": "name of the tool being executed", "tool_input": "object containing the tool's input parameters", "tool_use_id": "unique identifier for this tool use instance (internal format, e.g., toolu_xxx)", "tool_call_id": "original API call ID from the LLM provider (e.g., call_xxx for OpenAI/Qwen) (optional)" }

输出选项:

  • hookSpecificOutput.permissionDecision:“allow”、“deny” 或 “ask”(必填)
  • hookSpecificOutput.permissionDecisionReason:决策原因(必填)
  • hookSpecificOutput.updatedInput:修改后的工具输入参数,用于替代原始参数
  • hookSpecificOutput.additionalContext:额外的上下文信息

permissionDecision 的值控制工具是否运行:

  • "allow" — 运行工具,无需通常的批准提示。
  • "deny" — 阻止工具;工具不会执行,并向模型返回错误。
  • "ask" — 暂停并在 TUI 中要求用户确认工具调用,然后再运行。确认则运行一次工具;拒绝则取消。在无法提示确认的上下文中(如无头 --prompt 运行和后台子代理),"ask" 会回退到 "deny"。

对于 "ask",TUI 将 permissionDecisionReason 作为纯文本显示,而不是解析内联 Markdown。这使格式标记和链接目标对用户保持可见。

注意:虽然底层类在技术上支持 decision 和 reason 等标准 Hook 输出字段,但官方接口期望使用包含 permissionDecision 和 permissionDecisionReason 的 hookSpecificOutput。

输出示例:

{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Security policy blocks database writes", "additionalContext": "Current environment: production. Proceed with caution." } }

PostToolUse

用途:在工具成功完成后执行,用于处理结果、记录结果或注入额外的上下文。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "tool_name": "name of the tool that was executed", "tool_input": "object containing the tool's input parameters", "tool_response": "object containing the tool's response", "tool_use_id": "unique identifier for this tool use instance (internal format, e.g., toolu_xxx)", "tool_call_id": "original API call ID from the LLM provider (e.g., call_xxx for OpenAI/Qwen) (optional)", "duration_ms": "tool execution time in milliseconds, excluding approval (optional)" }

输出选项:

  • decision:“allow”、“deny”、“block”(如未指定则默认为 “allow”)
  • reason:决策原因
  • hookSpecificOutput.additionalContext:要包含的额外信息

输出示例:

{ "decision": "allow", "reason": "Tool executed successfully", "hookSpecificOutput": { "additionalContext": "File modification recorded in audit log" } }

PostToolUseFailure

用途:在工具执行失败时执行,用于处理错误、发送警报或记录失败。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "tool_use_id": "unique identifier for the tool use (internal format, e.g., toolu_xxx)", "tool_call_id": "original API call ID from the LLM provider (e.g., call_xxx for OpenAI/Qwen) (optional)", "tool_name": "name of the tool that failed", "tool_input": "object containing the tool's input parameters", "error": "error message describing the failure", "is_interrupt": "boolean indicating if failure was due to user interruption (optional)", "duration_ms": "tool execution time in milliseconds when execution had started (optional)" }

输出选项:

  • hookSpecificOutput.additionalContext:错误处理信息
  • 标准 Hook 输出字段

输出示例:

{ "hookSpecificOutput": { "additionalContext": "Error: File not found. Failure logged in monitoring system." } }

UserPromptSubmit

用途:在受支持的模型调用之前执行,用于验证、阻止或丰富其输入。在核心/无头路径上,该事件目前覆盖 UserQuery、ToolResult 和 Hook 发送,而 Retry、Steer、Cron、Notification 和 Teammate 发送会被跳过。因此它可能在续接路径上发生,prompt 不应被假定为原始用户输入。ACP 会话路径有自己的调用策略:重试和新分派的后台任务仍可调用旧式 hook;continue、已恢复问题和运行时目标轮次则不会。

事件特定字段:

{ "prompt": "legacy prompt for this invocation; semantics depend on the execution path", "submitted_prompt": "optional user text captured at a supported submission boundary" }

submitted_prompt 是可选的。它存在于受支持的交互式 TUI 提交和首轮无头 UserQuery 发送中。在 ACP 客户端、serve 和守护进程主机使用的 ACP 会话路径上,新轮次必须携带显式提交声明。缺失、非字符串、空或仅空白的声明会省略该字段;该值从不从 prompt 或显示标签重建。重试、续接和 channel 分类的轮次会省略它。channel 排除包括自动化事件和通过 channel 适配器中继的人类消息。

Web Shell 在其提交边界提供原始编辑器文本。实时语音交接不声明来源,因为其请求文本来自模型生成的工具参数。其他 ACP/守护进程 SDK 客户端可以通过 _meta: { "qwen.submittedPrompt": "original submitted text" } 按请求选择加入,在资源或模型专属扩展之前捕获。没有此声明的现有客户端继续运行旧式 hook,但不触发来源门控的 Auto Recall。不要将声明全局添加到 SDK 传输:计划任务、Live 任务运行、子会话生成、模型编写的跨会话消息和提升的轮次中间消息不得自动获取它。私有的 qwen.daemon.submittedPrompt 键保留用于守护进程到子进程的跳转,并从外部调用方剥离。这些声明是调用方提供的来源,而非人类作者身份或授权的证明。

在 ACP 路径上,初始旧式 prompt 是在资源、附件、斜杠命令或模型专属扩展之前用空格连接的请求文本块。它不暴露完整的展开后模型输入。submitted_prompt 可以等于该文本,但仅来自显式声明并保留其原始空白。在核心/无头路径上,旧式 prompt 表示 hook 调用的当前模型绑定文本。这两个字段都不是完整的 DLP 检查面。

以下编辑器规则适用于交互式 TUI,不适用于 ACP 客户端。延迟输入在其来源保持完整时可以保留该字段。组合批次仅在每个组成项都有来源时才保留来源;编辑的、部分已知或以其他方式不明确的输入会省略该字段。Prompt、命令和 shell 历史导航或选定的搜索匹配、跨重启的暂存恢复以及对话倒带恢复也会省略它,因为这些路径可能在没有原始来源的情况下显示模型绑定文本。需要用户提交文本的消费者应将缺失视为不可用,而不是回退到 prompt。

在恢复或来源不可用的模型绑定输入被清除或提交后,编辑器也会清除其撤销和重做历史。这可以防止撤销在标记或附带内容被消费后恢复展开的文本。

大段粘贴的占位符在 submitted_prompt 中保持紧凑;展开的粘贴内容仅出现在 prompt 中。在该 TUI 路径上,消费者应将该字段视为文本投影而不是剪贴板输入的逐字节记录。ACP 客户端没有等效的内置 Vim、粘贴占位符、历史或倒带来源跟踪;它们决定恢复或编辑的文本是否保留有效的提交声明。

在启用 Vim 模式时存在的任何非空输入都会省略 submitted_prompt,包括在禁用 Vim 之后,因为 Vim 寄存器在当前版本中不携带来源信息。此保守规则也涵盖了在启用 Vim 之前输入的草稿。清除编辑器会开始一个新的符合条件的输入。

此字段是来源信息,而非身份验证、租户身份、授权或 DLP。它是调用方提供的数据。为此事件配置的每个执行器都会接收到它;特别是,HTTP 钩子会将其发送到其端点,prompt 钩子会将其发送到其模型提供商。

当两个字段都存在时,prompt 钩子的有效载荷包含重叠的文本,可能会消耗额外的模型输入 token。当前版本中没有按钩子的字段抑制机制。

连续的 UserPromptSubmit 钩子可以将 additionalContext 追加到 prompt;submitted_prompt 继续表示捕获的提交。函数钩子是受信任的同进程代码,不受不可变性保证的约束。

当最终钩子输出包含非空的 additionalContext 时,Qwen 首先对该值进行清理,然后将其作为独立的文本部分发送给模型:

<qwen:user-prompt-submit-context> sanitized hook context </qwen:user-prompt-submit-context>

该标签告诉模型和会话记录消费者,该部分来自配置的钩子而非用户 prompt。它是一个来源标记,而非身份验证、授权或通用信任边界。

对于带有此附加上下文的 UserQuery,会话 JSONL 记录保留模型绑定的各部分(包括带标签的部分),并添加以下 systemPayload:

{ "displayText": "pre-hook display projection", "hookContext": "sanitized hook context" }

此双字段载荷仅针对此类用户 prompt 记录写入。hookContext 故意重复带标签的部分,以便离线和第三方消费者无需解析模型文本即可识别其来源。displayText 是钩子前的显示投影,绝不包含钩子上下文。在核心/无头路径上,它是可用时的提交投影,否则是展开后的钩子前 prompt。ACP 记录可信的显示投影或展开前的原始请求文本(当投影或附件引用需要载荷时);否则它记录用户消息而不带 systemPayload 或 displayText。

当 systemPayload.hookContext 为字符串时,会话记录显示消费者将 displayText 视为此用户 prompt 投影。为了与已发布的仅含 displayText 的用户 prompt 记录兼容,在至少一个其他部分之后的最后一个部分中包含完整的带标签上下文可作为等效的配对证据。通知、cron 和轮次中间记录也可能具有 displayText,但这些值是紧凑的显示标签,在没有该证据的情况下不得替换其模型绑定文本。 传统的裸上下文记录保留其模型绑定的显示行为,因为无法可靠地分离上下文。对于使用当前带标签形状的无元数据记录,兼容性消费者可以移除相同的完整最终带标签部分;他们不得推断任意类似标签的用户文本是钩子来源。

敏感 prompt 遥测属性(启用时)和托管自动记忆召回均使用钩子前的 prompt。它们不包含 UserPromptSubmit 添加的上下文。

输出选项:

  • decision:“allow”、“deny”、“block” 或 “ask”
  • reason:决策的人类可读解释
  • hookSpecificOutput.additionalContext:要追加到提示词的额外上下文(可选)

发送到模型时,注入的 additionalContext 会作为独立的消息部分追加,并包裹在保留的 <qwen:user-prompt-submit-context>...</qwen:user-prompt-submit-context> 标签中,因此它在模型历史和会话记录中与用户编写的文本保持可区分。钩子输出中的尖括号在包裹前会被转义,因此钩子内容无法关闭或伪造该标签。会话记录还会单独记录用户的原始提示文本;交互式 TUI 和 ACP/导出记录回放路径显示的是原始文本而非注入的上下文。

注意:由于 UserPromptSubmitOutput 继承了 HookOutput,因此所有标准字段都可用,但只有 hookSpecificOutput 中的 additionalContext 是专门为此事件定义的。

输出示例:

{ "decision": "allow", "reason": "Prompt reviewed and approved", "hookSpecificOutput": { "additionalContext": "Remember to follow company coding standards." } }

SessionStart

用途:在新会话开始时执行,用于执行初始化任务。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "source": "startup | resume | clear | compact", "model": "the model being used", "agent_type": "the type of agent if applicable (optional)" }

输出选项:

  • hookSpecificOutput.additionalContext:在会话中可用的上下文
  • 标准 Hook 输出字段

输出示例:

{ "hookSpecificOutput": { "additionalContext": "Session started with security policies enabled." } }

SessionEnd

用途:在会话结束时执行,用于执行清理任务。

事件特定字段:

{ "reason": "clear | logout | prompt_input_exit | bypass_permissions_disabled | other" }

输出选项:

  • 标准 Hook 输出字段(通常不用于阻塞)

SessionDelete

用途:在显式选择的会话被永久删除后运行。此事件是 fire-and-forget:输出和失败无法撤销删除操作。

事件特定字段:

{ "deleted_session_id": "the session that was deleted" }

该钩子使用删除运行时的常规会话字段(session_id、transcript_path 和 cwd);通过 ACP 时,transcript_path 为空,因为删除运行时没有自己的记录。SessionDelete 当前在交互式 /delete 流程和 ACP 的显式 deleteSession 方法中触发;守护进程 REST 批量删除和内部清理不会发出此事件。如果 Qwen 在分发后退出,命令钩子会被允许继续完成;其 stdout 和 stderr 会被忽略,并且独立于 Qwen 的管道。

MessageDisplay

用途:在助手回复流式传输时反复触发——在 Stop(在回合结束时触发一次)之前触发。适用于实时叙述、增量日志记录或任何希望在回复写入过程中(而非完成后)做出反应的消费者。这是一个 fire-and-forget 事件——钩子输出和退出码会被忽略。

事件特定字段:

{ "message_id": "stable id for the whole streamed message", "displayed_text": "the CUMULATIVE text streamed so far for this message (not a delta)", "is_final": "true on the last firing for this message, false otherwise" }

displayed_text 是累积的而非增量,因此钩子脚本无需自行重新组装片段——每次触发都携带到目前为止的完整文本。触发采用去抖动(至少每 ~200ms 一次),但最终触发(is_final: true)除外,它会在消息结束时始终触发一次,因此回复的尾部不会因等待去抖动窗口而丢失。

传递语义——钩子脚本可以依赖的行为:

  • 慢速钩子会看到更少、更新的载荷。 每条消息最多只有一个中间流式钩子执行在进行中;当一个运行时,新的去抖动载荷会_替换_队列中的载荷,而不是在其后面堆积。因此,比去抖动窗口更慢的钩子会跳过中间快照——这是无损的,因为每个载荷都携带完整的累积文本。
  • is_final 永远不会排在过时传递之后。 最终载荷会在消息结束的瞬间被分发——如果仍有中间执行在运行,则与其并行分发(这是唯一一次违反一对一规则的例外,理由相同:最终累积文本严格取代该执行正在处理的内容)。你的钩子总是会收到 is_final 载荷,并且在 Stop 钩子触发之前收到它。对有状态钩子的一个影响:当最终执行与被取代的中间执行重叠时,它们的_完成_顺序是不确定的——过时的执行可能在最终执行之后完成(甚至在 Stop 之后)。将 is_final 视为每个 message_id 的终止信号,让累积文本获胜,而不是假设最后完成的执行携带最新状态。
  • 回合会等待 is_final 传递完成——但不会无限等待。 回合的结束(以及 Stop 钩子,如果它触发的话)最多等待 5 秒让最终传递完成。在此预算内完成的钩子保持最强保证:无头运行(qwen -p ...)仅在钩子完成后退出,并且 is_final 执行在 Stop 开始之前完成。较慢的钩子仍然会先收到 is_final——只是等待其完成的时间有限:在终端 UI 或 ACP 会话中,执行会在后台继续完成,而无头运行会在不等待的情况下退出。钩子进程不会在退出时被终止;它会被允许自行完成,因此使用 qwen -p … && next-step 链接的脚本可能会观察到 next-step 在慢速钩子仍在运行时启动。达到此超时时会在 stderr 上打印警告。
  • 取消行为取决于时序。 在 is_final 分发_之前_取消的回合不会触发 is_final——消息被视为已放弃,缓冲到 is_final 的消费者应将取消静默视为其刷新/丢弃信号(例如超时回退)。判断标准是回合结束时中止信号的状态,而不是是否所有片段都已流式传输——在短暂间隙内到达的中止仍然可以抑制实际上已完成接收的消息的 is_final。在 is_final 已分发_之后_(在排空等待期间)取消则不同:仍在运行的钩子执行可能会被中途终止(SIGTERM),但载荷本身已经被传递。
  • displayed_text 在 is_final 之前是临时的。 它反映到目前为止已流式传输的内容;将中间载荷视为显示状态,而非权威的最终内容。
  • 使用工具的回合会产生多条消息。 每次模型调用都有自己的 message_id 和自己的 is_final: true 触发:工具调用前的文本是一条消息,工具结果后的续接是另一条。不产生显示文本的模型调用(仅工具调用)不会触发任何事件。

注意:在终端 UI、无头模式(-p)和 ACP(IDE/编辑器/qwen serve)会话中触发,每个界面使用相同的载荷契约。

Stop

用途:在 Qwen 结束其响应之前执行,用于提供最终反馈或总结。

事件特定字段:

{ "stop_hook_active": "true when this turn is continuing because a stop hook blocked the previous stop check (still true after tool calls made during that continuation); false on the first check and again once the stop is allowed, the blocking cap is reached, the user steers or sends new input, or a new turn, retry or goal turn starts", "last_assistant_message": "the last message from the assistant", "context_usage": "ratio of context window used (may exceed 1 when tokens exceed window; optional)", "context_limit": "context window size in tokens (optional)", "input_tokens": "prompt token count (may include output tokens depending on provider; optional)" }

context_usage、context_limit 和 input_tokens 字段允许 Hook 脚本观察上下文使用情况并实现自定义的 compact 策略——例如,当使用量超过自定义阈值时,打印运行 /compact 的提醒脚本。

输出选项:

  • decision:“allow”、“deny”、“block” 或 “ask”
  • reason:决策的人类可读解释
  • stopReason:包含在停止响应中的反馈
  • continue:设置为 false 以停止执行
  • hookSpecificOutput.additionalContext:额外的上下文信息

注意:由于 StopOutput 继承了 HookOutput,因此所有标准字段都可用,但 stopReason 字段与此事件特别相关。

输出示例:

{ "decision": "block", "reason": "Must be provided when Qwen Code is blocked from stopping" }

StopFailure

用途:当回合因 API 错误或循环检测而结束(而不是 Stop)时执行。这是一个 fire-and-forget 事件——Hook 输出和退出码会被忽略。

事件特定字段:

{ "error": "rate_limit | authentication_failed | billing_error | invalid_request | server_error | max_output_tokens | loop_detected | unknown", "error_details": "detailed error message (optional)", "last_assistant_message": "the last message from the assistant before the error (optional)" }

Matcher:匹配 error 字段。例如,"matcher": "rate_limit" 仅在触发速率限制错误时执行。

输出选项:

  • None - StopFailure 采用 fire-and-forget 模式。所有 hook 输出和退出码均被忽略。

退出码处理:

退出码行为
任意已忽略(fire-and-forget)

配置示例:

{ "hooks": { "StopFailure": [ { "matcher": "rate_limit", "hooks": [ { "type": "command", "command": "/path/to/rate-limit-alert.sh", "name": "rate-limit-alerter" } ] } ] } }

使用场景:

  • 速率限制监控与告警
  • 身份验证失败日志记录
  • 计费错误通知
  • 错误统计收集

如果 Qwen 在分发后退出,命令钩子会被允许继续完成;其 stdout 和 stderr 会被忽略,并且独立于 Qwen 的管道。

SubagentStart

用途:在启动子代理(如 Task 工具)时执行,用于设置上下文或权限。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "agent_id": "identifier for the subagent", "agent_type": "type of agent (Bash, Explorer, Plan, Custom, etc.)" }

输出选项:

  • hookSpecificOutput.additionalContext:子代理的初始上下文
  • 标准 hook 输出字段

输出示例:

{ "hookSpecificOutput": { "additionalContext": "Subagent initialized with restricted permissions." } }

SubagentStop

用途:在子代理完成时执行,用于执行收尾任务。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "stop_hook_active": "false on the first stop check; true when the subagent is continuing because a SubagentStop hook blocked its previous stop", "agent_id": "identifier for the subagent", "agent_type": "type of agent", "agent_transcript_path": "path to the subagent's transcript", "last_assistant_message": "the last message from the subagent" }

输出选项:

  • decision:“allow”、“deny”、“block” 或 “ask”
  • reason:决策的人类可读解释

输出示例:

{ "decision": "block", "reason": "Must be provided when Qwen Code is blocked from stopping" }

PreCompact

用途:在对话压缩(compaction)之前执行,用于准备或记录压缩操作。

事件特定字段:

{ "trigger": "manual | auto", "custom_instructions": "custom instructions currently set" }

输出选项:

  • hookSpecificOutput.additionalContext:压缩前要包含的上下文
  • 标准 hook 输出字段

输出示例:

{ "hookSpecificOutput": { "additionalContext": "Compacting conversation to maintain optimal context window." } }

PostCompact

用途:在对话压缩完成后执行,用于归档摘要或跟踪使用情况。

事件特定字段:

{ "trigger": "manual | auto", "compact_summary": "the summary generated by the compaction process" }

Matcher:匹配 trigger 字段。例如,"matcher": "manual" 仅在通过 /compact 命令进行手动压缩时触发。

输出选项:

  • hookSpecificOutput.additionalContext:附加上下文(仅用于日志记录)
  • 标准 hook 输出字段(仅用于日志记录)

注意:PostCompact 不在官方决策模式支持的事件列表中。decision 字段和其他控制字段不会产生任何控制效果——它们仅用于日志记录目的。

退出码处理:

退出码行为
0成功 - 在详细模式下向用户显示 stdout
其他非阻塞错误 - 在详细模式下向用户显示 stderr

配置示例:

{ "hooks": { "PostCompact": [ { "matcher": "manual", "hooks": [ { "type": "command", "command": "/path/to/save-compact-summary.sh", "name": "save-summary" } ] } ] } }

使用场景:

  • 将摘要归档到文件或数据库
  • 使用统计跟踪
  • 上下文变更监控
  • 压缩操作的审计日志

Notification

用途:在发送通知时执行,用于自定义或拦截通知。

事件特定字段:

{ "message": "notification message content", "title": "notification title (optional)", "notification_type": "permission_prompt | idle_prompt | auth_success" }

注意:elicitation_dialog 类型已定义但尚未实现。

输出选项:

  • hookSpecificOutput.additionalContext:要包含的附加信息
  • 标准 hook 输出字段

输出示例:

{ "hookSpecificOutput": { "additionalContext": "Notification processed by monitoring system." } }

PermissionRequest

用途:在显示权限对话框时执行,用于自动化决策或更新权限。

事件特定字段:

{ "permission_mode": "default | plan | auto_edit | yolo", "tool_name": "name of the tool requesting permission", "tool_input": "object containing the tool's input parameters", "permission_suggestions": "array of suggested permissions (optional)" }

输出选项:

  • hookSpecificOutput.decision:包含权限决策详情的结构化对象:
    • behavior:“allow” 或 “deny”
    • updatedInput:修改后的工具输入(可选)
    • updatedPermissions:修改后的权限(可选)
    • message:显示给用户的消息(可选)
    • interrupt:是否中断工作流(可选)

输出示例:

{ "hookSpecificOutput": { "decision": { "behavior": "allow", "message": "Permission granted based on security policy", "interrupt": false } } }

TodoCreated

用途:在通过 todo_write 工具创建新的 todo 项时执行。允许对 todo 创建进行验证、记录日志或阻止。

Todo hook 分两个阶段运行:

  • validation:在持久化之前运行。仅在此阶段进行验证;返回 block 或 deny 可阻止写入。
  • postWrite:在持久化之后运行。用于日志记录或同步等副作用;此阶段会忽略 block 或 deny。

事件特定字段:

{ "todo_id": "unique identifier for the todo item", "todo_content": "content/description of the todo item", "todo_status": "pending | in_progress | completed", "all_todos": "array of all todo items in the current list", "phase": "validation | postWrite" }

输出选项:

  • decision:“allow”、“block” 或 “deny”
  • reason:决策的人类可读解释(阻止时必填)

阻止行为:

在 validation 阶段,当 decision 为 block 或 deny(退出码 2)时,会阻止创建 todo。todo 列表保持不变,并将原因作为反馈提供给模型。

在 postWrite 阶段,todo 已被持久化。Hook 仍可返回输出,但 block / deny 不会撤销写入,也不应用于验证。

输出示例(允许):

{ "decision": "allow", "reason": "Todo content validated successfully" }

输出示例(阻止):

{ "decision": "block", "reason": "Todo content too short. Minimum 5 characters required." }

Hook 脚本示例:

#!/bin/bash # ~/.qwen/hooks/todo-validator.sh # Validates todo content before creation INPUT=$(cat) CONTENT=$(echo "$INPUT" | jq -r '.todo_content') # Check minimum length if [ ${#CONTENT} -lt 5 ]; then echo '{"decision": "block", "reason": "Todo content must be at least 5 characters"}' exit 2 fi # Block test-related todos if [[ "$CONTENT" =~ "test" ]]; then echo '{"decision": "block", "reason": "Test todos are not allowed in production"}' exit 2 fi echo '{"decision": "allow"}' exit 0

配置示例:

{ "hooks": { "TodoCreated": [ { "hooks": [ { "type": "command", "command": "$HOME/.qwen/hooks/todo-validator.sh", "name": "todo-validator", "timeout": 5000 } ] } ] } }

TodoCompleted

用途:在将 todo 项标记为已完成时执行。允许对 todo 完成进行验证、记录日志或阻止。

Todo hook 分两个阶段运行:

  • validation:在持久化之前运行。仅在此阶段进行验证;返回 block 或 deny 可阻止写入。
  • postWrite:在持久化之后运行。用于日志记录或同步等副作用;此阶段会忽略 block 或 deny。

事件特定字段:

{ "todo_id": "unique identifier for the todo item", "todo_content": "content/description of the todo item", "previous_status": "pending | in_progress (status before completion)", "all_todos": "array of all todo items in the current list", "phase": "validation | postWrite" }

输出选项:

  • decision:“allow”、“block” 或 “deny”
  • reason:决策的人类可读解释(阻止时必填)

阻止行为:

在 validation 阶段,当 decision 为 block 或 deny(退出码 2)时,会阻止完成 todo。todo 项保持其先前状态,并将原因作为反馈提供给模型。

在 postWrite 阶段,todo 已被持久化。Hook 仍可返回输出,但 block / deny 不会撤销写入,也不应用于验证。

输出示例(允许):

{ "decision": "allow", "reason": "Todo completion approved" }

输出示例(阻止):

{ "decision": "block", "reason": "Cannot complete this todo until dependent tasks are finished." }

Hook 脚本示例:

#!/bin/bash # ~/.qwen/hooks/todo-completion-validator.sh # Validates todo completion conditions INPUT=$(cat) TODO_ID=$(echo "$INPUT" | jq -r '.todo_id') ALL_TODOS=$(echo "$INPUT" | jq -r '.all_todos') # Check if there are incomplete dependent todos (example logic) INCOMPLETE_COUNT=$(echo "$ALL_TODOS" | jq '[.[] | select(.status != "completed")] | length') if [ "$INCOMPLETE_COUNT" -gt 5 ]; then echo '{"decision": "block", "reason": "Too many incomplete todos. Complete other tasks first."}' exit 2 fi echo '{"decision": "allow"}' exit 0

配置示例:

{ "hooks": { "TodoCompleted": [ { "hooks": [ { "type": "command", "command": "$HOME/.qwen/hooks/todo-completion-validator.sh", "name": "completion-validator", "timeout": 5 } ] } ] } }

使用场景:

  • 日志记录:跟踪 todo 的创建和完成情况以进行审计或分析
  • 验证:强制执行内容质量标准(最小长度、必需关键字)
  • 工作流控制:在满足先决条件之前阻止完成
  • 集成:将 todo 与外部任务管理系统(Jira、Trello 等)同步

Hook 配置

Hook 在 Qwen Code 设置中进行配置,通常位于 .qwen/settings.json 或用户配置文件中:

{ "hooks": { "PreToolUse": [ { "matcher": "^run_shell_command$", "sequential": false, "hooks": [ { "type": "command", "command": "/path/to/security-check.sh", "name": "security-check", "description": "Run security checks before tool execution", "timeout": 30 } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "echo 'Session started'", "name": "session-init" } ] } ] } }

Hook 执行

并行与顺序执行

  • 默认情况下,hook 并行执行以提升性能
  • 在 hook 定义中使用 sequential: true 以强制执行顺序执行
  • 顺序执行的 hook 可以修改链路中后续 hook 的输入

异步 Hook

只有 command 类型支持异步执行。设置 "async": true 会在后台运行 hook,不会阻塞主流程。

异步钩子的作用域限定在 Qwen 进程内,因为其捕获的输出通过内存中的异步钩子注册表传递。在 POSIX 系统上,Qwen 在退出时会回收仍在运行的异步钩子进程树,但其部分明确保证退出后 fire-and-forget 完成的事件类型除外。Windows 无法在其根进程退出后重建后代树,因此那里的完整父退出回收需要 Job Object 或后代跟踪。

特性:

  • 无法返回决策控制(操作已发生)
  • 结果会在下一轮对话中通过 systemMessage 或 additionalContext 注入,但上述文档中忽略输出的 fire-and-forget 事件类型除外
  • 适用于审计、日志记录、后台测试等场景
  • 占用 10 个并发异步 hook 槽位之一,直到完成或达到 timeout(默认 60 秒)

示例:

{ "hooks": { "PostToolUse": [ { "matcher": "write_file|edit", "hooks": [ { "type": "command", "command": "$QWEN_PROJECT_DIR/.qwen/hooks/run-tests-async.sh", "async": true, "timeout": 300 } ] } ] } }
#!/bin/bash INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then exit 0; fi RESULT=$(npm test 2>&1) if [ $? -eq 0 ]; then echo "{\"systemMessage\": \"Tests passed after editing $FILE_PATH\"}" else echo "{\"systemMessage\": \"Tests failed: $RESULT\"}" fi

安全模型

  • Hook 在用户环境中以用户权限运行
  • 项目级 hook 需要受信任的文件夹状态
  • 超时机制可防止 hook 挂起(默认:command hook 为 60 秒)

最佳实践

示例 1:安全验证 Hook

一个 PreToolUse hook,用于记录日志并可能拦截危险命令:

security_check.sh

#!/bin/bash # Read input from stdin INPUT=$(cat) # Parse the input to extract tool info TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input') # Check for potentially dangerous operations if echo "$TOOL_INPUT" | grep -qiE "(rm.*-rf|mv.*\/|chmod.*777)"; then echo '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Security policy blocks dangerous command" } }' exit 2 # Blocking error fi # Log the operation echo "INFO: Tool $TOOL_NAME executed safely at $(date)" >> /var/log/qwen-security.log # Allow with additional context echo '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Security check passed", "additionalContext": "Command approved by security policy" } }' exit 0

在 .qwen/settings.json 中配置:

{ "hooks": { "PreToolUse": [ { "hooks": [ { "type": "command", "command": "${SECURITY_CHECK_SCRIPT}", "name": "security-checker", "description": "Security validation for bash commands", "timeout": 10 } ] } ] } }

示例 2:HTTP 审计 Hook

一个 PostToolUse HTTP hook,用于将所有工具执行记录发送至远程审计服务:

{ "hooks": { "PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "http", "url": "https://audit.example.com/api/tool-execution", "headers": { "Authorization": "Bearer ${AUDIT_API_TOKEN}", "Content-Type": "application/json" }, "allowedEnvVars": ["AUDIT_API_TOKEN"], "timeout": 10, "name": "audit-logger" } ] } ] } }

示例 3:提交的 Prompt 验证 Hook

在核心/无头路径上,prompt 可能包含生成或展开的内容,而不是原始用户输入。在 ACP 上,它以展开前的请求文本开头,因此读取它不会检查附件正文或完整的模型输入。UserPromptSubmit 并不覆盖每次模型发送。当需要来源溯源时,不要从 submitted_prompt 静默回退到 prompt。

一个 UserPromptSubmit hook,用于验证受支持的已提交文本并为过长的提示词提供上下文。它还会在无头提交以及显式声明的 ACP/守护进程提交上运行;并非仅限于 TUI。它会跳过来源溯源不可用的调用。阻塞结果会停止受影响的调用,包括这些非 TUI 路径。关键字检查仅为示例,并非完整的 DLP 策略:

prompt_validator.py

import json import sys import re # Load input from stdin try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON input: {e}", file=sys.stderr) sys.exit(1) user_prompt = input_data.get("submitted_prompt") if user_prompt is None: # Do not mistake model-bound or machine-generated content for raw input. sys.exit(0) # Sensitive words list sensitive_words = ["password", "secret", "token", "api_key"] # Check for sensitive information for word in sensitive_words: if re.search(rf"\b{word}\b", user_prompt.lower()): # Block prompts containing sensitive information output = { "decision": "block", "reason": f"Prompt contains sensitive information '{word}'. Please remove sensitive content and resubmit.", "hookSpecificOutput": { "hookEventName": "UserPromptSubmit" } } print(json.dumps(output)) sys.exit(0) # Check prompt length and add warning context if too long if len(user_prompt) > 1000: output = { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "Note: User submitted a long prompt. Please read carefully and ensure all requirements are understood." } } print(json.dumps(output)) sys.exit(0) # No processing needed for normal cases sys.exit(0)

故障排除

  • 检查应用日志以获取 hook 执行详情
  • 验证 hook 脚本的权限和可执行性
  • 确保 hook 输出中的 JSON 格式正确
  • 使用特定的 matcher 模式以避免意外的 hook 执行
  • 使用 --debug 模式查看详细的 hook 匹配和执行信息。Prompt 钩子的输入可能会被写入会话调试日志,因此请应用适当的访问和保留控制。
  • 临时禁用所有 hook:在设置中添加 "disableAllHooks": true
Last updated on