Passer au contenu principal
J

hooks-adapter

johnxu22786/hooks-adapter

Réutilise les hooks déclarés dans les configurations Claude Code, Codex et opencode sur dsh, en exécutant des gestionnaires shell, webhook, oracle et proxy sans aucune dépendance d'exécution.

Installer

dsh plugin --profile web add github:johnxu22786/hooks-adapter

README

简体中文

hooks-adapter

A hooks configuration compatibility layer for DeepSeek Harness (dsh): it reads existing hooks config files from mainstream agent harnesses (such as the hooks declarations in .claude/settings.json, .codex/hooks.json, and the hooks section of opencode.json), maps their lifecycle events to dsh extension points, and executes four kinds of handlers — shell / webhook / oracle / proxy — so the same hooks config can be reused as-is across different harnesses.

  • Zero runtime dependencies (Node ≥ 18, pure ESM + JSDoc types)
  • Config is read-only, not migrated: your existing hooks declarations stay unchanged
  • All four handler kinds supported: command execution, HTTP callbacks, LLM evaluation, subagent delegation
  • Timeout control, failure degradation policy, and friendly config validation (validate subcommand)
  • Three integration modes: dsh plugin (Cordis apply), stdio JSON-lines protocol (any host), one-shot CLI
hooks-adapter/
├── package.json        # dsh bundle manifest (dsh.bundle + exports)
├── cordis.patch.yml    # composite package layer: inserts this plugin into the plugin tree
├── dsh/plugin.js       # dsh entry: Cordis plugin (name + apply(ctx, config))
├── lib/                # runtime core (usable independently of dsh)
│   ├── index.js        # CLI entry + programmatic API exports
│   ├── events.js       # canonical event catalog + four-dialect mapping table + matcher semantics
│   ├── discover.js     # config file discovery (global/project/local)
│   ├── parse.js        # four-dialect parsers (all go through diagnostics, never throw)
│   ├── config.js       # runtime assembly: merging, disableAllHooks, defaults
│   ├── contract.js     # stdin JSON contract construction + response decoding + decision folding
│   ├── execute.js      # four-kind handler executor + timeout + process tree cleanup
│   ├── dispatch.js     # dispatch pipeline: matcher matching, ordered execution, blockable constraints
│   └── serve.js        # stdio JSON-lines protocol server
├── docs/               # config formats, event mapping, contract, integration notes, CLI guide
├── examples/           # four-dialect example configs + local mock LLM
└── test/               # node:test tests (111 items)

What It Can Do

Declare hooks in .claude/settings.json (no matter which harness you wrote them for) and they keep working in dsh:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "guard.sh", "timeout": 10 }
        ]
      }
    ],
    "Stop": [
      { "hooks": [ { "type": "command", "command": "notify-send done" } ] }
    ]
  }
}

Config files must be strict JSON (no comments); see examples/ for complete four-dialect examples.

  • PreToolUse → interception point before tool execution: handler exit code 2 / JSON decision: "block" will block the tool call (or turn it into ask for human confirmation)
  • PostToolUse / PostToolUseFailure → after tool execution (mutually exclusive triggers): reject the write-back as result feedback, append context
  • UserPromptSubmit / SessionStart / Stop / SubagentStart / SubagentStop / SessionEnd → inject context, reject prompts, force the model to continue
  • Notification / PreCompact → triggered manually or via the stdio protocol

The four handler kinds (the type field in config follows each harness's conventions; normalized internally):

Config typeInternal kindBehaviorDefault timeout
commandshellspawn a shell process, feed the JSON contract on stdin600s
httpwebhookPOST JSON to a URL, the response body is the decision600s
promptoraclecall an LLM endpoint to evaluate, {ok:false} rejects30s
agent / subagentproxydelegate to a subagent runner (configurable command)60s

Quick Start

# From a directory containing this plugin checkout
dsh plugin --profile demo add ./hooks-adapter
dsh --profile demo

After loading, the plugin automatically discovers hooks configs in project and user directories (see below). You can also override the config line in the profile's cordis.patch.yml:

- replace:
    - id: hooks-adapter
      config:
        configPath: /abs/path/to/hooks.json   # pin a single file (skip discovery)
        discover: false
        llm: { baseUrl: "https://api.example.com/v1", model: "eval-small" }
        proxy: { command: "dsh run --quiet" }

Integration details: docs/INTEGRATION.md.

Installing in DSH

Install directly from the GitHub repository with the dsh plugin command:

dsh plugin --profile demo add github:JohnXu22786/hooks-adapter

The package is a dsh bundle (dsh.bundle.patch → cordis.patch.yml); once added, it inserts itself into the plugin tree and automatically discovers hooks configs on the next dsh run. Remove it with:

dsh plugin --profile demo remove hooks-adapter

Mode two: stdio protocol (any host)

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

Protocol details: docs/CONTRACT.md.

Mode three: one-shot CLI

node lib/index.js validate            # check 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

Where the Config Comes From

Auto-discovered and merged in order (later files append groups for same-named events; disableAllHooks follows the most specific file):

OrderFileDialect
1~/.claude/settings.jsonclaude
2~/.codex/hooks.jsoncodex
3~/.config/opencode/opencode.jsonopencode
4~/.config/hooks-adapter/hooks.jsonnative
5<project>/.claude/settings.jsonclaude
6<project>/.codex/hooks.jsoncodex
7<project>/opencode.jsonopencode
8<project>/.dsh-hooks.jsonnative
9<project>/.claude/settings.local.jsonclaude
  • Environment variables HOOKS_ADAPTER_CONFIG (same as --config) and HOOKS_ADAPTER_HOME (same as --home)
  • Any missing file is silently skipped; if an existing file has issues, it only produces diagnostics, it never blocks startup
  • Config file format details: docs/CONFIG.md

Event Mapping

Every harness's event names map to a set of canonical events (session:start, tool:before, ...), which then bind to dsh extension points:

Canonical eventclaude dialectcodex dialectopencode dialectdsh extension point
session:startSessionStartSessionStartsession.createdagent/session-start
session:endSessionEndSessionEndsession.deletedsession/disposed
prompt:submitUserPromptSubmitUserPromptSubmitchat.messageagent/pre-step
tool:beforePreToolUsePreToolUsetool.execute.beforetools/pre-execute
tool:afterPostToolUse / PostToolUseFailurePostToolUsetool.execute.aftertools/post-execute
turn:stopStopStopsession.idleagent/turn-stopping
subagent:startSubagentStartSubagentStarttool.execute.before.subagentsubagent/start
subagent:endSubagentStopSubagentStoptool.execute.after.subagentsubagent/end
noticeNotificationNotificationnotificationmanual / stdio
compact:beforePreCompact—experimental.session.compactingmanual / stdio

Full semantics (blockability, matcher rules, payload fields): docs/EVENTS.md.

Contract

  • stdin JSON: session_id, transcript_path, cwd, hook_event_name, permission_mode + event fields (tool_name/tool_input/tool_use_id/tool_response/prompt/source ...)
  • Exit codes: 0 = allow (when stdout is JSON, the decision is parsed from it); 2 = block (stderr is the reason); any other non-zero = non-blocking error
  • stdout JSON: decision, continue/stopReason, systemMessage, hookSpecificOutput.permissionDecision (allow/deny/ask), additionalContext, updatedInput; oracle answers {ok: true|false, reason}
  • Multi-hook folding: deny > ask > allow; any continue:false stops; context accumulates in hook order
  • Details and the stdio protocol: docs/CONTRACT.md

Testing

node --test

(The default test-discovery mode runs all 111 tests; helper scripts live in test-support/ and are not mistaken for tests.)

License

Released under the MIT License.

Plugins associés