Bundle
hooks-adapter
A universal hooks compatibility layer: run hooks declared in Claude Code / Codex / opencode / native configs on the DeepSeek Harness, with shell, webhook, oracle and proxy handlers.
- Source
- JohnXu22786
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 10 days ago
Readme
[简体中文](README.zh.md)
# 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:
```json
{
"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 type | Internal kind | Behavior | Default timeout |
| --- | --- | --- | --- |
| `command` | `shell` | spawn a shell process, feed the JSON contract on stdin | 600s |
| `http` | `webhook` | POST JSON to a URL, the response body is the decision | 600s |
| `prompt` | `oracle` | call an LLM endpoint to evaluate, `{ok:false}` rejects | 30s |
| `agent` / `subagent` | `proxy` | delegate to a subagent runner (configurable command) | 60s |
## Quick Start
### Mode one: dsh plugin (recommended)
```sh
# 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`:
```yaml
- 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](docs/INTEGRATION.md).
## Installing in DSH
Install directly from the GitHub repository with the dsh plugin command:
```sh
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:
```sh
dsh plugin --profile demo remove hooks-adapter
```
### Mode two: stdio protocol (any host)
```sh
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](docs/CONTRACT.md#stdio-协议).
### Mode three: one-shot CLI
```sh
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):
| 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 |
- 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](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 event | claude dialect | codex dialect | opencode dialect | 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 |
Full semantics (blockability, matcher rules, payload fields): [docs/EVENTS.md](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](docs/CONTRACT.md)
## Testing
```sh
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](LICENSE).
Install
dsh plugin --profile web add github:JohnXu22786/hooks-adapter
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install hooks-adapter from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.