Chapter 10: jevhooks-mod/src, routing and a filter that is wrong in only one direction
Every event Claude Code raises comes through here first, as a little JSON
question: { "event": "PreToolUse", "tool": "Bash", "command": "git status" }.
The answer is one of three roles, from chapter 8: decide (send it to the
daemon and wait), observe (send it and do not wait) or ignore. It
has to be answered inside Claude Code's own process, every time, in well
under a millisecond, so it does no I/O and asks no one.
route in lib.rs starts from the event's role and narrows it for
the four tool events (PreToolUse, PostToolUse, PostToolUseFailure,
PermissionDenied):
flowchart TD
E["an event"] --> T{"a tool event?"}
T -- no --> R["its role<br/>(chapter 8)"]
T -- yes --> B{"tool is Bash?"}
B -- no --> I1["ignore<br/>only Bash commands are judged"]
B -- yes --> RO{"plainly read-only?"}
RO -- yes --> I2["ignore<br/>plainly read-only"]
RO -- no --> R
Only Bash is judged, so only Bash's outcomes are worth reporting; and a
command the filter waves through is never judged, so its outcome is not
reported either. handle_json wraps route with the JSON on both sides,
and answers anything it cannot read (an event this build does not know,
or not an object at all) with ignore and a why.
The read-only filter
read_only.rs answers one question: can this command line only read? A yes means the command never leaves the process: no round trip, no question to Jev, no cost. It is the most important function in the mod, and it is deliberately dim.
It does not parse shell. Shell is a language in which ls can delete your
home directory, given the right $(...), and a parser that understands
every way would be a large thing to get right. So the filter refuses to
understand anything clever. If a command contains any character that could
chain, redirect, substitute, glob, escape or quote
(; | & < > $ ` ( ) { } * ? ~ ! # \ ' ", or a newline), it is not
plainly read-only. What is left is
a program name and plain words, and the program has to be on a short list:
| Program | Read-only when |
|---|---|
ls, pwd, cat, head, tail, wc, file, stat, du, df, whoami, which, echo, grep | always (with the rules below) |
rg | no --pre, which runs a command on every file |
tree | no -o, which writes a file |
date | no -s or --set, which set the clock |
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 |
And no word may point outside the project: nothing starting with /, no
.. segment, no =/. A command that reaches outside the project is not
waved through, however harmless it looks: cat /etc/hostname goes to Jev
like any other.
Aside: wrong in only one direction. Every filter is wrong sometimes. This one is designed so that all its mistakes are the cheap kind. If it says "read-only" about a command that is not, a command that changes something runs unjudged: that must never happen. If it says "not read-only" about a command that is (
cat 'my notes.txt', quoted, is refused), the only cost is that Jev is asked, a quarter of a second and a fraction of a cent. So every doubt is resolved towards "ask Jev", and the tests are mostly a list of commands that must be refused:ls; rm -rf target,ls $(rm x),git -c core.pager=sh log,git diff --output=x,rg --pre sh x,date -s tomorrow.
Try it.
cargo test -p jevhooks-mod.plain_reads_are_read_only,anything_that_could_write_is_notandreading_outside_the_project_is_left_to_jevare the filter; the rest of the tests are the routing. Add a command you are unsure about to the second list and see whether it stays green.
For the people who maintain it
The abi module, compiled only for wasm32, is what plugin/hooks/bridge.ts
calls: alloc(len) reserves bytes for the question, handle(ptr, len)
answers it and returns a pointer to the answer, out_len() gives the
answer's length, and dealloc(ptr, len) frees either. OUT_LEN is a
static mut, which is sound because the mod is single-threaded and reads
it straight after handle.
In this folder
| Path | What |
|---|---|
| lib.rs | Routed, route, handle_json, the wasm abi (alloc, dealloc, handle, out_len), and the routing tests. |
| read_only.rs | is_read_only, SHELL_SYNTAX, leaves_project, git_reads, and the filter's tests. |
← Previous: Chapter 9, jevhooks-mod/ · Up: jevhooks-mod · Next: Chapter 11, jevhooks-daemon/ →