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/mergingparseHookOutput → HookOutput; 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​