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]>
102 lines
4.3 KiB
Markdown
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).
|