dsh-hooks
Config-driven lifecycle hooks: event→command declarations in cordis.patch.yml, with Feishu card notifications and a QR scan-to-create bot setup.
Install
dsh plugin --profile web add dsh-hooks
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-hooks
DeepSeek Harness(dsh)的配置驱动生命周期 hooks 插件。
直接在 profile 的 cordis.patch.yml 里声明「事件 → 命令」——就像 Codex CLI / OpenCode 的 hooks,但属于 dsh。不需要写插件代码。
[English](README.md) | [设计](#设计) | [飞书示例](examples/notify-feishu.mjs)
安装
一个包搞定全部(hook 引擎 + Web GUI 设置页):
dsh plugin --profile web add dsh-hooks # 从 npm 安装 # 或直接从 git 安装: dsh plugin --profile web add github:PeterBon/dsh-hooks
重启 dsh web 生效。安装后设置面板里会出现「Hooks」分区(见 [Web GUI](#web-gui))。
配置
在你的 profile 的 cordis.patch.yml 里添加配置块:
- id: dsh-hooks
name: dsh-hooks
config:
hooks:
- on: 'turn/end'
when: 'completed' # 可选:只在回合正常完成时触发
run: 'node examples/notify-feishu.mjs'
timeoutMs: 10000 # 可选,默认 10000
- on: 'approval/asked'
run: 'powershell -Command "Add-Content hooks.log approval-requested"'
- on: 'tool/call'
match: # 可选:字段 → 正则,全部匹配才触发
tool: '^(rm|git|ssh)'
run: 'node examples/notify-webhook.mjs --slack'
- on: 'turn/end'
when: 'completed'
run: 'node examples/notify-feishu.mjs'
retries: 2 # 可选:非零退出码重试(默认 0 不重试)
retryDelayMs: 1000 # 可选:重试基础间隔,每次翻倍(默认 500)
- on: 'turn/end'
input: 'stdin' # 可选:把完整上下文 JSON 写入命令 stdin
run: 'node my-hook.mjs'
- on: 'approval/asked'
notify: # 内置通知:与 run 二选一,无需外部脚本
channel: 'desktop' # 桌面气泡/toast 通知
- on: 'turn/end'
when: 'completed'
notify:
channel: 'webhook' # POST JSON 到任意 HTTP 端点
url: 'https://hooks.slack.com/services/…'
slack: true # 可选:改为 { text } 单行摘要(Slack 风格)
- on: 'step/end'
run: 'node examples/log-step.mjs'
debounceMs: 500 # 可选:高频事件去抖,窗口内合并为一次
maxConcurrent: 2 # 可选:并发上限,超出的触发被丢弃
- on: 'tool/result'
match:
toolDurationMs: '>10000' # 数值比较(也支持 { gt: 10000 } 对象语法)
run: 'node examples/notify-slow-tool.mjs'
- on: 'turn/end'
enabled: false # 可选:停用但不删除(跳过派发)
cwd: 'session' # 可选:在会话工作目录执行
run: 'node examples/log-turn.mjs'
每个 hook 的完整字段:
字段 · 含义 · 默认
on · 触发事件(见上方事件表) · 必填
when · 对 turn/end 按结束原因过滤 · 全部原因
match · 字段 → 正则或数值比较,全部匹配才触发;字段为上下文键(tool/sessionName/sessionId/error/source/cwd/content/reason/turn/durationMs/toolDurationMs…),上下文中不存在的字段视为不匹配。正则匹配字段的字符串表示;数值比较({ gt: 10000 } 或 '>10000',支持 gt/gte/lt/lte/eq 组合)只对数字字段生效,非数字字段上的比较永不匹配 · 不过滤
run · 通过系统 shell 执行的命令(与 notify 二选一) · 二选一必填
notify · 内置通知(与 run 二选一):channel: webhook(HTTP JSON,url 可省略用 DSH_HOOKS_WEBHOOK_URL,slack: true 换单行摘要)、channel: desktop(系统气泡/toast)或 channel: feishu(用扫码流程写入的凭据直接发飞书卡片,无需外部脚本;见「飞书通知」一节) · 二选一必填
input · env 只传 DSH_HOOK_* 环境变量;stdin 额外把完整上下文 JSON 写入命令标准输入 · env
timeoutMs · 单次执行超时(毫秒),超时终止进程树;notify 的 webhook 通道用它作为单次请求超时(默认 10000),桌面通道是本地气泡(脚本本身要显示约 9 秒)故不适用 · 10000
retries · 非零退出码的重试次数(spawn 失败与超时不重试) · 0
retryDelayMs · 重试基础间隔(毫秒),每次翻倍、上限 30 秒 · 500
enabled · false 停用该 hook:配置保留、静默跳过派发(不计入失败) · true
cwd · 命令执行目录:session 在会话工作目录执行;绝对路径在指定目录执行(只作用于 run) · 插件进程目录
maxConcurrent · 该 hook 允许的最大并发进程数;超出的触发被丢弃(执行历史记 skipped) · 不限
debounceMs · 去抖窗口(毫秒):高频事件(step/end、tool/*…)窗口内的多次触发合并为一次 trailing 执行,携带最新上下文 · 0(不去抖)
事件(v1)
事件 · 触发时机 · 有用上下文
turn/start · 回合开始(若有 turn/start hook,派发延迟到本回合的首条用户消息分类后,把触发文本注入 DSH_HOOK_CONTENT;无用户消息的回合在 turn/end 时无内容派发,见下方说明) · 会话 id、回合号、触发消息文本
turn/end · 回合结束(completed / error / aborted / blocked / max-tokens / interrupted / forked) · reason、回合号、耗时、内容、本回合 token 用量、运行中子代理数
tree/settled · 回合结束后把工作交给子代理的会话,其整个子代理树全部落定(无存活子代理仍在运行) · 子代理总数、交接到落定的耗时
step/end · 回合内一步结束(一次模型调用 + 其工具执行) · 回合号、步号
tool/call · 模型请求一次工具调用 · 工具名、调用 id、原始参数 JSON
tool/result · 工具调用完成 · 工具名(自动反查)、结果文本、失败标识、工具耗时(配对失败时无耗时)
user/message · 会话表面出现用户角色消息 · 来源 kind(user / plugin / …)、消息文本
approval/asked · 工具调用请求用户审批 · 工具名、调用 id、审批 id、原因
approval/decided · 待审批项得出结果(与 approval/asked 按 id 配对) · 结果 outcome、工具名(自动反查)、调用 id、审批 id
session/title · 会话标题更新(显式改名 / LLM 生成 / 回退) · 新标题、来源 kind
session/created · 会话发布 · 会话 id、cwd
session/disposed · 会话离开注册表 · 会话 id、cwd
agent/created · Agent 发布 · 会话 id
agent/disposed · Agent 离开注册表 · 会话 id
agent/error · Agent 循环报错 · 错误文本
agent/status · Agent 状态切换 · 状态
hook/failed · 同一 hook 连续失败达到 failedAlertThreshold(默认 3;合成事件,从结果流发射) · 失败 hook 摘要、连续失败次数
usage/daily · 本地日历日翻篇后的下一个事件(合成事件,无定时器):报告刚结束那一天的 token 用量 · 覆盖日期、当日回合数、贡献会话数、当日 token 明细
turn/end 的 when 匹配结束原因(completed、error、aborted、blocked、max-tokens、interrupted、forked);其他事件的 hook 无条件执行。其中 forked 只由 fork seed 构造产生(agent loop 不会发射),因此 when: forked 只会在重放 fork seed 时命中。
命令执行
- 每个命中的 hook 通过系统 shell 执行
run,fire-and-forget:失败只console.warn、默认不重试、绝不阻塞 agent 循环。命令的 stdout/stderr 会被捕获(各 64 KiB 上限),非零退出码时把 stderr 尾部写进告警日志。 - 重试(
retries/retryDelayMs)对两个执行通道都生效,都是「首次尝试之后再重试 N 次」、间隔按retryDelayMs逐次翻倍(默认 500ms,上限 30 秒,避免一次触发在退避里挂几分钟);插件卸载(重启/重载 profile)会立即中止进行中的重试(含退避等待与在途请求),历史里记为skipped而不是失败: run:只重试非零退出码(spawn 失败与超时不重试)。notify的 webhook 渠道:重试传输失败(连接被重置、超时)与 HTTP 408 / 429 / 5xx;其余 4xx 表示请求本身有问题,不重试。默认retries: 0表示只尝试一次——0.13 之前 webhook 曾硬编码「传输失败自动再试一次」,现在这层兜底已并入retries,需要它的老配置请显式写retries: 1。notify的 desktop 渠道是本地弹窗,不重试。- 上下文通过环境变量传递(数据不拼接进 shell 字符串,防注入):
变量 · 含义
DSH_HOOK_EVENT · 事件类型,如 turn/end
DSH_HOOK_SESSION_ID · 会话 id
DSH_HOOK_SESSION_NAME · 会话可读标题(最新 session/title 日志事件,或首个用户消息回退)
DSH_HOOK_CWD · 会话工作目录
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::21,最后更新 2026-10-29