After the branch graded these figures by how they were obtained, the
docs still called them measured: jouledesk's channel example printed
"the teammate's measured contribution", Joule::energy_summary's doc and
SPEC §6 said the contribution "is *measured*", the room example sealed
an Estimator figure and closed "at its measured energy" (each turn
"metered {est} µJ" for a scripted estimate), and joulehook INSTALL.md
promised the Mesh proxy as "the only *measured* + *capped* option" that
"returns the measured energy".
Each now says the figure carries the grade of the meter that produced it
(a hardware reading only where a live counter read the work, else an
estimate graded as one), that a rollup is no better than the weakest
figure it sums, and the room example calls its figures estimates.
Text only; jouledesk and jouledesk-github tests pass unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.5 KiB
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
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
{
"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_TOOLSis 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. Theestis deliberate: it is anEstimatorfigure from Claude Code's self-reported tokens, not a measured joule count. For a metered figure, put the Mesh proxy (mesh-openai) on the model path viaANTHROPIC_BASE_URL(the §4 Proxy meter) — then the joule figure is the Mesh host's meter's, at that meter's grade (a hardware reading only where it read one), and the joule ceiling is enforced, not just observed.
3. Verify
# 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 metered + capped option)
The statusline is an honest estimate from Claude Code's self-reported tokens. To
get a metered joule figure — at the grade of the meter that produced it, a hardware
reading only where the Mesh host's meter read one — 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_urlat a runningmesh-openaiserver. Mesh serves undermetadata.joule_ceiling_ujand returns the served energy inusage.energy, graded by the meter that produced it (usage.energy.provenance), 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'sServeRequest— 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.
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 metered, at the grade of the meter that produced it.
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).