Skip to main content
PathDocs

Hooks Bridge (Claude / Codex)

In one sentence: DSH's core extension surface is typed interception points (native plugins suffice); hooks-claude-code/hooks-codex are bridge plugins: they run your existing Claude Code / Codex hooks.json shell hooks verbatim on those same interception points, keeping compatibility with the legacy ecosystem.

If you used Claude Code / Codex to configure a bunch of shell hooks (pre-commit, gates, notifications…), and you do not want to rewrite them for DSH, dsh-hooks-* lets them keep running.

1. Two concepts, don't conflate them

Native plugin (recommended)Hooks bridge (compatibility)
What it isAn ordinary Cordis plugin mounted on DSH's typed interception pointsA bridge translating the "external shell-hook protocol" to those same interception points
ExperienceTyped returns, no serialization boundaryGoes through subprocess shell hooks
When to useNew extensionsExisting Claude/Codex hook configs you want to keep compatible

Source quote: native plugins can do everything the bridge does, more powerfully. The bridge exists only as a compatibility path; anything you write fresh should be a native plugin.

2. Packages

PackageRole
dsh-hook-protocolShared shell-hook wire protocol (matcher, stdlib codec, ctx.shell execution, hook/* events)
dsh-hooks-claude-codeClaude Code hook bridge (CC dialect: stdin payload, ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} substitution)
dsh-hooks-codexCodex hook bridge (Codex dialect)

hook-protocol is not a plugin: it registers and injects nothing; it is just a library shared by both bridges.

3. Configuration: connecting a Claude hooks.json

# cordis.yml / patch
- id: hooks-claude-code
name: '@deepseek-ai/dsh-hooks-claude-code'
config:
configPath: ./.claude/hooks.json # required: hooks.json or a settings with a hooks key
pluginRoot: ./.claude/plugins/my # optional: substitutes ${CLAUDE_PLUGIN_ROOT}
projectDir: . # optional: substitutes ${CLAUDE_PROJECT_DIR}; default = session cwd
defaultTimeoutMs: 600_000 # optional: the default when a hook sets no timeout (CC default)
stderrSummaryMaxChars: 500 # optional: char cap for the stderr summary persisted in hook/result events

The Codex bridge config has the same shape, with one extra optional model (stamped on each stdin payload); configPath usually points to ./.codex/hooks.json.

  • Supports the command-hook subset that CC/Codex support; mapping: hook-neutral results → harness typed Decisions (allow/deny/ask, etc.); hook/* session events record hook executions
  • Config parses once at mount time; configPath is process-level (relative paths resolve against the startup cwd, with no per-session discovery); read/parse failures are isolated (logged, zero hooks registered)
  • Only type: 'command' shell hooks run; handlers like http/mcp_tool/prompt/agent are parsed then skipped with a warning

4. Hook protocol shape

The two bridges share one set of protocol primitives, each handling only its dialect differences:

ConcernShared library (dsh-hook-protocol)Bridge (-claude / -codex)
Matcher validation/matchingmatcherDiagnostic / matchesMatcherchooses mode: claude = literal or regex, codex = always regex
Running hooksrunHook: stdin payload + env via ctx.shellbuilds each event's stdin payload + dialect env
Decoding/mergingparseHookOutputHookOutput; mergeHookOutputs → most-restrictive resultmaps neutral results to interception-point typed Decisions
Persistent recordinghook/invoked / hook/result session eventsinvokes them around each call

Key semantics:

  • exit code 2 = blocking (with stderr); other failures are non-blocking errors.
  • Merging takes the most restrictive: deny > ask > allow; the first continue:false sticks; additionalContext/systemMessages accumulate in order.
  • matcher: claude mode treats pure [A-Za-z0-9_|]+ as literal (with | exact alternation), everything else as regex; codex mode is always unanchored regex.
  • hook/* events are log-only (like compaction/*), not SurfaceEventType: the paired hook/invoked and hook/result, stderr summary truncated at stderrSummaryMaxChars.

5. Interception point → decision mapping

The Claude bridge maps CC hooks to DSH interception points:

CC hookDSH interception pointMapping
SessionStartagent/session-start (emit)additionalContext → agent.inject() into the new session
UserPromptSubmitagent/pre-step (waterfall)deny → reject; additionalContext only → next() delegation then appended context
PreToolUsetools/pre-execute (waterfall)deny → deny; ask → ask
PostToolUsetools/post-execute (waterfall)deny → block + feedback
Stopagent/turn-stopping (serial)a blocking Stop hard-pushes the next step via steer()
SubagentStartsubagent/start (emit)additionalContext → injects into the live in-process subagent
SubagentStopsubagent/end (emit)observe only

The Codex bridge implements 5 of the 10 points (PreToolUse/PostToolUse/SessionStart/UserPromptSubmit/Stop), using block (exit 2) in place of deny, with no allow/ask for PreToolUse.

  • Emit points (SessionStart/SubagentStart/SubagentStop) run in isolation; no interception point is waiting on them; the run chain is traced, and on dispose it first aborts still-running hook processes, then drains continuations.
  • Matcher bodies: tool names (PreToolUse/PostToolUse), session source (SessionStart), the constant agent_type=general-purpose (SubagentStart/SubagentStop); UserPromptSubmit/Stop ignore the matcher.
  • Multiple hooks on the same interception point execute serially in config order and fold to the most restrictive.

6. Difference from tools gating

The two ultimately land on the same interception points, but don't conflate them (consistent with listening to events):

Native gating (recommended)Hooks bridge
Implementationctx.on('tools/pre-execute', ...) returns PreToolDecisiontranslates an external shell hook's stdout/exit code into the same decision
Typetyped return values, no serialization boundaryserialization through subprocess + stdin/stdout
When to useconfiguring new gates/interceptions on DSHexisting Claude/Codex hooks.json you want to keep compatible

7. When to use which

ScenarioUse
You already have a large set of Claude/Codex hooks and want a seamless migrationhooks bridge
Configuring new gates/interceptions on DSHnative plugins (tools pipeline, agent events)
Wiring up @dsh-external community hooksinstall as a plugin

8. Verification

# check whether the hooks bridge is mounted
dsh web --dump-config | grep -iE "hook"
# see hook executions in the session
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"hook/' | head

Next steps