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 eventopencodecodexcursorkimi
PreToolUse (Write/Edit)nativeunsupportedadapter (Rule)adapter
PreToolUse (Bash)nativeunsupportedadapter (Rule)adapter
PostToolUsenativeunsupportedunsupportedadapter
UserPromptSubmitnativeunsupportedadapteradapter
SessionStartnativeunsupportedadapter (Rule)adapter
SubagentStopnativeunsupportedunsupportedadapter
Notificationnativeunsupportedunsupportedadapter
PreCompactnativeunsupportedunsupportedadapter
Stopnativeunsupportedunsupportedadapter

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:

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:

  1. cp <target> <target>.bak.{timestamp} before any change.
  2. Write the new content with a managed-by: agent-hook anchor.
  3. prune_backups(keep=5) so backups don't accumulate forever.
  4. On uninstall, check the anchor — refuse to delete entries that aren't ours (e.g. user-written plugins).

Test coverage

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.