fix(claude_desktop): make Headroom actually save tokens (cowork routing + Kompress)

Headroom kept reporting zero savings for two independent reasons:

1. Desktop cowork/local-agent-mode sessions never reached the proxy.
   Desktop spawns its bundled Claude Code binary at an absolute path
   (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the
   production endpoint (headroom #869). Manage env.ANTHROPIC_BASE_URL
   in ~/.claude/settings.json instead — Claude Code writes settings
   `env` entries over the inherited environment at startup, and cowork
   sessions load user settings. Managed-value semantics: only set or
   remove the variable when absent or equal to the add-on-managed proxy
   URL, so a user-customized endpoint is never clobbered.

2. Even proxied traffic compressed nothing (175 requests, 0 saved).
   The proxy's startup preload is cache-only, but the HF model cache
   defaulted to ~/.cache -> tmpfs, wiped every restart, so the Kompress
   ONNX model and its separately fetched ModernBERT tokenizer were
   never cached and the engine idled in "deferred" mode forever
   (misleadingly logged as "Kompress: not installed"). svc-headroom now
   sets HF_HOME to persistent ~/.headroom/hf and pre-warms the cache
   once at startup, bounded at 300s so an offline install still starts
   the proxy in pass-through mode and retries next boot. The proxy
   extra's ONNX runtime suffices — the multi-GB PyTorch [ml] extra is
   deliberately not installed.

Verified live: proxy logs "Kompress: ENABLED (ModernBERT token
compressor)" after restart, and a terminal `claude -p` round-trip
increments the proxy's api_requests counter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
alexbelgium
2026-07-15 20:55:27 +02:00
parent 3bff6b65de
commit 3354af026f
5 changed files with 88 additions and 2 deletions

View File

@@ -1,3 +1,8 @@
## 1.27 (15-07-2026)
- Route Claude Desktop cowork/local-agent-mode sessions through the Headroom proxy. Desktop spawns its bundled Claude Code binary at an absolute path (bypassing the add-on's PATH wrapper) with `ANTHROPIC_BASE_URL` pinned to the production endpoint, so those sessions never produced proxy savings. The add-on now manages `env.ANTHROPIC_BASE_URL` in `~/.claude/settings.json` — settings `env` entries replace inherited environment values at CLI startup — gated on `headroom_wrap_claude_code` and never overwriting a user-customized endpoint.
- Fix Headroom's Kompress compression engine never activating, which made even proxied traffic record zero token savings (e.g. 175 requests, 0 saved). The proxy's startup preload is deliberately cache-only, but the HuggingFace model cache defaulted to `~/.cache` — tmpfs in this add-on, wiped every restart — so the ONNX model (plus the separately fetched `answerdotai/ModernBERT-base` tokenizer) was never cached and the engine idled in "deferred" mode forever, misleadingly logged as `Kompress: not installed`. `svc-headroom` now points `HF_HOME` at persistent storage (`~/.headroom/hf`, ~270 MB) and pre-warms the cache once at startup (bounded at 300 s, so an offline install still starts the proxy in pass-through mode and retries next boot). The already-installed `proxy` extra's ONNX runtime is sufficient — the multi-gigabyte PyTorch `ml` extra is deliberately not installed.
## 1.26 (15-07-2026)
- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`.

View File

@@ -143,7 +143,10 @@ RUN /usr/local/bin/rtk --version && /usr/local/bin/tokensave --version
# Install only the Headroom proxy, code-compression, and MCP features used by this add-on,
# plus mcp-proxy (stdio->HTTP bridge for the Home Assistant MCP server) and uv (fast
# installer used for the additional_pip option).
# installer used for the additional_pip option). The `proxy` extra already ships the ONNX
# runtime + transformers needed by the Kompress compressor — the `ml` extra (full PyTorch,
# ~5 GB with CUDA wheels) is deliberately NOT installed; svc-headroom pre-warms the ONNX
# model into the persistent HF cache instead.
RUN apt-get update && \
apt-get install -y --no-install-recommends nodejs && \
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \

View File

@@ -109,5 +109,5 @@ slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.26"
version: "1.27"
video: true

View File

@@ -308,6 +308,63 @@ if new != text:
PY
fi
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
# ~/.claude/settings.json. Claude Code writes settings `env` entries into the process
# environment at startup, replacing inherited values — this is the only supported way to reach
# Desktop cowork/local-agent-mode sessions, which spawn the bundled CLI at an absolute path
# (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the production endpoint
# (headroom #869). Managed-value semantics: only set or remove the variable when it is absent
# or already equals the add-on-managed proxy URL, so a user-customized endpoint is never
# clobbered. The svc-headroom longrun is s6-supervised, so a crashed proxy restarts within
# seconds; the terminal wrapper's per-launch health check remains as an extra safety net.
if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
HEADROOM_ROUTE_ACTION="add"
else
HEADROOM_ROUTE_ACTION="remove"
fi
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
import json
import os
from pathlib import Path
MANAGED_URL = "http://127.0.0.1:8787"
path = Path.home() / ".claude" / "settings.json"
try:
data = json.loads(path.read_text()) if path.exists() else {}
if not isinstance(data, dict):
data = {}
except Exception:
if path.exists():
path.rename(path.with_suffix(path.suffix + ".bak"))
data = {}
env = data.get("env")
if not isinstance(env, dict):
env = {}
current = env.get("ANTHROPIC_BASE_URL")
changed = False
if os.environ["HEADROOM_ROUTE_ACTION"] == "add":
if current is None or current == MANAGED_URL:
if current != MANAGED_URL:
env["ANTHROPIC_BASE_URL"] = MANAGED_URL
changed = True
else:
print(f"Claude settings env already sets ANTHROPIC_BASE_URL={current}; leaving it untouched")
elif current == MANAGED_URL:
del env["ANTHROPIC_BASE_URL"]
changed = True
if changed:
if env:
data["env"] = env
elif "env" in data:
del data["env"]
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed). Managed, idempotent block appended to
# the user's global CLAUDE.md; removed when the helper is disabled. Mirrors the headroom block.

View File

@@ -10,6 +10,27 @@ if bashio::config.true 'expose_headroom_dashboard'; then
fi
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
# Kompress (the ONNX compression engine) needs its model in the local HF cache: the
# proxy's startup preload is deliberately cache-only, and the default HF cache lands
# under ~/.cache, which the add-on points at tmpfs (/tmp/cache) — wiped on every
# restart. Without a warm persistent cache the proxy runs forever in "deferred" mode
# and records zero compression savings. Cache on persistent storage and pre-warm it
# once (bounded so an offline install still starts the proxy, in pass-through mode,
# and retries the download on the next start).
export HF_HOME="${HOME}/.headroom/hf"
mkdir -p "$HF_HOME"
chown abc:abc "$HF_HOME" 2> /dev/null || true
# headroom lives in the lsiopy virtualenv; plain `python3` does not see its packages,
# so derive the venv interpreter from the resolved headroom entry script.
headroom_python="$(dirname "$(readlink -f "$(command -v headroom)")")/python3"
if ! s6-setuidgid abc "$headroom_python" -c \
'from headroom.transforms.kompress_compressor import _load_kompress; _load_kompress(allow_download=False)' \
> /dev/null 2>&1; then
bashio::log.info "svc-headroom: pre-warming the Kompress model cache (one-time download)"
timeout 300 s6-setuidgid abc "$headroom_python" -c \
'from headroom.transforms.kompress_compressor import _load_kompress; _load_kompress()' \
|| bashio::log.warning "svc-headroom: Kompress pre-warm failed; compression stays deferred until the model can be downloaded"
fi
bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}"
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
fi