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/) →