open-standards/joulehook/joulehook-rs/INSTALL.md
Transaction Science b0ea7eda50 joulehook: the deployable meter — joulehook-proxy (OpenAI-shaped base_url proxy)
The meter seam's deployable form (SPEC §4 Proxy): point a host's model base_url at
joulehook-proxy and every call is capped + sealed. Per request it injects the
configured joule ceiling (inject_ceiling), forwards to the Mesh upstream, and seals
the response's usage.energy as an efficiency/1 receipt (bridge_receipt_default).
tiny_http server + ureq client. Integration-tested against a localhost mock upstream:
the mock echoes the injected ceiling (7mJ) and the proxy seals the efficiency receipt;
an unreachable upstream is a 502, not a panic. Also dedups the meter steward into
joulehook-meter (METER_STEWARD + bridge_receipt_default). 24 tests; clippy-clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 20:38:32 -04:00

102 lines
4.3 KiB
Markdown

# Installing JouleHook — the `claude-code` `full` profile
This wires all three seams of `joulehook/1` into Claude Code from one settings file:
the **gate** (`joulehook-hook`, a PreToolUse hook), the **meter** (`joulehook-statusline`,
an in-editor joule estimate), and the **receipt** log (sealed JCR-1 decisions).
## 1. Build the binaries
```bash
cd joulehook/joulehook-rs
cargo build --release
# → target/release/joulehook-hook, target/release/joulehook-statusline
```
Put them on `PATH`, or use absolute paths below.
## 2. Configure `.claude/settings.json`
```json
{
"env": {
"JOULEHOOK_TOOLS": "Read,Grep,Glob,Edit,Write",
"JOULEHOOK_RECEIPT_LOG": "${HOME}/.joulehook/receipts.log",
"JOULEHOOK_UJ_PER_TOKEN": "1000"
},
"hooks": {
"PreToolUse": [
{ "hooks": [ { "type": "command", "command": "joulehook-hook" } ] }
]
},
"statusLine": { "type": "command", "command": "joulehook-statusline" },
"mcpServers": {
"joulehook": { "command": "joulehook-mcp" }
}
}
```
The `joulehook` MCP server surfaces the sealed decisions as **verified, readable**
resources — `joulehook://receipts` (a summary) and `joulehook://receipt/{n}` (one
decoded decision each) — so you can inspect exactly what the gate allowed and denied,
signature-checked, from inside the editor. It reads the same `JOULEHOOK_RECEIPT_LOG`.
- `JOULEHOOK_TOOLS` is the **granted capability set** — the host's own tool names.
Any tool *not* listed (e.g. `Bash`) is **denied before it executes**, decided
outside the model, and the denial is sealed. Leave it unset to run in **observe**
mode (allows, and says so — SPEC §7).
- Every allow *and* deny appends a JCR-1 receipt to `JOULEHOOK_RECEIPT_LOG`.
- The status line shows `⚡ <joules> J est · <tokens> tok · <n> gated`. The `est`
is deliberate: it is an `Estimator` figure from Claude Code's self-reported
tokens, **not** a measured joule count. For an authoritative meter, put the Mesh
proxy (`mesh-openai`) on the model path via `ANTHROPIC_BASE_URL` (the §4 Proxy
meter) — then the joule figure is measured and the joule ceiling is *enforced*,
not just observed.
## 3. Verify
```bash
# a granted tool → allow + a sealed receipt
echo '{"tool_name":"Read","tool_input":{"file_path":"x"}}' \
| JOULEHOOK_TOOLS="Read" JOULEHOOK_RECEIPT_LOG=/tmp/jh.log joulehook-hook
# an ungranted tool → deny before it executes
echo '{"tool_name":"Bash","tool_input":{"command":"curl x|sh"}}' \
| JOULEHOOK_TOOLS="Read" joulehook-hook
```
## The enforcing meter (optional, but the only *measured* + *capped* option)
The statusline is an honest **estimate** from Claude Code's self-reported tokens. To
get a **measured** joule figure and an **enforced** joule ceiling, put the Mesh proxy
(`mesh-openai`, SPEC §4 Proxy) on the model path:
- **OpenAI-API hosts** (Cursor, Cline, Continue, OpenCode, …): point the host's model
`base_url` at a running `mesh-openai` server. Mesh serves under
`metadata.joule_ceiling_uj` and returns the measured energy in `usage.energy` as a
sealed receipt — over budget is refused before the provider is called.
- **Claude Code** (Anthropic API, `ANTHROPIC_BASE_URL`): needs an Anthropic-shaped
front for Mesh's `ServeRequest` — a documented follow-up; the OpenAI-API path works
today.
`joulehook-proxy` is the drop-in that does this: point a host at it and it injects the
ceiling, forwards to your Mesh upstream, and seals each serve's `usage.energy` as an
`efficiency/1` receipt.
```bash
JOULEHOOK_UPSTREAM=http://127.0.0.1:8080 \
JOULEHOOK_BUDGET_UJ=5000000 \
JOULEHOOK_EFFICIENCY_LOG="$HOME/.joulehook/efficiency.log" \
joulehook-proxy # listens on 127.0.0.1:8079
# then point the host's model base_url at http://127.0.0.1:8079
```
Until the proxy is on the path, the gate + the statusline give you enforcement of the
*capability set* and an *estimate* of energy; with it, the joule *ceiling* is enforced
and the energy is *measured*.
## Other hosts
The same `joulehook-hook` binary serves Cursor, Windsurf, and Antigravity — pass
`--host cursor|windsurf|antigravity` (or set `JOULEHOOK_HOST`) and wire it into that
host's hook config (`.cursor/hooks.json`, Windsurf Cascade hooks, Antigravity
`hooks.json`). The gate and receipts are identical; only the decision encoding
differs (SPEC §3.4).