1# Chapter 10: jevhooks-mod/src, routing and a filter that is wrong in only one direction 2 3Every event Claude Code raises comes through here first, as a little JSON 4question: `{ "event": "PreToolUse", "tool": "Bash", "command": "git status" }`. 5The answer is one of three roles, from chapter 8: **decide** (send it to the 6daemon and wait), **observe** (send it and do not wait) or **ignore**. It 7has to be answered inside Claude Code's own process, every time, in well 8under a millisecond, so it does no I/O and asks no one. 9 10`route` in [lib.rs](lib.rs) starts from the event's role and narrows it for 11the four tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`, 12`PermissionDenied`): 13 14```mermaid 15flowchart TD 16 E["an event"] --> T{"a tool event?"} 17 T -- no --> R["its role<br/>(chapter 8)"] 18 T -- yes --> B{"tool is Bash?"} 19 B -- no --> I1["ignore<br/>only Bash commands are judged"] 20 B -- yes --> RO{"plainly read-only?"} 21 RO -- yes --> I2["ignore<br/>plainly read-only"] 22 RO -- no --> R 23``` 24 25Only Bash is judged, so only Bash's outcomes are worth reporting; and a 26command the filter waves through is never judged, so its outcome is not 27reported either. `handle_json` wraps `route` with the JSON on both sides, 28and answers anything it cannot read (an event this build does not know, 29or not an object at all) with `ignore` and a `why`. 30 31## The read-only filter 32 33[read_only.rs](read_only.rs) answers one question: can this command line 34*only read*? A yes means the command never leaves the process: no round 35trip, no question to Jev, no cost. It is the most important function in 36the mod, and it is deliberately dim. 37 38It does not parse shell. Shell is a language in which `ls` can delete your 39home directory, given the right `$(...)`, and a parser that understands 40every way would be a large thing to get right. So the filter refuses to 41understand anything clever. If a command contains any character that could 42chain, redirect, substitute, glob, escape or quote 43(`` ; | & < > $ ` ( ) { } * ? ~ ! # \ ' " ``, or a newline), it is not 44plainly read-only. What is left is 45a program name and plain words, and the program has to be on a short list: 46 47| Program | Read-only when | 48| --- | --- | 49| `ls`, `pwd`, `cat`, `head`, `tail`, `wc`, `file`, `stat`, `du`, `df`, `whoami`, `which`, `echo`, `grep` | always (with the rules below) | 50| `rg` | no `--pre`, which runs a command on every file | 51| `tree` | no `-o`, which writes a file | 52| `date` | no `-s` or `--set`, which set the clock | 53| `git` | the subcommand comes first, and is `status`, `log`, `diff`, `show`, `blame`, `rev-parse`, `describe` or `ls-files` with no `--output` or `-o`; `branch` with only listing flags; or `remote -v` | 54 55And no word may point outside the project: nothing starting with `/`, no 56`..` segment, no `=/`. A command that reaches outside the project is not 57waved through, however harmless it looks: `cat /etc/hostname` goes to Jev 58like any other. 59 60> **Aside: wrong in only one direction.** Every filter is wrong sometimes. 61> This one is designed so that all its mistakes are the cheap kind. If it 62> says "read-only" about a command that is not, a command that changes 63> something runs unjudged: that must never happen. If it says "not 64> read-only" about a command that is (`cat 'my notes.txt'`, quoted, is 65> refused), the only cost is that Jev is asked, a quarter of a second and 66> a fraction of a cent. So every doubt is resolved towards "ask Jev", and 67> the tests are mostly a list of commands that must be refused: `ls; 68> rm -rf target`, `ls $(rm x)`, `git -c core.pager=sh log`, `git diff 69> --output=x`, `rg --pre sh x`, `date -s tomorrow`. 70 71> **Try it.** `cargo test -p jevhooks-mod`. `plain_reads_are_read_only`, 72> `anything_that_could_write_is_not` and 73> `reading_outside_the_project_is_left_to_jev` are the filter; 74> the rest of the tests are the routing. Add a command you are unsure 75> about to the second list and see whether it stays green. 76 77## For the people who maintain it 78 79The `abi` module, compiled only for wasm32, is what `plugin/hooks/bridge.ts` 80calls: `alloc(len)` reserves bytes for the question, `handle(ptr, len)` 81answers it and returns a pointer to the answer, `out_len()` gives the 82answer's length, and `dealloc(ptr, len)` frees either. `OUT_LEN` is a 83`static mut`, which is sound because the mod is single-threaded and reads 84it straight after `handle`. 85 86### In this folder 87 88| Path | What | 89| --- | --- | 90| [lib.rs](lib.rs) | `Routed`, `route`, `handle_json`, the wasm `abi` (`alloc`, `dealloc`, `handle`, `out_len`), and the routing tests. | 91| [read_only.rs](read_only.rs) | `is_read_only`, `SHELL_SYNTAX`, `leaves_project`, `git_reads`, and the filter's tests. | 92 93← Previous: [Chapter 9, jevhooks-mod/](../) · Up: [jevhooks-mod](../) · Next: [Chapter 11, jevhooks-daemon/](../../jevhooks-daemon/) →