DOCUMENTATION
Architecture
One Python script · four client shapes · anchor + backup keep it reversible.
Top-down map
┌─────────────────────────────────────────────────────────────────────┐
│ Agent_hook (this repo) │
│ │
│ registry/<name>/ ← single source of truth │
│ ├── manifest.yaml ← schema-validated metadata │
│ ├── source/hook.py ← Python stdin/stdout/exit-2 │
│ └── tests/ │
│ │
│ agent/lib/ │
│ ├── manifest.py ← shared schema (byte-id 3 repos) │
│ ├── adapter_opencode.py ← JS plugin generator │
│ ├── adapter_codex.py ← always skip (no-op) │
│ ├── adapter_cursor.py ← degrade-to-Rule mapper │
│ ├── adapter_kimi.py ← config.toml hooks=[] writer │
│ └── cli.py ← agent-hook list/install/... │
│ │
└────────────────────┬───────────────────────────────────────────────-┘
│ writes (with cp .bak.{ts} + managed-by anchor)
▼
┌─────────────┴────────────┬──────────────┬──────────────────┐
▼ ▼ ▼ ▼
~/.config/opencode/ ~/.codex/ ~/.cursor/ ~/.kimi/
plugins/ (no-op) rules/ config.toml
agent-hook-<name>.js agent-hook-<name>.mdc hooks=[...]
The 4-client compatibility matrix
| Hook event | opencode | codex | cursor | kimi |
|---|---|---|---|---|
| PreToolUse (Write/Edit) | native | unsupported | adapter (Rule) | adapter |
| PreToolUse (Bash) | native | unsupported | adapter (Rule) | adapter |
| PostToolUse | native | unsupported | unsupported | adapter |
| UserPromptSubmit | native | unsupported | adapter | adapter |
| SessionStart | native | unsupported | adapter (Rule) | adapter |
| SubagentStop | native | unsupported | unsupported | adapter |
| Notification | native | unsupported | unsupported | adapter |
| PreCompact | native | unsupported | unsupported | adapter |
| Stop | native | unsupported | unsupported | adapter |
The opencode adapter (the most interesting)
For opencode we generate a JS plugin file at ~/.config/opencode/plugins/agent-hook-<name>.js. The JS plugin acts as a thin wrapper that spawns the real Python on every event, passing event JSON via stdin:
// managed-by: agent-hook · protect-sensitive-files · 2026-05-16T...
// Source: /Users/lute/project/Agent/Agent_hook/registry/protect-sensitive-files/source/hook.py
import { spawnSync } from "node:child_process";
const HOOK_NAME = "protect-sensitive-files";
const PYTHON = "/usr/bin/python3";
const HOOK_SCRIPT = "/Users/lute/.../registry/protect-sensitive-files/source/hook.py";
function callHook(event) {
const proc = spawnSync(PYTHON, [HOOK_SCRIPT], {
input: JSON.stringify(event), encoding: "utf8", timeout: 15000
});
return {
blocked: proc.status === 2,
payload: proc.stdout ? JSON.parse(proc.stdout) : null,
stderr: (proc.stderr || "").trim(),
};
}
export async function onPreToolUse({ tool, parameters }) {
if (!["Write","Edit","MultiEdit"].some(m => new RegExp(m).test(tool))) return null;
const res = callHook({ event: "PreToolUse", tool, parameters });
return res.blocked ? { block: true, reason: res.stderr } : res.payload;
}
The cursor adapter (degradation)
Cursor has no native hooks — only Rules (always-applied system prompts). We can express some hook behaviors as soft constraints, but never enforce them:
- Mappable to Rule: protect-sensitive-files (don't write <X>), guard-bash (don't run <Y>), session-context-injector (always remember <Z>).
- Cannot degrade: post-edit-format (Rule can't trigger a command), final-verify (Rule can't run code at Stop), pre-compact-save (no PreCompact concept in cursor), subagent-acceptance (no subagent return-value parsing), notify-on-idle (no Notification event).
The kimi adapter
kimi has a hooks=[] array in ~/.kimi/config.toml. We append entries like:
[[hooks]]
name = "protect-sensitive-files"
events = ["PreToolUse"]
matchers = ["Write", "Edit", "MultiEdit"]
command = ["/usr/bin/python3", "/path/to/registry/protect-sensitive-files/source/hook.py"]
managed_by = "agent-hook"
Reversibility guarantees
Every write to a client config follows the same protocol:
cp <target> <target>.bak.{timestamp}before any change.- Write the new content with a
managed-by: agent-hookanchor. prune_backups(keep=5)so backups don't accumulate forever.- On uninstall, check the anchor — refuse to delete entries that aren't ours (e.g. user-written plugins).
Test coverage
- 28 schema tests — manifest validation, required fields, hook_events whitelist, secret-literal rejection.
- 39 behavior tests — every hook tested end-to-end with stdin/stdout via CLI subprocess.
- 14 adapter tests — install / uninstall round-trips for each of the 4 adapters, sibling protection, anchor refusal.
- Total: 81 tests, all green.
Why this shape
Prompt-only enforcement fails when the LLM thinks it knows better. Hook code that the runtime is forced to call before the destructive action — and that returns exit 2 — is the only deterministic mechanism. The bet of Agent_hook is that every team will arrive at this same conclusion, and we can save them the boilerplate.