1# Chapter 11: jevhooks-daemon, one process for the machine 2 3You might have three Claude Code sessions open, each about to run a 4command. Each could open its own connection to Jev, fetch the key, and keep 5its own log. Instead they all talk to **one daemon**: a small local server, 6one for the whole machine, listening on a Unix socket. It opens the 7connection to Jev once and keeps it warm, holds the key in one process and 8nowhere else, and writes every decision from every session to one log. 9 10This crate builds one binary, `jevhooks`, that plays every part: 11 12| Command | What it is | 13| --- | --- | 14| `jevhooks serve` | The daemon. | 15| `jevhooks mcp` | The MCP server a Claude Code session starts (the plugin's `.mcp.json`). | 16| `jevhooks status` | Prints the running daemon's status. | 17| `jevhooks recent [count]` | Prints the latest decisions and outcomes, oldest first (20 by default). | 18| `jevhooks stop` | Asks the daemon to stop. | 19 20## Who starts the daemon 21 22Nobody starts it by hand. Each session starts `jevhooks mcp`, and the MCP 23server's first act is to make sure a daemon is running, and that it is the 24right one: 25 26```mermaid 27sequenceDiagram 28 participant S as Claude Code session 29 participant M as jevhooks mcp 30 participant D as jevhooks serve 31 S->>M: start (stdio) 32 M-->>S: two tools, at once 33 M->>D: GET /status 34 alt same build answers 35 D-->>M: identity matches: done 36 else another build answers 37 M->>D: POST /shutdown, wait for the socket to close 38 M->>D: start one from this executable 39 else nothing answers 40 M->>D: start one from this executable 41 end 42 M->>D: GET /status until it answers (up to 30 s) 43``` 44 45"The right one" means **the same build**. A daemon's identity is its 46executable's device, inode, size and modification time, so the first 47session after `just daemon` sees a different identity, retires the old 48daemon and starts the new one. You never have to remember to restart it. 49 50The MCP server does not wait for any of that before answering Claude Code: 51fetching the key can take seconds, and a session should not wait on that to 52list two tools. The two tools are `status` and `recent`, both read-only, 53both just the daemon's own routes. 54 55> **Aside: a socket, a lock, and a process group.** Three small things 56> keep "one daemon" true. The socket is a file in the state directory, so 57> only processes on this machine, and with that user's permissions, can 58> reach it. A lock file (`daemon.lock`) is held for as long as a daemon 59> runs, so a second one started by accident sees it, says so on stderr, 60> and exits. And the daemon is started in its own process group, so the 61> session that started it can end without taking it along. 62 63## The key 64 65`TYPESAFE_API_KEY` reaches the daemon one of two ways: it is already in the 66environment of the session that starts it, and is inherited; or it is not, 67`op-env-run` is on `PATH`, and the daemon is started through it, which puts 68the key in the daemon's environment and nowhere else. With neither, the 69daemon still runs, asks Jev nothing, and every verdict is "pass"; `status` 70says `no key`. 71 72## What it keeps 73 74All under `$XDG_STATE_HOME/jevhooks` (`~/.local/state/jevhooks` when that is 75not set): 76 77| File | What | 78| --- | --- | 79| `daemon.sock` | The socket every session's mod and MCP server talk to. | 80| `daemon.lock` | Held while a daemon runs, so two cannot serve at once. | 81| `daemon.log` | The daemon's stderr. | 82| `decisions.jsonl` | Every judgment (what was judged, every option's probability, the verdict, time and cost) and what became of each judged command (`ran`, `failed`, `denied`). This is the evidence for tuning the thresholds. | 83 84Spend is booked in Jev's shared ledger, kept by `jev-http` under 85`$XDG_STATE_HOME/jev`, whose guards apply across every program on the 86machine that asks Jev: 30 questions a minute and $0.25 an hour by default 87(`JEV_MAX_QUESTIONS_PER_MINUTE`, `JEV_MAX_DOLLARS_PER_HOUR`). A question the 88ledger refuses is a "pass". 89 90## Its one setting 91 92`$XDG_CONFIG_HOME/jevhooks/config.toml` (`~/.config/jevhooks/config.toml`). 93The file is optional, and so is its one key: 94 95```toml 96# The headroom gate asks before a heavy command when too little memory is 97# available. By default "available" is the kernel's own figure (MemAvailable 98# on Linux), which is right on bare metal. Set a command where that figure is 99# wrong or missing: inside a virtual machine that cannot see its host, or on a 100# system with no such figure. It must print the megabytes available as its 101# first number, within two seconds. 102available_memory_command = "my-host-memory --megabytes" 103``` 104 105A misspelt key is reported by `status`, and the daemon runs on the defaults: 106a typo must not switch the gate off. Where there is neither a built-in 107figure nor a command, a command's load is simply not judged; everything 108else works. 109 110> **Try it.** Build it (`just daemon`, which needs the rmcp fork) and start 111> a session once, then from the repository root: 112> 113> plugin/bin/jevhooks status 114> plugin/bin/jevhooks recent 5 115> 116> `status` shows whether Jev can be asked (`ready`, `no key` or `broken`), 117> the pinned model, how many events it has decided and observed, the spend 118> ledger's line, where the memory figure comes from and what it reads now, 119> and every threshold that turns a probability into a verdict. 120 121## For the people who maintain it 122 123Measured on Claude Code 2.1.287: the mod's call to the daemon adds 4 to 13 124ms to the daemon's own 120 to 260 ms; the MCP server connects in 55 ms. 125 126The daemon's routes, served with hyper as HTTP/1.1 on the socket: 127 128| Route | What | 129| --- | --- | 130| `POST /decide` | A deciding event; answers with a `Decision`. | 131| `POST /observe` | An event the daemon needs but does not answer. | 132| `GET /status` | Whether Jev can be asked, the identity, counts, the ledger, the headroom source and the thresholds. | 133| `GET /recent?count=20` | The last records of `decisions.jsonl`, oldest first. | 134| `POST /shutdown` | Stop, so a newer build can take the socket. | 135 136### In this folder 137 138| Path | What | 139| --- | --- | 140| [src/](src/) | Chapter 12: the questions, the verdicts, and the daemon's parts. | 141| [Cargo.toml](Cargo.toml) | The `jevhooks` binary: hyper, tokio, rmcp, clap, toml, and `jev-http`/`jev-protocol` from `third-party/jevcrates`. | 142 143← Previous: [Chapter 10, jevhooks-mod/src/](../jevhooks-mod/src/) · Up: [crates](../) · Next: [Chapter 12, jevhooks-daemon/src/](src/) →