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:

  • PreToolUse is an interception point: exit code 2, or returning JSON decision: "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 / SessionEnd can inject context, reject prompts, or force the model to continue.
  • Notification and PreCompact (corresponding to compact:before in the opencode dialect) only support manual / stdio triggers.

Three Integration Modes

  1. dsh plugin (Cordis apply): Recommended way, takes effect automatically after installation.
  2. stdio JSON-lines protocol: Any host can connect.
  3. 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.patchcordis.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.
  • Notification and PreCompact in 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 / 幻方)