Skip to content

The hooks subsystem lets external CLI agents — Claude Code and Codex — participate in dsh's agent loop through their native command-hook mechanisms. A bridge plugin reads the dialect's hooks.json matcher groups, executes matched hooks through ctx.shell, decodes their exit-code/stdout contract into a neutral shape, merges the results most-restrictively, and records everything as durable hook/* session events.

PackageRole
@deepseek-ai/dsh-hook-protocolDialect-neutral protocol library (NOT a plugin)
@deepseek-ai/dsh-hooks-claude-codeClaude Code hook bridge plugin
@deepseek-ai/dsh-hooks-codexCodex hook bridge plugin

The shared protocol library

packages/hooks/hook-protocol is the shared core of the Claude Code / Codex hook wire protocol. It is not a Cordis plugin — it registers nothing and injects nothing. It is a library of dialect-neutral primitives the two bridge plugins import so neither re-implements the identical halves of the protocol. Codex deliberately reimplements a subset of the Claude Code hook protocol — the same hooks.json matcher-group shape, the same exit-code/stdout output contract, the same command-hook execution model — so the genuinely shared parts live here and each bridge owns only what differs.

From the README's split table:

ConcernHere (dsh-hook-protocol)The bridge
Matcher validation + testmatcherDiagnostic(pattern, mode), matchesMatcher(pattern, query, mode)picks its mode (claude-code = literal-or-regex, codex = always regex), rejects config groups carrying a diagnostic
Run a hookrunHook(bash, hook, opts, now) — stdin payload + env via ctx.shell, decodebuilds the per-event stdin payload + the dialect's env
Decode outputparseHookOutput(exit, stdout, stderr) → neutral HookOutputmaps HookOutput onto an extension-point-specific typed Decision
Merge N hooksmergeHookOutputs(outputs) → most-restrictive MergedHookOutcome
Durable recordappendHookInvoked / appendHookResult (hook/* session events)calls them around each invocation
Detached runscreateDetachedRuns() — track fire-and-forget chains; drain() aborts then awaitspasses signal to each detached runHook, registers drain as its effect disposer

The library lives in packages/hooks/hook-protocol/src/matcher.ts (pattern diagnostics and matching), runner.ts (hook execution via a ShellExecutor-shaped bash argument), merge.ts (most-restrictive merging), events.ts (the hook/* event shapes), codec.ts, detached.ts (quiescence tracking), and types.ts.

The bridge plugins

dsh-hooks-claude-code and dsh-hooks-codex are thin adapters: each owns its dialect's config (src/config.ts), the per-event stdin payload and environment, and the mapping from neutral HookOutput to the typed decision its extension point expects. Matcher mode differs by design — Claude Code treats a matcher as literal-or-regex, Codex always as regex — and a config group carrying a diagnostic is rejected by whichever bridge owns it.

Configuration (both bridges, from src/config.ts) drives: which hook events are enabled, the matcher groups, and the command lines executed. Hook execution goes through ctx.shell, so a hook command inherits the sandbox/policy world of the session — including whichever shell provider is composed (see Shell & Terminal).

How a hook decision flows

text
agent loop reaches an extension point (e.g. PreToolUse / Stop)


bridge plugin builds stdin payload + env for its dialect


runHook(bash, hook, opts) ──► ctx.shell executes the hook command


parseHookOutput(exit, stdout, stderr) ──► neutral HookOutput


bridge maps HookOutput ──► typed Decision (allow / deny / modify ...)


mergeHookOutputs across hooks ──► most-restrictive MergedHookOutcome


appendHookInvoked / appendHookResult ──► durable hook/* session events

Detached (fire-and-forget) runs are tracked by createDetachedRuns() so process teardown can drain() them — abort, then await — instead of leaking chains.

Why this matters for dsh

The hooks subsystem is one of the ways dsh interoperates with the broader agent ecosystem: instead of reimplementing Claude Code or Codex, a dsh composition can host their hook protocol and let their command hooks act on dsh sessions. It is the mirror image of the subagent backends (subagent-claude-code, subagent-codex) that run those CLIs as agents (see Subagents) — there the CLI is the worker, here its hook contract is the extension point.

Key source files

Repo-relative pathWhat it provides
packages/hooks/hook-protocol/src/matcher.tsMatcher diagnostics + matching
packages/hooks/hook-protocol/src/runner.tsrunHook via ctx.shell
packages/hooks/hook-protocol/src/merge.tsmergeHookOutputs
packages/hooks/hook-protocol/src/events.tshook/* session events
packages/hooks/hooks-claude-code/src/config.tsClaude Code bridge config
packages/hooks/hooks-codex/src/config.tsCodex bridge config

Further reading

  • Subagents — the other direction: running Claude Code / Codex as subagents
  • Shell & Terminalctx.shell, the execution world hooks run in
  • Session Management — where hook/* events land in the log
  • Repo: packages/hooks/hook-protocol/README.md, packages/hooks/hooks-claude-code/README.md, packages/hooks/hooks-codex/README.md