Introduction¶
Writing hooks is the most common method to customize agent workflows: intercepting dangerous commands before tool execution, sending notifications when a task ends, or injecting context when a session starts. If you have written such declarations in Claude Code, Codex, or opencode, you will encounter a practical problem when switching to DeepSeek Harness (dsh): these configurations follow the original harness formats, which dsh cannot directly recognize. You have to rewrite them or manually synchronize them across multiple configurations.
The hooks-adapter introduced below addresses this problem: it reads existing hook configuration files, maps events from various platforms to dsh extension points, and allows the original declarations to continue running in dsh without rewriting.
What is it¶
hooks-adapter (v0.1.0, MIT license, author JohnXuXianyu22786) is positioned as a general hooks compatibility layer. It reads hook sections from .claude/settings.json, .codex/hooks.json, opencode.json, as well as native configurations (~/.config/hooks-adapter/hooks.json, <project>/.dsh-hooks.json). It maps lifecycle events from each harness to standard events (e.g., session:start, tool:before) and binds them to dsh extension points (e.g., agent/session-start, tools/pre-execute). It executes these bindings using four types of handlers: shell, webhook, oracle, and proxy.
A few design decisions are worth clarifying first:
- Config read-only, no migration: Existing hook declarations remain unchanged.
- Zero runtime dependencies: Requires Node >= 18, pure ESM + JSDoc types.
- Failure doesn’t block startup: Missing config files are skipped silently; if existing files have issues, only diagnostics are generated, never blocking dsh startup.
Core Features¶
Configuration Sources and Discovery¶
The plugin automatically discovers and merges 9 configuration file locations in a fixed order (global / project / local):
| Order | File | Dialect |
|---|---|---|
| 1 | ~/.claude/settings.json |
claude |
| 2 | ~/.codex/hooks.json |
codex |
| 3 | ~/.config/opencode/opencode.json |
opencode |
| 4 | ~/.config/hooks-adapter/hooks.json |
native |
| 5 | <project>/.claude/settings.json |
claude |
| 6 | <project>/.codex/hooks.json |
codex |
| 7 | <project>/opencode.json |
opencode |
| 8 | <project>/.dsh-hooks.json |
native |
| 9 | <project>/.claude/settings.local.json |
claude |
Merge rules: Same-named events are appended by the later-reading file; disableAllHooks follows the most specific file. Two environment variables can change behavior: HOOKS_ADAPTER_CONFIG (same as --config) and HOOKS_ADAPTER_HOME (same as --home). Configuration files must be strict JSON, comments are not allowed.
Four Types of Handlers¶
The type field in the configuration follows the conventions of each harness, normalized internally into four kinds:
| Config type | Internal kind | Behavior | Default timeout |
|---|---|---|---|
command |
shell |
Starts a shell process, feeding the JSON contract from stdin | 600s |
http |
webhook |
POSTs JSON to a URL, response body serves as decision | 600s |
prompt |
oracle |
Calls an LLM endpoint to evaluate, {ok:false} means reject |
30s |
agent / subagent |
proxy |
Delegates to a sub-agent runner (command is configurable) | 60s |
Timeout control and failure fallback strategies are built-in; the validate subcommand provides friendly config validation.
Event Mapping and Intercept Behavior¶
Event names from each harness are first mapped to standard events, then bound to dsh extension points:
| Standard Event | claude | codex | opencode | dsh Extension Point |
|---|---|---|---|---|
session:start |
SessionStart |
SessionStart |
session.created |
agent/session-start |
session:end |
SessionEnd |
SessionEnd |
session.deleted |
session/disposed |
prompt:submit |
UserPromptSubmit |
UserPromptSubmit |
chat.message |
agent/pre-step |
tool:before |
PreToolUse |
PreToolUse |
tool.execute.before |
tools/pre-execute |
tool:after |
PostToolUse / PostToolUseFailure |
PostToolUse |
tool.execute.after |
tools/post-execute |
turn:stop |
Stop |
Stop |
session.idle |
agent/turn-stopping |
subagent:start |
SubagentStart |
SubagentStart |
tool.execute.before.subagent |
subagent/start |
subagent:end |
SubagentStop |
SubagentStop |
tool.execute.after.subagent |
subagent/end |
notice |
Notification |
Notification |
notification |
manual / stdio |
compact:before |
PreCompact |
— | experimental.session.compacting |
manual / stdio |
Key behavioral points:
PreToolUseis an interception point: exit code 2, or returning JSONdecision: "block", will block tool execution (or force manual confirmation).PostToolUse/PostToolUseFailure(mutually exclusive triggers) can reject writing back results as feedback, or append context.UserPromptSubmit/SessionStart/Stop/SubagentStart/SubagentStop/SessionEndcan inject context, reject prompts, or force the model to continue.NotificationandPreCompact(corresponding tocompact:beforein the opencode dialect) only support manual / stdio triggers.
Three Integration Modes¶
- dsh plugin (Cordis
apply): Recommended way, takes effect automatically after installation. - stdio JSON-lines protocol: Any host can connect.
- One-time CLI:
validate,run,dump,list.
Installation and Enablement¶
Install from the GitHub repository:
dsh plugin --profile demo add github:JohnXuXianyu22786/hooks-adapter
This package is a dsh bundle (dsh.bundle.patch → cordis.patch.yml), adding it inserts into the plugin tree. Afterward, start dsh:
dsh --profile demo
The plugin will automatically discover hook configurations in the project directory and user directory. You can also install from a local checkout directory:
dsh plugin --profile demo add ./hooks-adapter
dsh --profile demo
To uninstall:
dsh plugin --profile demo remove hooks-adapter
Typical Usage¶
Reusing Existing Hook Declarations¶
Assume you already have such a declaration in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "guard.sh", "timeout": 10 }
]
}
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "notify-send done" } ] }
]
}
}
Meaning: Run guard.sh (timeout 10 seconds) before the Bash tool executes, intercept via exit code; trigger notify-send done on every Stop event. After installing the plugin, this configuration doesn’t need changing; it takes effect automatically when dsh runs next.
Overriding Plugin Configuration¶
When you need to adjust behavior, replace id: hooks-adapter in the profile’s cordis.patch.yml:
- replace:
- id: hooks-adapter
config:
configPath: /abs/path/to/hooks.json # Fix to a single file, skip discovery
discover: false
llm: { baseUrl: "https://api.example.com/v1", model: "eval-small" }
proxy: { command: "dsh run --quiet" }
configPath fixes a single configuration file and skips auto-discovery; discover: false turns off discovery; llm configures the LLM endpoint used by the oracle handler; proxy configures the command for the sub-agent runner delegated by the proxy handler.
stdio Protocol and One-time CLI¶
To reuse the same set of hook executors in a host program outside of dsh, using the stdio JSON-lines protocol:
echo '{"op":"ping"}' | node lib/index.js listen --config hooks.json
echo '{"op":"dispatch","event":"PreToolUse","payload":{"tool_name":"Bash","tool_input":{}}}' | node lib/index.js listen
Use the one-time CLI for debugging and checking. Validating first, then looking at the merge result, is a stable order:
node lib/index.js validate # Validate all discoverable configs, exit code 0/1
node lib/index.js run --event PreToolUse --payload payload.json
node lib/index.js dump # Print the merged effective config
node lib/index.js list # List discovered config files
After the above steps, you can confirm whether the configuration is correctly discovered and whether events trigger as expected without starting a full dsh session. More format details can be found in the docs/ and examples/ directories of the repository.
Applicable Scenarios and Notes¶
Suitable scenarios:
- Migrating from Claude Code / Codex / opencode to dsh, wanting to reuse existing hook logic as-is.
- Using multiple harnesses in parallel, wanting to maintain a single set of hook declarations instead of multiple.
- Wanting to embed the hook executor into your own host program (stdio protocol).
Notes before use:
- Requires Node >= 18; configuration files must be strict JSON, comments are not allowed.
NotificationandPreCompactin the opencode dialect only support manual / stdio triggers.- Security: The plugin runs with the permissions of the current dsh process; commands, webhooks, and LLM calls declared in hooks are also executed within this permission scope. Before installing any third-party plugins, it is recommended to read the source code and check the license (this project is MIT).
Conclusion¶
What hooks-adapter does isn’t complicated: it translates hook declarations from various harnesses into event bindings that dsh can execute. The configuration is read-only and unmodified; install and use, remove and revert. For developers migrating from other harnesses, it saves the cost of rewriting and synchronizing multiple configurations.
- GitHub: https://github.com/JohnXuXianyu22786/hooks-adapter
- Community Plugin Directory: https://www.skillhub.cn/plugins/JohnXuXianyu22786/hooks-adapter (Directory maintained by an independent community site, no official affiliation with DeepSeek / 幻方)