| 1 | /** |
| 2 | * Dialect-neutral vocabulary and log-only events shared by the Claude Code and |
| 3 | * Codex hook bridges. Payload construction, matching differences, environment, |
| 4 | * and extension-point-specific decision mapping remain owned by each bridge. |
| 5 | * @module @deepseek-ai/dsh-hook-protocol/types |
| 6 | */ |
| 7 | |
| 8 | /** |
| 9 | * The bridge that ran a hook — the CC bridge stamps `'claude-code'`, the Codex |
| 10 | * bridge `'codex'`. A native plugin at the interception points is not a bridge |
| 11 | * and writes no `hook/*` invocation/result records (see the interception extension-points Agent Note). |
| 12 | */ |
| 13 | export type HookDialect = 'claude-code' | 'codex' |
| 14 | |
| 15 | /** |
| 16 | * One configured command hook (the `{ type: 'command', command, timeout? }` |
| 17 | * shape shared by both dialects). Non-command hook types (CC's `prompt`/`agent`/ |
| 18 | * `http`) are parsed-and-skipped by a bridge, so only this shape reaches the |
| 19 | * runner. |
| 20 | */ |
| 21 | export interface CommandHook { |
| 22 | /** The shell command line to run. */ |
| 23 | command: string |
| 24 | /** Per-hook timeout in SECONDS (the wire unit); the runner converts to ms. */ |
| 25 | timeoutSec?: number |
| 26 | } |
| 27 | |
| 28 | /** |
| 29 | * One matcher group: a `matcher` pattern (absent / `''` / `'*'` = match-all) |
| 30 | * plus the command hooks that run when it matches. Both dialects share this |
| 31 | * shape (CC's `hooks.json` and Codex's `hooks.json`). |
| 32 | */ |
| 33 | export interface MatcherGroup { |
| 34 | matcher?: string |
| 35 | hooks: CommandHook[] |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * How a matcher pattern is interpreted. Claude Code uses {@link literal} when the |
| 40 | * pattern is purely `[A-Za-z0-9_|]+` (pipe = exact-match alternation) and |
| 41 | * {@link regex} otherwise; Codex is always {@link regex}. The bridge picks the |
| 42 | * mode for its dialect. |
| 43 | */ |
| 44 | export type MatcherMode = 'claude-code' | 'codex' |
| 45 | |
| 46 | /** |
| 47 | * The dialect-neutral OUTCOME a hook produced, parsed from its exit code + |
| 48 | * stdout JSON + stderr by {@link parseHookOutput}. A bridge maps this onto a |
| 49 | * extension-point-specific typed Decision (PreToolDecision, PreStepDecision, …). Every field |
| 50 | * is OPTIONAL because a hook may exercise any subset; the bridge decides which |
| 51 | * fields are meaningful for its hook point and which it ignores (faithful-but- |
| 52 | * degraded — e.g. Codex ignores `allow`/`ask`). |
| 53 | */ |
| 54 | export interface HookOutput { |
| 55 | /** The raw process exit code (`undefined` if the hook could not be run). */ |
| 56 | exitCode: number | undefined |
| 57 | /** Trimmed stderr — the block-reason source on a blocking (exit 2) hook. */ |
| 58 | stderr: string |
| 59 | /** |
| 60 | * Trimmed stdout, verbatim. On a clean exit a hook may emit PLAIN (non-JSON) |
| 61 | * stdout that the protocol renders as output (CC) or treats as |
| 62 | * `additionalContext` (Codex SessionStart/UserPromptSubmit) — so the bridge |
| 63 | * needs the raw text, not just the parsed structured fields. Empty string when |
| 64 | * the hook produced no stdout. |
| 65 | */ |
| 66 | stdout: string |
| 67 | /** |
| 68 | * `false` ⇒ the hook asked to halt (CC/Codex `continue:false`); pairs with |
| 69 | * {@link stopReason}. `true`/absent ⇒ proceed. |
| 70 | */ |
| 71 | continue?: boolean |
| 72 | /** Human-readable reason shown when {@link continue} is `false`. */ |
| 73 | stopReason?: string |
| 74 | /** |
| 75 | * The neutral blocking decision a hook expressed, folded from the two channels |
| 76 | * the reference protocols keep DISTINCT: the legacy top-level `decision` |
| 77 | * (`approve`/`block` only) and `hookSpecificOutput.permissionDecision` |
| 78 | * (`allow`/`deny`/`ask`). We normalize them to one enum — `'block'`/`'deny'` |
| 79 | * forbid, `'approve'`/`'allow'` permit, `'ask'` requests confirmation — but |
| 80 | * `'allow'`/`'deny'`/`'ask'` arise ONLY from a `permissionDecision`, never from |
| 81 | * a top-level `decision` (an out-of-band `{"decision":"deny"}` is invalid and |
| 82 | * ignored, matching the schemas). Absent ⇒ no explicit decision (exit code governs). |
| 83 | */ |
| 84 | decision?: 'approve' | 'allow' | 'block' | 'deny' | 'ask' |
| 85 | /** The reason/explanation accompanying {@link decision}. */ |
| 86 | reason?: string |
| 87 | /** |
| 88 | * Event discriminator claimed by `hookSpecificOutput`. On mismatch, |
| 89 | * {@link parseHookOutput} preserves this value but discards event-scoped fields. |
| 90 | */ |
| 91 | hookEventName?: string |
| 92 | /** Extra context to inject for the next model request (CC `additionalContext`). */ |
| 93 | additionalContext?: string |
| 94 | /** A warning surfaced to the user (CC `systemMessage`). */ |
| 95 | systemMessage?: string |
| 96 | /** |
| 97 | * A tool-input rewrite a hook requested (CC `updatedInput`). PARSED but NOT |
| 98 | * honored — input rewrite is deferred (see the interception extension-points Agent Note); a |
| 99 | * bridge logs + warns when this is present. |
| 100 | */ |
| 101 | updatedInput?: Record<string, unknown> |
| 102 | } |
| 103 |