Round-1 Codex review findings on the bridges: - Stop force-continue (both bridges): a blocking Stop hook with EMPTY stderr yielded decision 'deny' + reason undefined, and the `&& reason !== undefined` guard let the turn STOP — the opposite of a blocking Stop hook. Force-continue on any deny; fall back to a generic steering line when there is no reason. - Codex payload tool_name: hardcoded "Bash" disagreed with the exec.name matcher subject, so a real Codex `matcher:"Bash"` never fired against the harness's lowercase `bash` tool. Use exec.name in both payload builders (matches the matcher subject and the sibling CC bridge). Doc/RFC updated. - Codex plain-stdout context: SessionStart/UserPromptSubmit are documented to treat a clean hook's PLAIN (non-JSON) stdout as additionalContext, but nothing folded it. runPoint now folds plain stdout into context for those two events, gated on the codec's JSON gate so structured stdout is never dumped as prose. - continue:false is deferred, not honored: the seams have no hard-halt primitive yet. TODO(hook-continue-false) at both bridges + an RFC deferred note; the two tests now assert the LOG records the halt request AND that the run is NOT actually halted (no longer misleading). - README concurrency wording: hooks run SERIALLY (deliberate — adjacent invoked/result log pairs, order-independent fold), not concurrently. Fixed the CC README claim + an RFC note. Regression guards proven red on the unfixed code, then reverted. The mismatched- hookEventName discard (also flagged) is fixed in dsh-hook-protocol and merged down.
@deepseek-ai/dsh-hooks-claude
A cordis plugin that runs a user's existing Claude Code hook config (a hooks.json, or a settings file's hooks key) on the harness's canonical interception seams. It is the CC dialect half of the hooks subsystem: it owns CC's per-event stdin payloads, CC's env + ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} substitution, and the mapping from a hook's neutral outcome onto the harness's typed Decisions. The dialect-agnostic primitives (matcher, exit-code/stdout codec, ctx.bash execution, most-restrictive merge, the hook/* events) come from @deepseek-ai/dsh-hook-protocol.
A native cordis plugin could do everything this bridge does — more powerfully, with typed returns and no serialization boundary. The bridge exists only to run UNMODIFIED external CC hooks faithfully; anything bespoke should be a native plugin on the same seams (see the interception-seams RFC).
Config
import type { Config } from '@deepseek-ai/dsh-hooks-claude'
const config: Config = {
configPath: '/path/to/hooks.json', // required: a hooks.json or a settings file with a `hooks` key
pluginRoot: '/path/to/plugin', // optional: replaces ${CLAUDE_PLUGIN_ROOT} in command strings
projectDir: '/path/to/project', // optional: replaces ${CLAUDE_PROJECT_DIR} AND set as the hook env var
defaultTimeoutMs: 600_000, // optional: per-hook timeout when a hook sets none (CC default)
}
In a cordis.yml:
- dsh-hooks-claude:
configPath: ./.claude/hooks.json
pluginRoot: ./.claude/plugins/my-plugin
projectDir: .
The config is parsed once at load. A read/parse failure is contained — the bridge logs a warning and registers nothing rather than crashing boot (a typo'd path must not take the agent down). Only type: 'command' hooks run; a prompt/agent/HTTP hook is parsed-and-skipped with a warning.
Hook points → seam Decisions
| CC hook | Harness seam | Mapping |
|---|---|---|
SessionStart |
agent/session-start (emit) |
additionalContext → agent.inject() into the new session (cannot block) |
UserPromptSubmit |
agent/prompt-submit (waterfall) |
deny → PromptDecision.block; additionalContext → allow with context |
PreToolUse |
tools/pre-execute (waterfall) |
deny → PreToolDecision.deny; ask → PreToolDecision.ask |
PostToolUse |
tools/post-execute (waterfall) |
deny → block with feedback; additionalContext → accept with context |
Stop |
agent/turn-continuation (waterfall) |
a blocking Stop hook forces continue, feeding its reason as next-step steering |
SubagentStart |
subagent/start (emit) |
additionalContext → agent.inject() into the live child |
SubagentStop |
subagent/end (emit) |
observe-only |
The matcher subject is the tool name (PreToolUse/PostToolUse), the session source (SessionStart), or the child's agent type (SubagentStart/SubagentStop); UserPromptSubmit/Stop ignore matchers. Multiple file-configured hooks on one point run serially, in config order, and fold most-restrictively (deny > ask > allow, see dsh-hook-protocol); serial keeps each hook's hook/invoked/hook/result pair adjacent in the log, and the fold is order-independent for the decision (see the RFC's "run serially, not concurrently" note).
Context source
Injected context carries an explicit { kind: 'plugin', plugin: 'hooks-claude' } source. agent.inject() defaults a missing source to { kind: 'user' }, which would mislabel plugin context as a user prompt — so the bridge always names itself.
Deferred (faithful-but-degraded)
updatedInput(tool-input rewrite) is logged + warned, not honored — input rewrite is a deferred consistency-design problem (the pre-tool-input-rewrite RFC).- Stop loop-guard. CC breaks an infinite force-continue with
stop_hook_active(true once a Stop hook has fired this run) plus a max-consecutive cap; both are deferred (TODO(stop-loop-guard)). Todaystop_hook_activeis alwaysfalse, so a Stop hook that unconditionally blocks would force-continue every step — a hook author must self-limit until the guard lands.