jevhooks.git / plugin / hooks / CLAUDE.md

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.