1# Chapter 3: hooks, the mod that sits in the doorway 2 3This is the part of jevhooks that runs inside Claude Code. It is a 4**mod**: a TypeScript module whose `register` function is handed `on`, and 5calls `on(event, handler)` for each event it wants to hear. If you have 6written Express or Koa middleware you already know the shape, with one 7twist: 8 9```ts 10on('classic.Stop', async ($, e, next) => { 11 const [decision, beneath] = await Promise.all([decide($, 'Stop', ...), next(e)]) 12 ... 13 return beneath 14}) 15``` 16 17`next(e)` runs whatever sits beneath this hook (other plugins, and the 18hooks in your own Claude Code settings) and **hands back their answer**. 19So the mod never replaces your hooks. It runs them alongside its own 20question and then combines the two, and the stricter answer wins. The 21`$` is the mod's only window on the world: `$.http.fetch`, `$.env.get`, 22`$.session.id`, `$.ui.log`, `$.clock.sleep`. A mod has no Node, no DOM, no 23`setTimeout`. 24 25[register.tsx](register.tsx) registers six hooks: 26 27| Hook | What it does | 28| --- | --- | 29| `session.start` | Registers the `/jev` command. | 30| `command.run` for `jev` | Prints the daemon's status and its last ten records. | 31| `classic.PreToolUse` | Asks the routing whether to judge this tool call; if so, sends it to the daemon and waits. | 32| `classic.Stop` | Sends the turn's end to the daemon and waits. | 33| `classic.*` | Every other event: asks the routing whether the daemon needs to hear of it, and if so sends it without waiting. | 34| `ui.render` for `AbovePrompt` | Draws the band above the prompt: the last decision. | 35 36## Combining verdicts 37 38The daemon answers a deciding event with a verdict: `allow`, `ask` or 39`pass`. Here is how `classic.PreToolUse` folds that into what the hooks 40beneath said: 41 42| Beneath says | Jev says `allow` | Jev says `ask` | Jev says `pass`, or no answer | 43| --- | --- | --- | --- | 44| deny | deny | deny | deny | 45| ask | ask, with their reason | ask, with their reason | ask | 46| nothing, or allow | allow | ask, with Jev's line | whatever beneath said | 47 48A deny beneath always wins; then any ask; then this plugin's allow. A 49`classic.Stop` that Jev judges "stopped early" becomes a `block` with Jev's 50line and "Finish what was asked, or say plainly what is stopping you.", 51unless something beneath already blocked. 52 53> **Aside: the 2.5 second rule.** The mod races every request to the 54> daemon against `$.clock.sleep(2500)`. The daemon gives up on Jev after 55> 1.5 s; this second limit is for a daemon that is itself wedged. If the 56> clock wins, the mod acts as if it were not installed. A slow judge is 57> never allowed to become a slow assistant. 58 59## Reaching the daemon 60 61`daemon()` calls `$.http.fetch('http://jevhooks/decide', { socketPath, ... })`. 62The host name is decoration; `socketPath` is what matters. It is 63`$XDG_STATE_HOME/jevhooks/daemon.sock` (or `~/.local/state/jevhooks/daemon.sock`), 64the same path the daemon's own `paths.rs` computes from the same two 65variables. Every failure, a 66missing socket or a refusal, comes back as `undefined`, which every caller 67treats as "pass". 68 69Every event sent carries an envelope: the session id, the event's name, its 70own fields, the working directory and the project root. 71 72## The bridge to Rust 73 74The routing decision ("judge this? report this? ignore it?") is written in 75Rust (chapter 10) and compiled into [jevhooks.js](jevhooks.js). Talking to 76compiled Rust means talking through its memory: a flat array of bytes. 77[bridge.ts](bridge.ts) does that in one function, `route()`: 78 79```mermaid 80sequenceDiagram 81 participant B as bridge.ts route() 82 participant W as jevhooks.js (Rust, as JS) 83 B->>W: alloc(len): somewhere to put the question 84 B->>W: write the JSON bytes into memory.buffer 85 B->>W: handle(ptr, len) 86 W-->>B: a pointer to the answer 87 B->>W: out_len(): how long it is 88 B->>B: read and JSON.parse the answer 89 B->>W: dealloc both 90``` 91 92> **Aside: why memory is touched in exactly one file.** When the Rust side 93> needs more memory, wasm2js replaces the whole `ArrayBuffer` behind 94> `memory.buffer`. A `Uint8Array` made before that still points at the old 95> one, and reads garbage without complaint. So `bridge.ts` takes a fresh 96> view from `memory.buffer` at the moment it reads or writes, and exports 97> only `route()`: nothing outside it can hold a view at all. 98 99> **Try it.** In a session (`claude --plugin-dir ./plugin`, with the daemon 100> built), ask the assistant to run `git status`, then `cargo build`. The 101> first never reaches the daemon (the routing calls it plainly read-only), 102> so the band does not change. The second does: the band shows a line such 103> as `command: Jev: builds or tests (97%); undone by one command (1.0 of 3); 104> allowed`, the milliseconds it took, and the session's cost so far. Then 105> type `/jev`. 106 107## For the people who maintain it 108 109### In this folder 110 111| Path | What | 112| --- | --- | 113| [hooks.json](hooks.json) | Tells Claude Code which modules to load: `./register.tsx`. | 114| [register.tsx](register.tsx) | The hooks, the band, and `/jev`. | 115| [bridge.ts](bridge.ts) | The only code that touches the compiled module's memory; exports `route()` and the `Role` type. | 116| [jevhooks.js](jevhooks.js) | **Generated** by `just mod` from `crates/jevhooks-mod` (chapter 9): wasm2js output, about 21,700 lines. Committed, because the plugin loads it; never edited by hand. Exports `memory`, `alloc`, `dealloc`, `handle`, `out_len`. | 117| [jevhooks.d.ts](jevhooks.d.ts) | Types for `jevhooks.js`'s exports, kept by hand to match the crate's `abi` module. | 118 119The events the generic `classic.*` hook reports are whatever the routing 120calls `observe` (chapter 8): `UserPromptSubmit`, `PostToolUse`, 121`PostToolUseFailure`, `PermissionDenied` and `SessionEnd`. It reads the 122settings-hook field names (`tool_name`, `tool_input.command`) and sends 123`prompt`, `source` and `tool_use_id`. 124 125The band is hidden while a survey is showing and before the first 126decision; it is yellow for an `ask` and dim otherwise. 127 128← Previous: [Chapter 2, .claude-plugin/](../.claude-plugin/) · Up: [plugin](../) · Next: [Chapter 4, types/](../types/) →