For agents, on top of README.md, which they read first.
Notes for agents
jevhooks.js is generated: never edit it. just mod writes it from
crates/jevhooks-mod (cargo wasm32 build, wasm-opt -Oz, wasm2js -O2). Rebuild and
commit it with any change to jevhooks-mod or jevhooks-events, or the plugin runs
the old routing. Update jevhooks.d.ts by hand with any change to the crate's abi
exports.
Memory is touched only in bridge.ts. wasm2js swaps the module's ArrayBuffer
whenever memory grows, so a Uint8Array kept across a call into Rust can point at
storage the module has already replaced. Never export memory, or a view over it,
from bridge.ts; take every view from memory.buffer at the moment it is used.
$ calls live in top-level functions of register.tsx. Mods check $ calls from
the module source; a call written in a hook, or in a top-level function given $,
passes (claude plugin validate reports it "via" that function).
Hooks beneath this plugin still run. classic.PreToolUse and classic.Stop call
next(e) and combine: a deny beneath wins, then an ask, then this plugin's allow.
Returning without next would skip the user's own settings hooks.
The mod reaches the daemon with $.http.fetch and socketPath, not $.mcp.call.
$.mcp.call is an ordinary tool call: it goes through tool.call and the permission
check (refused outside bypass mode, whatever the type docs say), raises the plugin's
own hooks, and measured about 60 ms against 4 to 13.
socketPath must stay the path crates/jevhooks-daemon/src/paths.rs computes
($XDG_STATE_HOME, else $HOME/.local/state, then jevhooks/daemon.sock). Change
one and the other, or every event finds no daemon and silently passes.
observe never rejects and is never awaited. Nothing waits on it, so nothing
could handle a rejection; keep the try/catch and the void.
Every failure to reach the daemon is undefined, which means pass. Do not turn a
missing socket into an error a hook throws.