denceee/dsh-compat-adapter

dsh-compat-adapter

DSH 前置/基座适配插件:把 Claude Code / Codex / WorkBuddy / OpenCode / GitHub Copilot 的 hooks 与 rules,通过显式声明适配到 DSH seam。5 家 vendor、85 个事件,每条适配逐项列出损失,fail-closed。

미분류adapterai-agentsclaude-code

dsh-compat-adapter

让 Claude Code / Codex / WorkBuddy / OpenCode / Copilot 的 hooks 与 rules,在 DSH 里真正跑起来。

一个 DSH 前置适配插件:显式声明 → 能力分级 → 路由到真实 seam。
不猜、不假装修好了、不把别家运行时塞进 Host。

🔌 我要用它 — 别的插件如何调用本适配器(消费者指南)

English · 简体中文


这是什么

DSH(DeepSeek Harness)是新的 agent 运行时。而你已经在 Claude Code、Codex、WorkBuddy、OpenCode、GitHub Copilot 里积累了成套的 hooks 与 rules —— 那些"写文件前先拦一下""工具调用后补一段上下文""会话结束时跑个脚本"的东西。

把它们搬到 DSH 最省事的做法,是照着名字硬接一遍。那会静默地坏掉:

Claude 的 updatedInput 被解析了但不生效; Codex 的 permissionDecision: "ask" 没有审批路径,退化成放行; Copilot 把 deny 写在顶层,而 DSH 的 hook 解析器只读 hookSpecificOutput —— 直接复用的话,每一个 deny 都会被当成"没有决定"而放行; OpenCode 的插件是 TypeScript 回调,"名字"能对上,"回调体"根本过不来。

这个插件存在的意义,是让每一次适配的"损失"都变成可读的输出,而不是一个看起来能跑的假象。

它只做三件事:

声明预置作者显式写出 vendor 与 event。从不嗅探文件名、从不猜归属。
分级每个资产返回冻结的四级结果,损失逐条列出。
路由有官方桥就复用官方桥,有可拦截 seam 就真翻译,只能观察就明说只能观察,做不到就拒绝。

两个不做

  • 不重造官方桥。 能映射的事件直接交给 @deepseek-ai/dsh-hooks-claude-code / -codex,本插件只负责"把声明变成桥的配置"和"没有桥的事件怎么办"。
  • 不执行外部代码。 JS 函数 handler、OpenCode 的 TS 回调、Claude 的 http / mcp_tool / prompt / agent handler —— 一律 UNSUPPORTED。别家插件运行时不嵌进 DSH Host。

核心设计:分级,而不是布尔

每个注册都返回一个冻结的能力结果。没有 supported: true 这种把风险抹平的字段。

分级含义本版本情况
DIRECTDSH 存在 1:1 seam,语义原样保留。零。见下。
ADAPTEDDSH 有 seam,但存在语义损失;degradations[] 逐条列明。全部 112 个事件
CARRY无通用映射,原文逐字节携带、会话开始注入;永不变成可执行策略。全部 5 家 rules
UNSUPPORTED无忠实映射;拒绝并给出 reason。程序性边界(见下)

为什么一个 DIRECT 都没有

两个官方桥本身就是"尽力而为"的方言适配。宣称逐字节忠实,就是虚假能力。

所以本插件任何一行都不标 DIRECT —— 全部 112 行都是 ADAPTED,每一行都必须写出自己的损失。这不是悲观,这是诚实:test/matrix-readme.test.js 会断言 DIRECT.length === 0,并断言每一行 ADAPTED 都至少带一条 degradations。

全覆盖不等于等价。 112 个事件全部可执行(0 拒绝),但这是"每行都写明损失"的全覆盖,不是"全都一模一样"。其中 10 个事件在自己的 degradations 里明确写着 Approximate(Claude MessageDisplay、StopFailure;Codex Interrupt;WorkBuddy Elicitation、ElicitationResult;OpenCode installation.updated、server.connected、session.deleted、session.updated、tui.toast.show),其余 102 个为真实/精确映射。test/doc-counts.test.js 会把这组数字与代码逐项比对,文档不能比代码多宣称一分能力。

四条硬性不变量(测试强制,不是文档承诺)

不变量由谁保证
ADAPTED 必须至少列出一条 degradations,否则构造即抛错capabilityResult() + test/capability.test.js
UNSUPPORTED 必须带 reason,否则构造即抛错capabilityResult() + test/capability.test.js
不认识的 vendor / event 抛错(UNKNOWN_VENDOR / UNKNOWN_EVENT),不静默 fallbacktest/service.test.js
UNSUPPORTED 的资产永不挂载:不写配置、不挂桥、不进 status() 的可执行列表test/service.test.js + verify:live

那么在全部可执行的今天,UNSUPPORTED 还剩哪些真实触发路径?

  • JS 函数 handler —— 不把外部代码执行进 Host。
  • 缺 ctx.shell —— 组合里没有 shell executor,命令 hook 跑不了;rules 仍然可用(只读文件、只注入)。
  • 缺官方桥包 —— dsh-hooks-* 从 profile 解析不到时拒绝,不退回自研实现。
  • 非 command handler —— http / mcp_tool / prompt / agent 抛 INVALID_HANDLER。

支持的 vendor

五家互相独立。每家是一个纯数据模块(src/vendors/<vendor>.js):无 I/O、不 import DSH,所以整张矩阵脱离 Harness 也能单测。

Vendor别名官方桥事件官方桥原生翻译只观察自建桥近似rules
claudeclaude-code, ccdsh-hooks-claude-code33724202CARRY
codexopenai-codexdsh-hooks-codex1254301CARRY
workbuddycodebuddydsh-hooks-claude-code24519002CARRY
opencode—无29029005CARRY
copilotgithub-copilot无14040100CARRY
合计112178051010

"近似"列统计 degradations[] 中自述 Approximate 的事件数。它与其他列并列展示、而不是藏进正文:它不为零,就不许写成零。

三个容易被误读的点:

  • workbuddy:WorkBuddy 桌面端没有公开 hook API。本插件按 CodeBuddy 引擎(腾讯文档为 Claude-Code 兼容)处理,并把"桌面端无 API""CODEBUDDY_PROJECT_DIR 不导出""modifiedInput 不被读取"写进每一条 degradations,由测试断言。
  • opencode:29 个回调名全部映射到 DSH seam,但 TS 回调"体"不携带 —— 必须改写为命令。这是 out of scope,不是"忘做了"。
  • copilot:DSH 没有官方 Copilot 桥,所以其 14 个事件里 10 个走本插件自建桥(builtin-bridge),4 个走原生翻译。deny 真拦工具、block 真续跑、子 agent 真注入、approval 真决定 —— 详见 Copilot 的特殊路径。

工作流程

flowchart TB
    P["预置插件<br/>preset-class plugin"] -->|"显式声明 vendor + event"| S["ctx.compatAdapter<br/>registerHook / registerRules"]
    S --> G{"能力分级"}
    G -->|"official-bridge · 17"| B["官方桥<br/>dsh-hooks-claude-code / -codex"]
    G -->|"native-translate · 80"| T["原生翻译层<br/>src/translate.js"]
    G -->|"native-seam · 5"| O["只观察路由<br/>src/observe.js"]
    G -->|"builtin-bridge · 10"| BB["自建桥<br/>src/bridges/copilot.js"]
    G -->|"CARRY · rules"| R["原文逐字节携带"]
    G -->|"UNSUPPORTED"| X["拒绝 + reason<br/>永不挂载"]
    B --> SEAM["DSH seam<br/>tools/pre-execute · approval/request<br/>agent/turn-stopping · session/event …"]
    T --> SEAM
    O --> SEAM
    BB --> SEAM
    R --> CTX["agent/created<br/>注入模型可见上下文"]
    SEAM --> RUN["真实拦截<br/>deny 阻断 · block 续跑 · ask 走审批"]

四种路由的差别,就是**"决定能不能到达 DSH"**:

路由决定是否生效用在哪
official-bridge✅ 由官方桥映射有官方桥的 vendor(claude / codex / workbuddy)
native-translate✅ 本插件直接产出 DSH 裁决类型无桥但 seam 可拦截
native-seam❌ 只观察,阻断尝试被记录并忽略无取消能力的生命周期时刻
builtin-bridge✅ 本插件自建的桥(仅官方无桥时使用)copilot

安装

标准 DSH Cordis bundle(package.json 的 dsh.bundle.patch + cordis.patch.yml)。

# 方式 A:plugin_manager 工具(受管理路径,推荐)
#   action: install_bundle, target: <本仓库绝对路径>

# 方式 B:CLI
dsh plugin --profile <profile> add <本仓库绝对路径>

装好后插件只做一件事:提供 ctx.compatAdapter 服务。它自己不做任何适配 —— 适配由预置插件声明触发。

⚠️ 一个必须知道的坑:cordis.patch.yml 里的 name 必须是 bare specifier

bundle patch 的 baseUrl 是 profile 根目录,不是 patch 文件所在目录。所以 row 的 name 写成相对路径或 node_modules/<pkg>/ 前缀都会找不到包。

# ✅ 正确
- insert:
    - id: compat-adapter
      name: 'dsh-compat-adapter'

test/index.test.js 会断言这一点(并先剥掉注释,避免把注释里的反例误判成 patch 内容)。

预置类插件怎么用

在预置的 plugins 列表里插入本插件(装载顺序由 inject 保证),然后在你自己的 apply() 里调用服务:

- insert:
    - id: preset-my-preset
      name: '@deepseek-ai/dsh-agent-preset'
      config:
        id: my-preset
        name: My Preset
        plugins:
          - id: compat-adapter
            name: 'dsh-compat-adapter'
          - id: my-vendor-assets
            name: './my-vendor-assets/index.js'
// my-vendor-assets/index.js
export const name = 'my-vendor-assets'
export const inject = ['compatAdapter']

// apply 可以是 async:Cordis 会等待它,且注册必须在插件 setup 阶段完成。
export async function apply(ctx) {
  // ── hooks ──────────────────────────────────────────────────────────────
  const r = await ctx.compatAdapter.registerHook({
    vendor: 'claude',                        // 显式声明,不靠检测
    event: 'PreToolUse',
    matcher: 'Write',                        // 厂商 matcher 语义,原样交给官方桥
    handler: 'node ./hooks/no-write.mjs',    // 命令字符串 = command hook
  })
  // r.capability === 'ADAPTED'
  // r.seams      === ['tools/pre-execute']
  // r.degradations === [... 逐条损失 ...]

  // ── rules ──────────────────────────────────────────────────────────────
  const rules = await ctx.compatAdapter.registerRules({
    vendor: 'codex',
    source: 'rules/*.rules',            // 绝对路径 / 相对 baseDir / file:// / glob
    baseDir: '/abs/path/to/package',    // 相对路径的基准;必须绝对
  })
  // rules.capability === 'CARRY',会话开始时作为上下文注入
}

公共 API

registerHook(declaration)

字段必填说明
vendor✅claude / codex / workbuddy / opencode / copilot,或文档化别名
event✅厂商事件名(如 PreToolUse);未知事件抛 UNKNOWN_EVENT
matcher—厂商 matcher 字符串;非字符串抛 INVALID_MATCHER
handler—命令字符串,或 { type: 'command', command, timeoutSec? }。http / mcp_tool / prompt / agent 抛 INVALID_HANDLER;JS 函数返回 UNSUPPORTED
id—稳定 id,缺省 <vendor>:<event>:<n>

返回 Promise<CapabilityResult>(冻结)。可移植性不足不抛错 —— 只有"声明本身无法处理"才抛错。

registerRules(declaration)

字段必填说明
vendor✅同上
source✅字符串或字符串数组:绝对路径、baseDir 相对路径、file:// URL、glob
baseDir—相对源的基准目录,必须绝对路径(否则抛 INVALID_DECLARATION)
id—稳定 id,缺省 <vendor>:rules:<n>

返回 Promise<CapabilityResult>,capability === 'CARRY',seams === ['agent/created']。

规则语义:注册时读盘并逐字节携带(附 sha256 与 bytes),在 agent/created(fresh / resume / clear / compact 全覆盖)时通过 agent.inject(createUserMessage(...)) 作为模型可见上下文入队。注入消息的 source.kind 是 compat-adapter-rules,与 DSH 自带的 agent-instructions 区分,日志里可追溯。

规则永远不是安全边界。 全部 CARRY。尤其 Codex 的 Starlark 规则是命令策略 DSL,DSH 没有对应规则引擎 —— 本插件只把它作为"原工具策略的说明文本"注入,不翻译、不强制执行。任何"规则会阻止某事"的预期都是错的;要强制,请写原生 DSH 插件挂到 tools/pre-execute。

其他方法

方法作用
matrix()静态能力矩阵(本版本"永远能做什么")。
status()运行时审计("此刻实际在跑什么"):每个 hook 的分级 / seam / 是否挂了桥,规则文件的路径 / 字节 / sha256,以及当前组合里 shell / sessionProjections 是否存在。
dispose()卸载所有桥与监听;之后注册抛 ADAPTER_DISPOSED(不会半死不活地继续)。

配置项(cordis.patch.yml 的 row config)

字段默认说明
logRegistrationstrue每次分级注册输出一行结构化日志
failClosedtrue缺所需 seam 时拒绝注册,而不是挂一个空转的 no-op
targetDshVersion0.2.0-rc.2目标 DSH 版本;不一致时按 failClosed 报错或告警
maxRuleBytes524288单文件携带上限;超限报错而不是静默截断

阻断契约

参考工具文档里"exit code 2 = 阻断"的行为,在本 DSH 版本的两个官方桥里不可靠(可能被记录为 decision: 'pass' 而放行)。因此本插件统一契约:

stdout 输出 hookSpecificOutput.permissionDecision(deny / ask)+ 退出码 0。

#!/usr/bin/env bash
# 例:拒绝写入 —— deny + exit 0
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"no writes in this preset"}}'
exit 0

本插件生成的任何示例、测试与文档都用这一种;从不生成依赖 exit 2 的 handler。src/vendors/shared.js 的 permissionDecisionOutput() 是唯一生成入口,测试直接断言生成物含 permissionDecision、且不含遗留的顶层 decision 字段。

ask 映射成 DSH 的 {kind:'ask'},deny 映射成 {kind:'deny'}(经官方桥,不经过本插件)。exit 2 仍会被桥解析(用户手写的老 hook 不会被吞掉),但这不是本插件承诺的契约路径。


Copilot 的特殊路径

GitHub Copilot 是唯一没有官方 DSH 桥的 vendor(Claude / Codex / WorkBuddy 都能复用官方桥),所以它的适配路径不同:本插件真的实现翻译层,不是旁观。

为什么不能直接复用 hook protocol

DSH 的 @deepseek-ai/dsh-hook-protocol 只认 Claude/Codex 方言:

// DSH 解析器只读这一层
{ "hookSpecificOutput": { "permissionDecision": "deny", "permissionDecisionReason": "…" } }

而 Copilot 把决定写在顶层:

{ "permissionDecision": "deny", "permissionDecisionReason": "…" }

若直接复用,每一个 Copilot deny 都会被静默当成"没有决定"而放行。所以 src/translate.js 里有一层 parseCopilotStdout() / normalizeCopilotOutput():先按 Copilot 原生格式解析,再归一化成内部形状。

这条路径有专门的测试(test/smoke-copilot.test.js,21 个用例)并纳入 npm run verify:live 的真实启动验证。

事件映射

Copilot 事件DSH seam能否阻断
preToolUsetools/pre-execute✅ deny / ask 真的生效
postToolUsetools/post-execute转发 additionalContext(modifiedResult 不采纳)
subagentStartsubagent/start✅ 可把上下文注入子 agent(经 ctx.agents.get(runId))
subagentStopsubagent/end只观察(拿到 stopReason / lastAssistantMessage)
errorOccurredagent/error只观察(DSH 错误路径无裁决通道)
sessionStartagent/created注入 additionalContext 为消息
sessionEndagent/disposed只观察
userPromptSubmittedagent/pre-step注入上下文(不改写 prompt)
agentStopagent/turn-stopping✅ decision:"block" 真的强制续跑(含 8 次 runaway guard)
notificationagent/status只观察(DSH 只发布状态翻转,无 notification 类型)
permissionRequestapproval/request✅ allow → allowed-once,deny → rejected
postToolUseFailuretools/post-execute仅当结果被标记为 error 时触发

三处必须注意的方言差异

  1. matcher 是锚定正则。 Copilot 编译为 ^(?:PATTERN)$ 且必须全匹配 —— bas 不会匹配 bash;无效正则会让该 hook 整体跳过(不是 match-all)。本插件按此实现。
  2. allow 不预批准。 Copilot 的 allow 是"不弹权限提示",不是"强制放行",所以翻译成 next() 而非 allow 短路。
  3. 阻断契约仍统一为 stdout + exit 0。 Copilot 本身还接受 exit 2 表示 deny,但本插件不让 hook 依赖它 —— 同一个 hook 脚本在各 vendor 下行为一致。

支持矩阵

本节由代码生成:npm run matrix:write 从 src/matrix.js 重新生成,test/matrix-readme.test.js 在漂移时报错。不要手改。

读矩阵需要注意的三件事:

  1. ADAPTED 的损失是逐条列出的,不是修辞。例如 Claude PreToolUse 的 updatedInput 只被解析、不被采纳;Codex PreToolUse 的 ask 没有审批路径;Claude SubagentStart 的 agent_type 恒为 general-purpose(针对具体 agent type 的 matcher 永不命中)。
  2. native-seam 的事件是"只观察":PreCompact 映射到 session/event 的 compaction/start,SessionEnd 映射到 agent/disposed。DSH 的压缩是服务调用、不是可取消 waterfall,所以不能否决压缩;尝试阻断只会被记入审计日志并忽略。
  3. WorkBuddy 的每一行都必须说明"桌面端无公开 hook API" —— 这是最容易误读的地方,所以它被写进每条 degradations 并由测试断言。
展开完整矩阵(112 行,含每行损失说明)

Hooks: vendor event → DSH seam

VendorEventCapabilityRouteDSH seam(s)Losses / reason
claudeSessionStartADAPTEDofficial bridge (claude)agent/createdRuns detached through the bridge, so context can miss the model first request.; Plain-stdout context, initialUserMessage, sessionTitle, watchPaths and CLAUDE_ENV_FILE are not consumed.; Payload omits model, agent_type and session_title.
claudeUserPromptSubmitADAPTEDofficial bridge (claude)agent/pre-stepDeny is honored; plain-stdout context, sessionTitle and suppressOriginalPrompt are not.; Unless the hook sets its own timeout the bridge uses its 600s default, not the reference 30s.
claudePreToolUseADAPTEDofficial bridge (claude)tools/pre-executedeny and ask are honored; allow does not pre-approve and defer is unsupported.; updatedInput is parsed and warned about but not honored.; additionalContext from a PreToolUse hook is ignored.
claudePostToolUseADAPTEDofficial bridge (claude)tools/post-executeBlocking feedback and additionalContext work; updatedToolOutput and updatedMCPToolOutput do not.; tool_response is flattened to text.
claudeStopADAPTEDofficial bridge (claude)agent/turn-stoppingA blocking Stop forces another step, but stop_hook_active is always false and last_assistant_message is omitted.; There is no consecutive-block cap; an unconditional blocking hook keeps forcing turns.
claudeSubagentStartADAPTEDofficial bridge (claude)subagent/startagent_type is the constant general-purpose; a matcher naming a specific agent type never fires.; Injected context can only reach an in-process child.
claudeSubagentStopADAPTEDofficial bridge (claude)subagent/endObserve-only: cannot block the child or feed it context.; agent_transcript_path, last_assistant_message, background_tasks and session_crons are omitted.
claudePostCompactADAPTEDnative translation (compaction/end)session/eventObserve-only: the compaction already completed, so a hook can react but never change the result.; The summary is taken from the preceding durable compaction/summary event; the hook sees the same text, not a live object.; trigger is reported as "auto" because DSH does not distinguish a manual /compact from automatic pressure at this seam.
claudePreCompactADAPTEDnative seam, observe-only (compaction/start)session/eventCannot veto compaction: DSH compaction is a service call, not a cancellable waterfall, so the hook is observe-only.; Fires on the durable compaction/start event after compaction was requested, not before the summary is computed.; A blocking decision (deny / exit 2) is audited but does NOT prevent compaction.
claudeSessionEndADAPTEDnative seam, observe-only (agent/disposed)agent/disposedagent/disposed fires when an Agent leaves the live registry, which is broader than the reference end-of-session moment (dispose also happens on reload/teardown).; Payload carries no reason and no transcript path; anything the hook needs must be read from the session working directory.
claudeSetupADAPTEDnative translation (session/created)session/createdObserve-only: DSH announces a created session but exposes no pre-activation configuration channel here.; Fires when a Session object is created, which is after the harness is already running — not before hooks are enabled as the reference tool does.
claudeInstructionsLoadedADAPTEDnative translation (agent/instructions-loaded)session/createdFires once per created session with the full discovered list, not once per file as the reference event does.; The list is what DSH discovery would find (CLAUDE.md, AGENTS.md, configured candidates); a file added later is not re-reported.; Observe-only: DSH loads instructions through dsh-agent-instructions, which exposes no veto or rewrite here.
claudeUserPromptExpansionADAPTEDnative translation (agent/pre-step)agent/pre-stepObserve-only: DSH pre-step receives the already-assembled batch, so the hook cannot rewrite the expansion.; Fires per step, not once per expanded prompt.
claudeMessageDisplayADAPTEDnative translation (session/event)session/eventObserve-only and approximate: DSH has no display seam, so this reports the durable message event that would be displayed.; The hook cannot alter, delay, or suppress presentation.
claudePermissionRequestADAPTEDnative translation (approval/request)approval/requestDecisions map onto DSH ApprovalOutcome: allow→allowed-once, deny→rejected. DSH has no broader grant, so a hook cannot express "always allow".; The approval waterfall only runs when DSH actually asks for approval; a tool the preset already permits never reaches this hook.; The reference payload’s permission-mode detail is absent; DSH reports the tool and its reason only.
claudePermissionDeniedADAPTEDnative translation (session/event)session/eventObserve-only: the decision already committed, so the hook cannot change it.; Driven by the durable approval/decided audit event with outcome rejected; other outcomes do not fire it.
claudePostToolUseFailureADAPTEDnative translation (tools/post-execute)tools/post-executeMounted by this adapter rather than the shipped bridge (the bridge does not map this event).; Only fires when the result is marked as an error; DSH uses one waterfall for successes and failures.
claudePostToolBatchADAPTEDnative translation (step/end)session/eventREAL batch boundary: DSH defines one step as one model call plus the tool executions it requested, so this fires once per completed step with the batch's tool names and count.; The payload carries the batch identity and tool names, not the individual tool results; a hook needing result bodies should also use PostToolUse.
claudeNotificationADAPTEDnative translation (agent/status)agent/statusObserve-only and coarse: DSH publishes agent status flips, not typed notifications.; matcher cannot narrow because DSH carries no notification type.
claudeTaskCreatedADAPTEDnative translation (todo/write)session/eventObserve-only: DSH publishes a whole todo-list snapshot, so creation is derived by diffing snapshots.; Only todo-style tasks are visible; jobs and team tasks register through services with no event.
claudeTaskCompletedADAPTEDnative translation (todo/write)session/eventObserve-only: completion is derived from a todo snapshot where an item's status became completed.; A task removed from the list without being completed does not fire it.
claudeStopFailureADAPTEDnative translation (agent/error)agent/errorObserve-only: DSH exposes no verdict channel on its error path, so this hook can report but never retry or alter the failure.; Fires for any turn/step agent error, not only the "API error ended the turn" moment the reference tool reports.; Reference payload fields (error type, retryability) are approximated from the thrown error and are not normalized to Claude’s vocabulary.
claudeTeammateIdleADAPTEDnative translation (agent/status)agent/statusObserve-only: DSH status flips are a consequence of driver activity, so a hook cannot make an agent idle.; Fires for any agent entering idle, not only teammates in a team.
claudeConfigChangeADAPTEDnative translation (settings/document-updated)settings/document-updatedObserve-only: the new settings revision has already committed, so a hook cannot block or rewrite it.; DSH reports settings namespaces, not the configuration FILES the reference tool watches; filePath is null and a matcher on a path never narrows.
claudeCwdChangedADAPTEDnative translation (session/created)session/createdObservational approximation: DSH pins the session working directory at creation and never changes it, so this fires once at session creation with the initial cwd instead of on a change.
claudeDirectoryAddedADAPTEDnative translation (session/created)session/createdObservational approximation: DSH grants one working directory per session and has no directory-addition event, so this fires once at session creation.
claudeFileChangedADAPTEDnative translation (fs/watch)fs.watchThis adapter ARMS a real filesystem watcher (ctx.fs.watch) on the session working directory, so external edits fire it — not only DSH-made changes.; One watcher covers the session cwd with depth 0, matching the reference tool's project-directory watch; a change deep inside a subtree may not be reported by every backend.; Bursts are coalesced with a 250ms debounce, so several rapid edits report as one change.; A composition without ctx.fs falls back to the turn-summary approximation, and the reason is logged.
claudeWorktreeCreateADAPTEDnative translation (git/worktrees)shellREAL detection: this adapter polls git worktree list --porcelain and fires when a worktree actually appears, so the hook receives the worktree path, HEAD and branch.; DSH has no worktree lifecycle event, so polling is used; a create/remove pair inside one poll interval is observed as the net change.; Requires ctx.shell and a real git repository; a non-repository session yields no worktrees and the hook never fires.
claudeWorktreeRemoveADAPTEDnative translation (git/worktrees)shellREAL detection: fires when a worktree disappears from git worktree list --porcelain, and the payload names the removed path with its last known HEAD and branch.; Polling-based, so the exact removal moment is bounded by the poll interval.
claudePreModelSwitchADAPTEDnative translation (request/header)session/eventREAL detection: fires when the durable request/header shows a changed provider/model, carrying both the previous and current values.; Timing differs from the reference tool: DSH logs the header when the next request carries the new config, so the hook observes th