Claude Code to dsh — DeepSeek Harness Guide
dsh ships an official compatibility plugin, @deepseek-ai/dsh-hooks-claude-code, which runs part of an existing Claude Code hooks.json against dsh’s own interception points. Two bugs in it will make a migrated hook stop firing with no error anywhere. If you use a PreToolUse hook to block dangerous commands, that means a security control quietly switching itself off, so both are worth checking before you trust the migration.
Enabling the compatibility layer
Section titled “Enabling the compatibility layer”Add the plugin to your cordis.yml, or to a patch layer (see Profiles, bundles & patches):
- dsh-hooks-claude-code: configPath: ./.claude/hooks.json pluginRoot: ./.claude/plugins/my-plugin # optional projectDir: . # optionalconfigPath is required and points at your existing Claude Code hooks.json, or at a settings file with a hooks key. Only shell-form command hooks execute. The http, mcp_tool, prompt, and agent types get parsed, then skipped with a warning. Config is read once at process load, and there’s no live reload or per-session discovery yet.
Seven of Claude Code’s 30 hook events are mapped: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStart, SubagentStop. All seven mappings are documented as partial, with the field-by-field gaps listed in the package README. Most of those gaps announce themselves. The two below don’t, which is what makes them worth your time.
Gotcha 1: matcher case sensitivity silently disables the hook
Section titled “Gotcha 1: matcher case sensitivity silently disables the hook”Claude Code’s hooks.json names tools in PascalCase: Bash, Read. dsh registers its tools in lowercase: bash, read. The matcher compares them with a case-sensitive equality check:
return pattern.split('|').includes(query)query is the tool name being called (bash) and pattern is what your hooks.json says (Bash). Since "bash" !== "Bash", .includes() returns false and the matcher loop moves on to the next hook. No error, no warning, nothing in the logs. A PreToolUse hook copied over from Claude Code to deny dangerous Bash calls never fires, and the call goes through as though you’d never written the hook.
Check this: open your migrated hooks.json and read every matcher value. Any PascalCase tool name in there (Bash, Read, Edit, Write, …) means that hook isn’t firing under dsh as of this writing.
Workaround: lowercase your matchers to the names dsh registers, bash in place of Bash and so on, until a fix lands.
A cherry-pickable fix was proposed and hadn’t merged as of the report. It makes literal matcher comparisons case-insensitive, so Bash matches bash, while leaving regex matchers like ^Bash$ case-sensitive and keeping Bash from matching BashOutput. Status at Discussion #582.
Gotcha 2: a zero or invalid timeout makes every hook fail open
Section titled “Gotcha 2: a zero or invalid timeout makes every hook fail open”defaultTimeoutMs: 0 passes the plugin’s own config validation. So does a negative number, or a fraction. The bash executor underneath rejects any timeoutMs <= 0, and runHook reads that rejection as an infrastructure fault, which means it fails open: the hook is skipped and the tool call it was gating runs anyway. For a PreToolUse hook whose whole job is denial, that’s a security bypass, and it doesn’t look like a disabled hook. It looks like nothing happened.
hooks-claude-code: configPath: ./hooks.json defaultTimeoutMs: 0 # <- every hook now fails open, silentlyThe same thing happens with a per-hook "timeout": 0 inside hooks.json itself.
You won’t hit this by accident on a default install. The built-in timeout is 10 minutes (600_000 ms), so something has to set 0 or another non-positive value explicitly. The realistic way in is templated or scripted hook config where a variable resolves to 0.
Check this: grep your hooks.json and any cordis.yml or patch config for timeout and defaultTimeoutMs. None of them should be 0, negative, or fractional.
Same situation as gotcha 1: a cherry-pickable fix exists, unmerged as of the report. It rejects non-positive defaultTimeoutMs at load time, the way the neighboring stderrSummaryMaxChars field already is, so a misconfiguration fails loudly instead of switching your protection off. Status at Discussion #583.
Source
Section titled “Source”@deepseek-ai/dsh-hooks-claude-codeREADME — official compatibility layer docs, config shape, and the full list of partial/unsupported hook events.- Discussion #582 — Claude hook matcher 大小写敏感,Bash 选不中 bash,安全 hook 静默失效
- Discussion #583 — defaultTimeoutMs: 0 使全部 hook fail-open,误配置应在加载时失败