@deepseek-ai/dsh-hook-protocol 0.1.1-rc.1 → 0.1.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +122 -23
- package/README.zh.md +122 -23
- package/package.json +9 -9
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/hooks/hook-protocol/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: ba67dcd503fb8b5578ec09dab1f10dc74fadc989
|
|
6
|
+
README.zh.md: 1a853130197d886c7f4d20a5df24d91478a37b0d
|
package/README.md
CHANGED
|
@@ -1,44 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The shared hook rules behind the Claude Code and Codex bridges — what a hook can do and what happens when it runs — for users and maintainers of the hooks subsystem."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-hook-protocol
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-hook-protocol` makes both bridges handle your hooks identically: it defines what a hook can do and what happens when it runs. You never install or configure it yourself — choose `dsh-hooks-claude-code` or `dsh-hooks-codex`, point it at your existing `hooks.json`, and these rules apply to your hooks. Through either bridge, a hook can block a prompt or tool call with a message the model sees, attach extra context to the conversation, or ask the run to stop. Only command hooks run; `http`, `mcp_tool`, `prompt`, and `agent` handlers are skipped with a warning.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
You don't install or configure this package directly — mounting `dsh-hooks-claude-code` or `dsh-hooks-codex` applies these rules to your existing `hooks.json` hooks. Use this page to learn what a hook can do and what happens when it runs; the two bridge pages list which events each dialect supports.
|
|
29
|
+
|
|
30
|
+
### When to choose it
|
|
31
|
+
|
|
32
|
+
Choose `dsh-hooks-claude-code` or `dsh-hooks-codex` when you have existing Claude Code or Codex hooks and want them to keep working during agent runs. You never choose this package directly. Avoid the whole group for bespoke behavior with no reference-tool equivalent: a native Cordis plugin has the full harness API with no hook protocol in between.
|
|
33
|
+
|
|
34
|
+
### What a hook can do
|
|
35
|
+
|
|
36
|
+
- **Block an action with a message** — a hook that exits with code 2 stops the prompt or tool call, and its error output is shown as the reason.
|
|
37
|
+
- **Ask before a tool runs** — a Claude Code hook can request confirmation instead of blocking outright; the Codex bridge does not surface this option.
|
|
38
|
+
- **Attach context** — a hook can return extra text that the model sees in the next request.
|
|
39
|
+
- **Run on chosen moments** — a hook config selects which events it fires on by name or pattern; an absent, empty, or `'*'` pattern means every event of that kind.
|
|
40
|
+
- **Fail without stopping the run** — any exit code other than 2 is a non-blocking failure: the action proceeds and the failure is logged, and a hook that cannot be started at all is treated the same way.
|
|
41
|
+
- **Ask the run to stop** — a hook can request that the run halt (`{"continue": false}`); the request is recorded but has no run-level effect (see Known Limitations).
|
|
42
|
+
|
|
43
|
+
### What you see when hooks run
|
|
44
|
+
|
|
45
|
+
- When a hook blocks, the action does not happen and the hook's message is shown.
|
|
46
|
+
- When a hook attaches context, the model sees that text in its next request.
|
|
47
|
+
- A hook that fails — a bad command, a crash, or any exit other than 2 — is logged and does not stop the agent.
|
|
48
|
+
- If the hook config cannot be read or parsed, the bridge logs a warning and no hooks run; the agent still starts.
|
|
49
|
+
- Configs that mix hook types still work: `http`, `mcp_tool`, `prompt`, and `agent` handlers are skipped with a warning, and their command hooks run.
|
|
50
|
+
|
|
51
|
+
-----
|
|
52
|
+
|
|
53
|
+
<a id="understand-the-implementation"></a>
|
|
54
|
+
## Understand the implementation
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>Implementation internals — click to expand</summary>
|
|
58
|
+
|
|
59
|
+
This section explains the design decisions behind the library and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
6
60
|
|
|
7
|
-
|
|
61
|
+
### Processing pipeline
|
|
8
62
|
|
|
9
|
-
|
|
63
|
+
The library is a chain of single-purpose steps, one function each: validate the matcher pattern, run the command through the `dsh-shell` executor, decode the outcome, merge every matched hook's outcome into one most-restrictive result, and record the durable `hook/*` event pair. The matcher's `mode` parameter is the single axis the dialects differ on — `claude-code` interprets a pattern as literal alternatives or a regex, `codex` always as an unanchored regex. Every step degrades to a contained outcome instead of throwing, so a hook can never crash the calling turn: an invalid regex is a non-match, an executor rejection becomes a `HookOutput` with no exit code, exit 2 blocks with stderr as the reason, and every other failure stays non-blocking. Merging applies `deny > ask > allow` precedence, keeps the first `continue: false` stop sticky, and accumulates context in hook order. Detached runs are tracked so `fiber.dispose()` reaches quiescence, and the invariant companion rejects `hook/*` records outside an open turn. The steps live in [`src/matcher.ts`](src/matcher.ts), [`src/runner.ts`](src/runner.ts), [`src/codec.ts`](src/codec.ts), [`src/merge.ts`](src/merge.ts), [`src/events.ts`](src/events.ts), [`src/detached.ts`](src/detached.ts), and [`src/invariant.ts`](src/invariant.ts).
|
|
10
64
|
|
|
11
|
-
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| Matcher validation + test | `matcherDiagnostic(pattern, mode)` for parse-time diagnostics; `matchesMatcher(pattern, query, mode)` for contained runtime matching | picks its `mode` (`claude` = literal-or-regex, `codex` = always regex) and rejects a config group carrying a diagnostic |
|
|
14
|
-
| Run a hook | `runHook(bash, hook, opts, now)` — stdin payload + env via `ctx.shell`, decode | builds the per-event stdin **payload** + the dialect's **env** |
|
|
15
|
-
| Decode output | `parseHookOutput(exit, stdout, stderr)` → neutral `HookOutput` | maps the neutral `HookOutput` onto an extension-point-specific typed Decision |
|
|
16
|
-
| Merge N hooks | `mergeHookOutputs(outputs)` → most-restrictive `MergedHookOutcome` | — |
|
|
17
|
-
| Durable record | `appendHookInvoked` / `appendHookResult` (`hook/*` session events; the result's `decision`/`stderrSummary` derive from the `HookOutput` here) | calls them around each invocation |
|
|
18
|
-
| Detached-run quiescence | `createDetachedRuns()` — track fire-and-forget run chains; `drain()` aborts, then awaits them | passes `signal` to each detached `runHook`, registers `drain` as its effect disposer |
|
|
65
|
+
### `hook/*` session events
|
|
19
66
|
|
|
20
|
-
|
|
67
|
+
The `hook/invoked` and `hook/result` events are declaration-merged into `SessionEventMap` as log-only records: like `compaction/*`, they are not surface events and carry no `surfaceOp`. A `hook/result` pairs with its `hook/invoked` by `handlerId`, and `appendHookResult` owns the decision rule. Payloads and per-event JSDoc live in the generated [persistence log event catalog](../../../docs/persistence-catalog.md).
|
|
21
68
|
|
|
22
|
-
|
|
23
|
-
- **`runHook(bash, hook, options, now)`** — require and forward the caller-owned `options.signal`, serialize `options.payload` to the hook's stdin (with a trailing newline iff `options.trailingNewline`), merge `options.env` after the executor's credential scrub (the `dsh-shell` trusted-plugin API), honor the hook's `timeoutSec` (else `options.defaultTimeoutMs` — the bridge owns the default, its config defaulting to the lib's `DEFAULT_HOOK_TIMEOUT_MS` 10-minute reference), and decode the result (threading `options.expectedEventName` to the codec). Cancellation therefore reaches the executor's process-group kill and join boundary. Never throws: an executor rejection (infra fault) becomes a `HookOutput` with `exitCode: undefined` (a non-blocking error). `now` is injected for testable durations.
|
|
24
|
-
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** decodes exit status and structured stdout. Exit 2 blocks with stderr; other failures are non-blocking. A matching hook-specific permission decision overrides the legacy top-level decision; mismatched or missing event discriminators suppress only event-specific fields. Top-level fields remain event-agnostic, and successful non-JSON output is left to the bridge.
|
|
25
|
-
- **`mergeHookOutputs(outputs)`** — fold the results of every hook that matched one point: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined with `\n\n`, `additionalContext`/`systemMessages` accumulated in order.
|
|
26
|
-
- **`createDetachedRuns()`** — quiescence tracking for the emit-shaped points, which run detached (no extension point awaits them). The bridge tracks each run chain — the hook run PLUS its continuation — and registers `drain()` as its effect disposer: drain fires the tracker's abort `signal` (so a still-running hook process is killed via `runHook`, not awaited out to its timeout), then resolves once every tracked chain has settled. `fiber.dispose()` resolving therefore means no detached hook work is left to fire into a disposed context ([defensive patterns](../../../docs/defensive-patterns.md): dispose must reach quiescence).
|
|
69
|
+
Invocation and result records must sit inside an open turn: `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` satisfy that relation by construction, while `SessionStart` runs before turn 1 and gets no `hook/*` record — its injected context is delivered instead. The invariant companion registers on `ctx.invariants` and rejects `hook/*` events appended outside an open turn, a result without a matching invoked, an unknown dialect, or a non-finite duration.
|
|
27
70
|
|
|
28
|
-
|
|
71
|
+
### Design philosophy
|
|
29
72
|
|
|
30
|
-
|
|
73
|
+
- **One axis of difference collapsed into `mode`.** The dialects differ only in how a matcher pattern is interpreted, so the matcher takes the mode as a parameter instead of duplicating the engine.
|
|
74
|
+
- **The executor owns process control.** Commands run through the `dsh-shell` executor rather than a bespoke spawn: the executor already provides the scrubbed-but-overridable environment, process-group cancellation, and timeout the protocol needs.
|
|
75
|
+
- **Never throw into the loop.** Every failure mode — malformed JSON, an invalid regex, an executor rejection — degrades to a contained outcome or a non-match, so a hook can never crash the calling turn.
|
|
76
|
+
- **Log-only, turn-enclosed events.** The `hook/*` records are durable evidence of what ran and what it decided; they are not surface events, and the invariant companion rejects them outside an open turn.
|
|
31
77
|
|
|
32
|
-
|
|
78
|
+
The [hook-protocol-lib Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md) records the shared-versus-per-dialect split and the alternatives considered.
|
|
33
79
|
|
|
80
|
+
### Source map
|
|
81
|
+
|
|
82
|
+
| File | Role |
|
|
83
|
+
|---|---|
|
|
84
|
+
| [`src/index.ts`](src/index.ts) | Public exports of every primitive and event helper |
|
|
85
|
+
| [`src/matcher.ts`](src/matcher.ts) | Match-all sentinels, literal-vs-regex mode, validation and runtime matching |
|
|
86
|
+
| [`src/runner.ts`](src/runner.ts) | `runHook` execution through `ctx.shell` and `DEFAULT_HOOK_TIMEOUT_MS` |
|
|
87
|
+
| [`src/codec.ts`](src/codec.ts) | Exit-code and structured-stdout decoding into `HookOutput` |
|
|
88
|
+
| [`src/merge.ts`](src/merge.ts) | Most-restrictive merge and the `MergedHookOutcome` type |
|
|
89
|
+
| [`src/events.ts`](src/events.ts) | `hook/*` event declaration, append helpers, stderr summary |
|
|
90
|
+
| [`src/detached.ts`](src/detached.ts) | Detached-run quiescence tracking |
|
|
91
|
+
| [`src/types.ts`](src/types.ts) | `HookOutput`, `MatcherGroup`, `CommandHook`, and the `hook/*` payload types |
|
|
92
|
+
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: pairing, turn enclosure, dialect, and duration checks |
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
-----
|
|
97
|
+
|
|
98
|
+
<a id="further-exploration"></a>
|
|
99
|
+
## Further Exploration
|
|
100
|
+
|
|
101
|
+
Read these pages when the package-level contract is not enough. They move from the shared rules to the bridges that apply them and the extension points they program against.
|
|
102
|
+
|
|
103
|
+
- [Hooks group map](../README.md) — the sibling group page and its package table.
|
|
104
|
+
- [Hook protocol library Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md) — why the protocol core is shared and what each bridge owns.
|
|
105
|
+
- [Hook bridges Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md) — how the two bridges use these primitives.
|
|
106
|
+
- [Interception extension-points Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md) — the typed-Decision surface the bridges map onto.
|
|
107
|
+
- [Generated persistence log event catalog](../../../docs/persistence-catalog.md) — the `hook/*` event payloads and per-event JSDoc.
|
|
108
|
+
|
|
109
|
+
-----
|
|
110
|
+
|
|
111
|
+
<a id="model-experience"></a>
|
|
34
112
|
## Model Experience
|
|
35
113
|
|
|
36
|
-
Indirectly, through `dsh-hooks-claude-code` and `dsh-hooks-codex`, which
|
|
114
|
+
Indirectly, through `dsh-hooks-claude-code` and `dsh-hooks-codex`, which are the only consumers that render decoded hook output into model context.
|
|
37
115
|
|
|
38
116
|
#### KV Cache effect
|
|
39
117
|
|
|
40
|
-
No direct invalidation; the named
|
|
118
|
+
No direct invalidation; the named consumers own any request-prefix changes.
|
|
41
119
|
|
|
42
120
|
## Known Limitations and Deferred Work
|
|
43
121
|
|
|
44
|
-
|
|
122
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
These limits describe what hooks cannot do through the shared engine yet. They are current package constraints, not a task backlog.
|
|
126
|
+
|
|
127
|
+
- **`HookOutput.updatedInput` is parsed but not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)); a bridge logs and warns when a hook sets it.
|
|
128
|
+
- **A folded halt has no run-level effect** — `mergeHookOutputs` folds `continue: false` into a sticky `stop`, but the interception points have no hard-halt primitive, so a bridge records the halt and keeps the hook's per-point effect.
|
|
129
|
+
- **Only the command-hook shape runs** — the protocol executes `{ type: 'command', command, timeout? }`; a bridge parses-and-skips the other shapes its dialect defines (`http`, `mcp_tool`, `prompt`, `agent`).
|
|
130
|
+
|
|
131
|
+
<a id="dev-note"></a>
|
|
132
|
+
### Dev Note
|
|
133
|
+
|
|
134
|
+
<details>
|
|
135
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
136
|
+
|
|
137
|
+
This Dev Note is working context for maintainers: open questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
|
|
138
|
+
|
|
139
|
+
#### Future: run-level halt
|
|
140
|
+
|
|
141
|
+
A hook that asks to halt the whole run (`continue: false`) is folded into `MergedHookOutcome.stop` but not applied anywhere: the interception points lack a hard-halt primitive, and mid-turn requests record the halt in `hook/result` instead. A run-level halt mechanism would let the bridges honor it; no design exists yet.
|
|
142
|
+
|
|
143
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,44 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Claude Code 与 Codex 桥接背后的共享钩子规则——钩子能做什么、运行时会发生什么——供 hooks 子系统的用户与维护者阅读。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-hook-protocol
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-hook-protocol` 让两个桥接以相同方式处理你的钩子:它定义钩子能做什么、运行时会发生什么。你无需自行安装或配置它——选择 `dsh-hooks-claude-code` 或 `dsh-hooks-codex`,把它指向你现有的 `hooks.json`,这些规则就会作用于你的钩子。通过任一桥接,钩子都可以带一条模型可见的消息阻塞提示词或工具调用、向对话附加额外上下文,或请求运行停止。只有 command 钩子会运行;`http`、`mcp_tool`、`prompt` 与 `agent` handler 会被跳过并给出警告。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
你无需直接安装或配置本包——挂载 `dsh-hooks-claude-code` 或 `dsh-hooks-codex` 就会把这些规则应用到你的 `hooks.json` 钩子上。用本页了解钩子能做什么、运行时会发生什么;两个桥接页面列出各方言支持的事件。
|
|
29
|
+
|
|
30
|
+
### 何时选择
|
|
31
|
+
|
|
32
|
+
当你持有现有的 Claude Code 或 Codex 钩子、希望它们在 agent(智能体)运行期间继续工作时,选择 `dsh-hooks-claude-code` 或 `dsh-hooks-codex`。你永远不会直接选择本包。没有参考工具对应物的定制行为请避开整个组:原生 Cordis 插件拥有完整的 harness API,无需中间的钩子协议。
|
|
33
|
+
|
|
34
|
+
### 钩子能做什么
|
|
35
|
+
|
|
36
|
+
- **带消息阻塞操作**——退出码为 2 的钩子会停止提示词或工具调用,其错误输出会作为原因展示。
|
|
37
|
+
- **工具运行前请求确认**——Claude Code 钩子可以请求确认而非直接阻塞;Codex 桥接不呈现此选项。
|
|
38
|
+
- **附加上下文**——钩子可以返回额外文本,模型会在下一次请求中看到。
|
|
39
|
+
- **在选定时刻运行**——钩子配置按名称或 pattern 选择触发的事件;缺失、空或 `'*'` pattern 表示该类的每个事件。
|
|
40
|
+
- **失败不停止运行**——除 2 以外的任何退出码都是非阻塞失败:操作继续,失败被记录;完全无法启动的钩子按同样方式处理。
|
|
41
|
+
- **请求运行停止**——钩子可以请求运行暂停(`{"continue": false}`);该请求会被记录,但没有运行级效果(见已知限制)。
|
|
42
|
+
|
|
43
|
+
### 钩子运行时你会看到什么
|
|
44
|
+
|
|
45
|
+
- 钩子阻塞时,操作不会发生,钩子的消息会被展示。
|
|
46
|
+
- 钩子附加上下文时,模型会在下一次请求中看到该文本。
|
|
47
|
+
- 失败的钩子——命令错误、崩溃或除 2 以外的任何退出码——会被记录,不会停止 agent。
|
|
48
|
+
- 如果钩子配置无法读取或解析,桥接会记录警告且不运行任何钩子;agent 仍会启动。
|
|
49
|
+
- 混合钩子类型的配置仍然可用:`http`、`mcp_tool`、`prompt` 与 `agent` handler 会被跳过并给出警告,其 command 钩子照常运行。
|
|
50
|
+
|
|
51
|
+
-----
|
|
52
|
+
|
|
53
|
+
<a id="understand-the-implementation"></a>
|
|
54
|
+
## 理解实现
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>实现细节——点击展开</summary>
|
|
58
|
+
|
|
59
|
+
本节解释本库背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
6
60
|
|
|
7
|
-
|
|
61
|
+
### 处理流水线
|
|
8
62
|
|
|
9
|
-
|
|
63
|
+
本库是一串单一用途的步骤,每个步骤一个函数:校验 matcher pattern、通过 `dsh-shell` 执行器运行命令、解码结果、把每个匹配 hook 的结果合并为最严格的一个结果,并记录持久的 `hook/*` 事件对。matcher 的 `mode` 参数是两个方言唯一的差异轴——`claude-code` 把 pattern 解释为字面量备选或正则,`codex` 始终解释为未锚定正则。每个步骤都会降级为受控结果而不是抛异常,因此钩子永远不会使调用轮次崩溃:无效正则是运行时的不匹配,执行器拒绝会变成没有退出码的 `HookOutput`,退出码 2 以 stderr 作为原因阻塞,其他失败均不阻塞。合并应用 `deny > ask > allow` 优先级,保持首个 `continue: false` 停止的粘性,并按 hook 顺序累积上下文。脱离运行会被跟踪,因此 `fiber.dispose()` 能达到完全停稳;不变式伴生插件会拒绝未开启轮次外的 `hook/*` 记录。这些步骤位于 [`src/matcher.ts`](src/matcher.ts)、[`src/runner.ts`](src/runner.ts)、[`src/codec.ts`](src/codec.ts)、[`src/merge.ts`](src/merge.ts)、[`src/events.ts`](src/events.ts)、[`src/detached.ts`](src/detached.ts) 与 [`src/invariant.ts`](src/invariant.ts)。
|
|
10
64
|
|
|
11
|
-
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| Matcher 校验与匹配判断 | `matcherDiagnostic(pattern, mode)` 用于解析时诊断;`matchesMatcher(pattern, query, mode)` 用于隔离的运行时匹配 | 选择自身的 `mode`(`claude` = 字面量或正则,`codex` = 始终使用正则),并拒绝带有诊断的配置组 |
|
|
14
|
-
| 运行 hook | `runHook(bash, hook, opts, now)`:通过 `ctx.shell` 提供 stdin payload + env,再解码 | 构造每个事件的 stdin **payload** + 该方言的 **env** |
|
|
15
|
-
| 解码输出 | `parseHookOutput(exit, stdout, stderr)` → 中性 `HookOutput` | 将中性 `HookOutput` 映射到扩展点特定的类型化 Decision |
|
|
16
|
-
| 合并 N 个 hook | `mergeHookOutputs(outputs)` → 最严格的 `MergedHookOutcome` | (无) |
|
|
17
|
-
| 持久记录 | `appendHookInvoked` / `appendHookResult`(`hook/*` 会话事件;结果的 `decision`/`stderrSummary` 从此处的 `HookOutput` 派生) | 在每次调用前后调用它们 |
|
|
18
|
-
| 脱离运行的完全停稳 | `createDetachedRuns()`:跟踪触发后不等待的运行链;`drain()` 先 abort,再等待它们 | 将 `signal` 传给每个脱离的 `runHook`,并将 `drain` 注册为 effect disposer |
|
|
65
|
+
### `hook/*` 会话事件
|
|
19
66
|
|
|
20
|
-
|
|
67
|
+
`hook/invoked` 与 `hook/result` 事件通过 declaration merging 合并进 `SessionEventMap`,作为仅日志记录:与 `compaction/*` 相同,它们不是 surface 事件,也不携带 `surfaceOp`。`hook/result` 按 `handlerId` 与其 `hook/invoked` 配对,决策规则由 `appendHookResult` 负责。载荷与逐事件 JSDoc 位于生成的[持久化日志事件目录](../../../docs/persistence-catalog.zh.md)中。
|
|
21
68
|
|
|
22
|
-
|
|
23
|
-
- **`runHook(bash, hook, options, now)`**:要求并转发调用方拥有的 `options.signal`,将 `options.payload` 序列化到 hook stdin(当且仅当 `options.trailingNewline` 时添加尾随换行符),在执行器凭证清理后合并 `options.env`(`dsh-shell` 受信任插件接口),遵循 hook 的 `timeoutSec`(否则使用 `options.defaultTimeoutMs`;默认值属于桥接,其配置默认为 lib 的 `DEFAULT_HOOK_TIMEOUT_MS` 10 分钟参考值),再解码结果(将 `options.expectedEventName` 传递给 codec)。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 `HookOutput`,其 `exitCode: undefined`(非阻塞错误)。`now` 会被注入,以便测试持续时间。
|
|
24
|
-
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** 解码退出状态与结构化 stdout。退出码为 2 时,会以 stderr 内容阻止执行;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
|
|
25
|
-
- **`mergeHookOutputs(outputs)`**:折叠在一个点上匹配的每个 hook 结果:权限优先级为 **deny > ask > allow**,从首个 `continue:false` 起,halt 状态保持不变,阻塞原因用 `\n\n` 连接,`additionalContext`/`systemMessages` 按顺序累积。
|
|
26
|
-
- **`createDetachedRuns()`**:跟踪以 emit 形式脱离运行的点是否完全停稳(没有扩展点等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,不会遗留任何可能作用于已 dispose(资源释放)的上下文的脱离 hook 工作(见 [防御模式](../../../docs/defensive-patterns.zh.md):dispose 必须达到完全停稳)。
|
|
69
|
+
调用与结果记录必须位于尚未结束的轮次内:`UserPromptSubmit`、`PreToolUse`、`PostToolUse` 与 `Stop` 按构造满足该关系,而 `SessionStart` 在轮次 1 之前运行、没有 `hook/*` 记录——改为投递其注入的上下文。不变式伴生插件注册到 `ctx.invariants`,拒绝在未开启轮次外追加的 `hook/*` 事件、没有匹配 invoked 的结果、未知方言或非有限时长。
|
|
27
70
|
|
|
28
|
-
|
|
71
|
+
### 设计理念
|
|
29
72
|
|
|
30
|
-
|
|
73
|
+
- **把唯一差异轴收拢进 `mode`。** 两个方言只在 matcher pattern 的解读方式上不同,因此 matcher 把 mode 作为参数,而不是复制引擎。
|
|
74
|
+
- **执行器拥有进程控制。** 命令通过 `dsh-shell` 执行器运行,而非自建 spawn:执行器已经提供了协议所需的已清理但可覆盖的环境、进程组取消与超时。
|
|
75
|
+
- **绝不向循环抛异常。** 每种失败模式——格式错误的 JSON、无效正则、执行器拒绝——都会降级为受控的结果或不匹配,因此钩子永远不能使调用轮次崩溃。
|
|
76
|
+
- **仅日志、轮次内的事件。** `hook/*` 记录是「运行了什么、决定了什么」的持久证据;它们不是 surface 事件,不变式伴生插件会拒绝未开启轮次外的记录。
|
|
31
77
|
|
|
32
|
-
|
|
78
|
+
[hook-protocol-lib Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.zh.md) 记录了共享与逐方言的划分以及备选方案。
|
|
33
79
|
|
|
80
|
+
### 源码地图
|
|
81
|
+
|
|
82
|
+
| 文件 | 职责 |
|
|
83
|
+
|---|---|
|
|
84
|
+
| [`src/index.ts`](src/index.ts) | 每个原语与事件辅助函数的公开导出 |
|
|
85
|
+
| [`src/matcher.ts`](src/matcher.ts) | 匹配全部哨兵、字面量或正则模式、校验与运行时匹配 |
|
|
86
|
+
| [`src/runner.ts`](src/runner.ts) | 通过 `ctx.shell` 的 `runHook` 执行与 `DEFAULT_HOOK_TIMEOUT_MS` |
|
|
87
|
+
| [`src/codec.ts`](src/codec.ts) | 退出码与结构化 stdout 解码为 `HookOutput` |
|
|
88
|
+
| [`src/merge.ts`](src/merge.ts) | 最严格合并与 `MergedHookOutcome` 类型 |
|
|
89
|
+
| [`src/events.ts`](src/events.ts) | `hook/*` 事件声明、追加辅助函数、stderr 摘要 |
|
|
90
|
+
| [`src/detached.ts`](src/detached.ts) | 脱离运行的完全停稳跟踪 |
|
|
91
|
+
| [`src/types.ts`](src/types.ts) | `HookOutput`、`MatcherGroup`、`CommandHook` 与 `hook/*` 载荷类型 |
|
|
92
|
+
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:配对、轮次包裹、方言与时长检查 |
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
-----
|
|
97
|
+
|
|
98
|
+
<a id="further-exploration"></a>
|
|
99
|
+
## 进一步探索
|
|
100
|
+
|
|
101
|
+
当包级约定不够用时阅读以下页面。它们从共享规则进入应用这些规则的桥接,以及它们所面向的扩展点。
|
|
102
|
+
|
|
103
|
+
- [hooks 组地图](../README.zh.md)——同级组页面及其包表。
|
|
104
|
+
- [hook-protocol-lib Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.zh.md)——协议核心为何共享、各桥接负责什么。
|
|
105
|
+
- [钩子桥接 Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md)——两个桥接如何使用这些原语。
|
|
106
|
+
- [拦截扩展点 Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md)——桥接所映射的类型化 Decision 接口面。
|
|
107
|
+
- [生成的持久化日志事件目录](../../../docs/persistence-catalog.zh.md)——`hook/*` 事件载荷与逐事件 JSDoc。
|
|
108
|
+
|
|
109
|
+
-----
|
|
110
|
+
|
|
111
|
+
<a id="model-experience"></a>
|
|
34
112
|
## 模型体验
|
|
35
113
|
|
|
36
|
-
通过 `dsh-hooks-claude-code` 与 `dsh-hooks-codex`
|
|
114
|
+
通过 `dsh-hooks-claude-code` 与 `dsh-hooks-codex` 间接影响;它们是将解码后的 hook 输出渲染为模型上下文的唯一消费方。
|
|
37
115
|
|
|
38
116
|
#### KV Cache 影响
|
|
39
117
|
|
|
40
118
|
不会直接失效;请求前缀变更由上述消费方负责。
|
|
41
119
|
|
|
42
|
-
##
|
|
120
|
+
## 已知限制与延期工作
|
|
121
|
+
|
|
122
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
这些限制描述钩子目前还无法通过共享引擎做到的事情。它们是当前包约束,而非任务积压。
|
|
126
|
+
|
|
127
|
+
- **`HookOutput.updatedInput` 会被解析但不会应用**——输入改写是已延期的设计一致性问题(见 [pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md));当 hook 设置它时,桥接会记录并警告。
|
|
128
|
+
- **折叠出的停止没有运行级效果**——`mergeHookOutputs` 把 `continue: false` 折叠为粘性 `stop`,但拦截点没有硬停止原语,因此桥接只记录该停止并保留 hook 的逐点效果。
|
|
129
|
+
- **只有 command 形态会运行**——协议只执行 `{ type: 'command', command, timeout? }`;桥接会解析并跳过其方言定义的其他形态(`http`、`mcp_tool`、`prompt`、`agent`)。
|
|
130
|
+
|
|
131
|
+
<a id="dev-note"></a>
|
|
132
|
+
### 开发备注
|
|
133
|
+
|
|
134
|
+
<details>
|
|
135
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
136
|
+
|
|
137
|
+
本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
|
|
138
|
+
|
|
139
|
+
#### 未来:运行级停止
|
|
140
|
+
|
|
141
|
+
请求停止整个运行的 hook(`continue: false`)会被折叠进 `MergedHookOutcome.stop`,但不会在任何地方生效:拦截点缺少硬停止原语,轮次中途的请求改为在 `hook/result` 中记录该停止。运行级停止机制可以让桥接真正应用它;目前尚无设计。
|
|
43
142
|
|
|
44
|
-
|
|
143
|
+
</details>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-hook-protocol",
|
|
3
3
|
"description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/dsh-shell": "^0.1.
|
|
36
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
37
|
-
"@deepseek-ai/
|
|
38
|
-
"@deepseek-ai/
|
|
35
|
+
"@deepseek-ai/dsh-shell": "^0.1.2-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
37
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
38
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@deepseek-ai/dsh-shell": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
44
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
41
|
+
"@deepseek-ai/dsh-shell": "^0.1.2-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
45
45
|
}
|
|
46
46
|
}
|