mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-23 05:13:32 +02:00
Merge pull request #2861 from alexbelgium/agent/claude-tools-hardening
Improve Claude Desktop optimization tooling
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
## 1.20 (15-07-2026)
|
||||
|
||||
- Complete the TokenSave Claude Code integration at startup: install its MCP server, permissions, PreToolUse/UserPromptSubmit/Stop hooks, global guidance, and Git synchronization hooks instead of registering only `tokensave serve`.
|
||||
- Add `tokensave_project_paths` for explicit per-repository initialization and incremental synchronization; no repositories are scanned or indexed unless listed.
|
||||
- Route PATH-based Claude Code launches through the already-supervised Headroom proxy by default with a recursion-safe `/usr/local/bin/claude` wrapper; fall back to the official binary when the proxy is unavailable.
|
||||
- Pass the local proxy URL explicitly to the Headroom MCP server, while retaining MCP-only integration for the Desktop Electron application.
|
||||
- Keep the unauthenticated Headroom dashboard container-local by default; add `expose_headroom_dashboard` and leave port `8787/tcp` unmapped until explicitly enabled.
|
||||
- Fix the hourly gains report so Headroom no longer suppresses RTK output, add TokenSave gains, and gate each tool on its actual add-on option.
|
||||
- Add `claude-tools-doctor.sh` to inspect binaries, redacted MCP registrations, hooks, proxy health, routing, project indexes, and gains.
|
||||
- Install local validation tools (`jq`, `shellcheck`, `yamllint`, current `hadolint`, and current `actionlint`) to reduce avoidable CI round-trips.
|
||||
- Disable the unpinned third-party Caveman startup installer by default; it remains opt-in.
|
||||
|
||||
## 1.19 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 1.18 (14-07-2026)
|
||||
@@ -17,7 +29,7 @@
|
||||
- Minor bugs fixed
|
||||
## 1.15 (13-07-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
## ubunturesolute-version-6dc44b0e (2026-07-13)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
## 1.14 (10-07-2026)
|
||||
|
||||
@@ -34,6 +34,7 @@ RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /ou
|
||||
/out/bin/tokensave --version
|
||||
|
||||
FROM ${BUILD_FROM}
|
||||
ARG BUILD_ARCH
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
@@ -73,15 +74,17 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
# cannot alter executables elsewhere in the image.
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
# hadolint ignore=DL4005
|
||||
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
|
||||
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
|
||||
|
||||
# Install Claude Desktop, Claude Code, and Python tooling. gnome-keyring provides the
|
||||
# Secret Service backend Electron safeStorage needs to persist sign-in and dispatch grants.
|
||||
# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators.
|
||||
# gnome-keyring provides the Secret Service backend Electron safeStorage needs to persist
|
||||
# sign-in and dispatch grants.
|
||||
RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc https://downloads.claude.ai/claude-desktop/key.asc && \
|
||||
curl -fsSLo /etc/apt/keyrings/claude-code.asc https://downloads.claude.ai/keys/claude-code.asc && \
|
||||
@@ -96,10 +99,42 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
dbus-x11 \
|
||||
git \
|
||||
gh \
|
||||
ripgrep && \
|
||||
ripgrep \
|
||||
jq \
|
||||
shellcheck \
|
||||
yamllint && \
|
||||
test -x /usr/bin/claude && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the current upstream hadolint and actionlint releases for both supported
|
||||
# architectures. The GitHub release API resolves the latest asset at build time, so these
|
||||
# developer tools are intentionally not version-pinned.
|
||||
RUN set -eux; \
|
||||
case "${BUILD_ARCH}" in \
|
||||
amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \
|
||||
aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \
|
||||
*) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
hadolint_name="hadolint-linux-${hadolint_arch}"; \
|
||||
hadolint_url="$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \
|
||||
| jq -r --arg name "${hadolint_name}" '.assets[] | select(.name == $name) | .browser_download_url' \
|
||||
| head -n 1)"; \
|
||||
test -n "${hadolint_url}"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 -o /usr/local/bin/hadolint "${hadolint_url}"; \
|
||||
chmod 0755 /usr/local/bin/hadolint; \
|
||||
actionlint_suffix="_linux_${actionlint_arch}.tar.gz"; \
|
||||
actionlint_url="$(curl -fsSL https://api.github.com/repos/rhysd/actionlint/releases/latest \
|
||||
| jq -r --arg suffix "${actionlint_suffix}" '.assets[] | select(.name | endswith($suffix)) | .browser_download_url' \
|
||||
| head -n 1)"; \
|
||||
test -n "${actionlint_url}"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 -o /tmp/actionlint.tar.gz "${actionlint_url}"; \
|
||||
tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint; \
|
||||
install -m 0755 /tmp/actionlint /usr/local/bin/actionlint; \
|
||||
rm -f /tmp/actionlint /tmp/actionlint.tar.gz; \
|
||||
hadolint --version; \
|
||||
actionlint -version
|
||||
|
||||
# Copy the pinned Bookworm-built RTK and tokensave binaries and execute them in the final
|
||||
# image. This makes an ABI mismatch fail the image build instead of surfacing at runtime.
|
||||
COPY --from=rtk-builder /out/rtk /usr/local/bin/rtk
|
||||
@@ -107,7 +142,7 @@ COPY --from=tokensave-builder /out/bin/tokensave /usr/local/bin/tokensave
|
||||
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->SSE bridge for the Home Assistant MCP server) and uv (fast
|
||||
# plus mcp-proxy (stdio->HTTP bridge for the Home Assistant MCP server) and uv (fast
|
||||
# installer used for the additional_pip option).
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
@@ -159,7 +194,6 @@ CMD [ "/ha_entrypoint.sh" ]
|
||||
# 5 Labels #
|
||||
############
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_DESCRIPTION
|
||||
ARG BUILD_NAME
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
![Supports amd64 Architecture][amd64-shield]
|
||||
![Project Maintenance][maintenance-shield]
|
||||
|
||||
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom MCP
|
||||
context compression, RTK Bash-output acceleration, and code-intelligence
|
||||
tooling wired in by default.
|
||||
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom context
|
||||
compression, RTK Bash-output acceleration, and TokenSave semantic code
|
||||
intelligence wired in by default.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -24,15 +24,35 @@ currently does not include Computer Use or dictation.
|
||||
Everything is built around the Claude Desktop app. Claude Code is installed in
|
||||
the same image but is not exposed as a standalone service: Claude Desktop's
|
||||
cowork and dispatch sessions run it internally, and they pick up the shared
|
||||
Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically.
|
||||
Claude Code configuration (`~/.claude`), hooks, MCP servers, permissions, and
|
||||
PATH tools.
|
||||
|
||||
- **Claude Desktop** uses Headroom through its MCP tools.
|
||||
- **Claude Code sessions inside Desktop** get the same MCP servers via
|
||||
`~/.claude.json` and RTK's `PreToolUse` Bash hook via
|
||||
`~/.claude/settings.json`.
|
||||
- **Claude Code sessions inside Desktop** get the same MCP servers, permission
|
||||
mode, and RTK/TokenSave hooks through the shared Claude Code configuration.
|
||||
- PATH-based Claude Code launches are routed through the supervised Headroom
|
||||
proxy when `headroom_wrap_claude_code` is enabled. If a Desktop release calls
|
||||
`/usr/bin/claude` directly, the session remains functional and still has the
|
||||
shared permission mode and Headroom MCP tools, but transparent proxy
|
||||
compression cannot be injected.
|
||||
- **gnome-keyring** provides the Secret Service backend Electron needs to
|
||||
persist sign-in and dispatch permission grants across restarts.
|
||||
|
||||
## Optimization layers
|
||||
|
||||
The three bundled optimization tools are complementary:
|
||||
|
||||
- **RTK** rewrites supported Bash commands so Claude receives compact output.
|
||||
- **TokenSave** builds a local semantic graph for explicitly selected code
|
||||
repositories and steers Claude away from repeated Explore/Grep/Read fan-out.
|
||||
- **Headroom** transparently compresses proxied Claude Code traffic and also
|
||||
exposes on-demand compress/retrieve/statistics MCP tools to Claude Desktop.
|
||||
|
||||
TokenSave's complete Claude integration is installed at startup: MCP server,
|
||||
permissions, PreToolUse/UserPromptSubmit/Stop hooks, global prompt rules, and
|
||||
Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
`tokensave_project_paths`; no automatic filesystem scan is performed.
|
||||
|
||||
## Features
|
||||
|
||||
- Claude Desktop in single-app Selkies mode with Home Assistant ingress.
|
||||
@@ -41,16 +61,20 @@ Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically.
|
||||
- Persistent `$HOME` at the configured `data_location` (default `/data/data`),
|
||||
preserving Desktop and Claude Code state across restarts.
|
||||
- Persistent sign-in through a bundled, auto-unlocked gnome-keyring.
|
||||
- Configurable Claude Code permissions: strict prompts, automatic safe-action
|
||||
approval, or explicit full bypass for trusted installations.
|
||||
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
|
||||
- Optional extra apt and pip package installation (pip installs use `uv` for
|
||||
speed).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), and `ripgrep`.
|
||||
- Optional extra apt and pip package installation (pip installs use `uv`).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||
`hadolint`, and `actionlint`.
|
||||
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||
- Bundled optimization tools: Headroom (MCP + local proxy), RTK, tokensave,
|
||||
and Caveman — each individually switchable.
|
||||
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
||||
available as an opt-in plugin.
|
||||
- Optional Home Assistant MCP bridge so Claude can query and control Home
|
||||
Assistant.
|
||||
- Headroom dashboard exposed on mapped port `8787`.
|
||||
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
|
||||
- `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP
|
||||
registrations, project indexes, proxy health, permissions, and gains.
|
||||
- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
|
||||
|
||||
## Options
|
||||
@@ -64,42 +88,99 @@ Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically.
|
||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
||||
| `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. |
|
||||
| `install_headroom` | `true` | Register the Headroom MCP server and run the supervised local proxy/dashboard. |
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. |
|
||||
| `install_tokensave` | `true` | Register the tokensave code-intelligence MCP server for Desktop and Claude Code. |
|
||||
| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. |
|
||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
|
||||
| `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. |
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. |
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
||||
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
|
||||
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
|
||||
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
|
||||
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
|
||||
| `github_username` | | Optional global Git author name. |
|
||||
| `github_email` | | Optional global Git author email. |
|
||||
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/mcp_server/sse` | SSE endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. |
|
||||
| `additional_apps` | | Comma-separated Debian apt packages to install at startup. |
|
||||
| `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). |
|
||||
| `data_location` | `/data/data` | Persistent home directory for Claude and tooling. |
|
||||
| `env_vars` | `[]` | Additional environment variables exported inside the container. |
|
||||
|
||||
### Permission modes
|
||||
|
||||
```yaml
|
||||
permission_mode: auto
|
||||
```
|
||||
|
||||
- `strict` keeps Claude Code's normal interactive permission prompts.
|
||||
- `auto` asks Claude Code's automatic permission classifier to approve safe
|
||||
operations while retaining prompts for risky actions. This is the default.
|
||||
- `bypass` disables Claude Code permission checks by using
|
||||
`bypassPermissions` in the shared settings and
|
||||
`--dangerously-skip-permissions` for wrapper-launched sessions.
|
||||
|
||||
`bypass` gives Claude broad authority over all mounted writable data and every
|
||||
command or credential available inside the add-on. Enable it only in a trusted
|
||||
installation with trusted repositories and mounts.
|
||||
|
||||
### TokenSave project example
|
||||
|
||||
Only repositories listed here are indexed. Paths must be absolute, mounted in
|
||||
the add-on, and resolve to a Git working tree:
|
||||
|
||||
```yaml
|
||||
tokensave_project_paths:
|
||||
- /share/projects/hassio-addons
|
||||
- /share/projects/birdnet-go
|
||||
```
|
||||
|
||||
At startup, an uninitialized repository receives `tokensave init`; an existing
|
||||
index receives an incremental `tokensave sync`. Removing a path from the option
|
||||
stops automatic synchronization but does not delete its `.tokensave` database.
|
||||
Configured repositories are added to Git's `safe.directory` list for the shared
|
||||
runtime user before TokenSave performs repository discovery.
|
||||
|
||||
## Headroom behavior
|
||||
|
||||
When `install_headroom` is enabled, the add-on registers `headroom mcp serve`
|
||||
in Claude Desktop and Claude Code, and starts a supervised local Headroom
|
||||
backend. Claude can use `headroom_compress`, `headroom_retrieve`, and
|
||||
`headroom_stats` through MCP.
|
||||
with the explicit local proxy URL in Claude Desktop and Claude Code, then starts
|
||||
a supervised Headroom backend on `127.0.0.1:8787`.
|
||||
|
||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched
|
||||
without proxy injection; the MCP integration is the supported path.
|
||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so Desktop chat deliberately uses
|
||||
the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claude
|
||||
Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised
|
||||
backend without starting a second proxy.
|
||||
|
||||
The Headroom dashboard is available at:
|
||||
The dashboard is disabled externally by default. To expose it:
|
||||
|
||||
```text
|
||||
http://<home-assistant-host>:8787/dashboard
|
||||
1. Set `expose_headroom_dashboard: true`.
|
||||
2. Map `8787/tcp` in the add-on **Network** section.
|
||||
3. Open `http://<home-assistant-host>:8787/dashboard`.
|
||||
|
||||
The dashboard is unauthenticated. Do not publish this port to the public
|
||||
internet.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Run the following inside the add-on through a custom script or container console:
|
||||
|
||||
```bash
|
||||
claude-tools-doctor.sh
|
||||
```
|
||||
|
||||
through the default `8787/tcp` port mapping. The dashboard is unauthenticated
|
||||
and is reachable wherever Home Assistant publishes that port, so treat it as
|
||||
sensitive: do not expose it directly to the public internet, and unmap the port
|
||||
in the add-on **Network** section if you do not want it reachable at all.
|
||||
The report checks the tool binaries, configuration switches, redacted MCP
|
||||
registrations, Claude hooks, permission mode, Headroom health, TokenSave indexes,
|
||||
routing, and recorded savings. It never prints MCP environment values because
|
||||
the Home Assistant MCP entry can contain a long-lived token.
|
||||
|
||||
The hourly report can also be invoked manually:
|
||||
|
||||
```bash
|
||||
claude-gains-report.sh
|
||||
```
|
||||
|
||||
## Home Assistant MCP bridge
|
||||
|
||||
@@ -129,9 +210,12 @@ Persistent state is stored in the configured `data_location` (default
|
||||
|
||||
- Claude Desktop sign-in: `~/.config/Claude` (token encrypted via
|
||||
gnome-keyring; keyring DB in `~/.local/share/keyrings`)
|
||||
- Claude Code settings, hooks, sessions, and plugins: `~/.claude`
|
||||
- Headroom, RTK, and tokensave user state: their standard paths below the
|
||||
- Claude Code settings, hooks, sessions, plugins, and permission mode:
|
||||
`~/.claude`
|
||||
- Headroom, RTK, and TokenSave user state: their standard paths below the
|
||||
shared home
|
||||
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
|
||||
project
|
||||
|
||||
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||
`$HOME/.cache`.
|
||||
|
||||
@@ -2,7 +2,7 @@ arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
audio: true
|
||||
description: "Claude Desktop with Headroom MCP context compression and RTK acceleration"
|
||||
description: "Claude Desktop with Headroom, RTK, and TokenSave optimization"
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/dri/card0
|
||||
@@ -46,19 +46,24 @@ options:
|
||||
ha_mcp_token: ""
|
||||
github_token: ""
|
||||
github_username: ""
|
||||
install_caveman: true
|
||||
enable_tools_health_report: true
|
||||
expose_headroom_dashboard: false
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
install_github_cli: true
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
install_tokensave: true
|
||||
permission_mode: auto
|
||||
tokensave_project_paths: []
|
||||
panel_admin: false
|
||||
panel_icon: mdi:robot-happy
|
||||
ports:
|
||||
3001/tcp: null
|
||||
8787/tcp: 8787
|
||||
8787/tcp: null
|
||||
ports_description:
|
||||
3001/tcp: Claude Desktop web interface
|
||||
8787/tcp: Headroom dashboard and proxy
|
||||
8787/tcp: Optional Headroom dashboard and proxy
|
||||
privileged:
|
||||
- SYS_ADMIN
|
||||
- DAC_READ_SEARCH
|
||||
@@ -83,14 +88,20 @@ schema:
|
||||
ha_mcp_token: password?
|
||||
github_token: password?
|
||||
github_username: str?
|
||||
enable_tools_health_report: bool
|
||||
expose_headroom_dashboard: bool
|
||||
headroom_wrap_claude_code: bool
|
||||
install_caveman: bool
|
||||
install_github_cli: bool
|
||||
install_headroom: bool
|
||||
install_rtk: bool
|
||||
install_tokensave: bool
|
||||
permission_mode: list(strict|auto|bypass)
|
||||
tokensave_project_paths:
|
||||
- str
|
||||
slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.19"
|
||||
version: "1.20"
|
||||
video: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hourly rtk + headroom token-savings report to the add-on log (heartbeat + gains).
|
||||
# Hourly RTK + Headroom + TokenSave savings report to the add-on log.
|
||||
# Seeded to /data/data/crontabs/root by init-crontab-config and run by svc-cron; edit the
|
||||
# persistent copy to customize. Output goes to /proc/1/fd/1 so it shows in the add-on log.
|
||||
0 * * * * /usr/local/bin/claude-gains-report.sh > /proc/1/fd/1 2>&1
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -A REPOS_SEEN=()
|
||||
while IFS= read -r configured_path; do
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
[ -n "$configured_path" ] || continue
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[ -d "$configured_path" ] || continue
|
||||
|
||||
# The one-shot safe.directory override is used only to discover the repository root.
|
||||
# Persist the resolved root in the shared runtime user's Git config before 82-claude_tools.sh
|
||||
# performs normal repository detection, avoiding Git's dubious-ownership rejection.
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" \
|
||||
git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
[ -n "$repo_root" ] && [ "$repo_root" != "/" ] || continue
|
||||
[[ -z "${REPOS_SEEN[$repo_root]:-}" ]] || continue
|
||||
REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if ! s6-setuidgid abc env HOME="$HOME" git config --global --get-all safe.directory \
|
||||
| grep -Fxq -- "$repo_root"; then
|
||||
s6-setuidgid abc env HOME="$HOME" git config --global --add safe.directory "$repo_root"
|
||||
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||
fi
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
@@ -7,15 +7,18 @@ PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else ech
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
|
||||
mkdir -p "$HOME/.claude"
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||
}
|
||||
|
||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
||||
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
|
||||
printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE"
|
||||
|
||||
# headroom's "wrap"/proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||
# Electron app force-overrides to the production endpoint (headroom #869), so transparent
|
||||
# compression cannot be applied to the desktop launch. The integration that does work with
|
||||
# Claude Desktop is headroom's MCP server, which exposes the headroom_compress/headroom_retrieve/
|
||||
# headroom_stats tools inside the app.
|
||||
# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||
# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore
|
||||
# uses Headroom's MCP tools. Claude Code launches that resolve `claude` through PATH use the
|
||||
# add-on's /usr/local/bin/claude wrapper and can be transparently proxied when enabled.
|
||||
#
|
||||
# Register the add-on-managed MCP servers (headroom, tokensave, homeassistant) in both Claude
|
||||
# Desktop's config and Claude Code's user config (used by Desktop cowork/dispatch sessions).
|
||||
@@ -38,10 +41,18 @@ TOKENSAVE_ENABLED=false
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
if command -v tokensave &> /dev/null; then
|
||||
TOKENSAVE_ENABLED=true
|
||||
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; registering the tokensave MCP server"
|
||||
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; configuring the complete Claude Code integration"
|
||||
# The upstream installer adds the MCP entry, PreToolUse/UserPromptSubmit/Stop hooks,
|
||||
# MCP permissions, global CLAUDE.md rules, and the global post-commit/checkout sync hook.
|
||||
run_as_runtime_user tokensave install --agent claude --git-hook yes \
|
||||
|| bashio::log.warning "tokensave Claude Code integration setup failed"
|
||||
else
|
||||
bashio::log.warning "tokensave is not available"
|
||||
fi
|
||||
elif command -v tokensave &> /dev/null; then
|
||||
bashio::log.info "Removing the tokensave Claude Code integration"
|
||||
run_as_runtime_user tokensave uninstall --agent claude \
|
||||
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
@@ -80,7 +91,10 @@ MANAGED_BASENAMES = {
|
||||
|
||||
desired = {}
|
||||
if os.environ["HEADROOM_ENABLED"] == "true":
|
||||
desired["headroom"] = {"command": os.environ["HEADROOM_BIN"], "args": ["mcp", "serve"]}
|
||||
desired["headroom"] = {
|
||||
"command": os.environ["HEADROOM_BIN"],
|
||||
"args": ["mcp", "serve", "--proxy-url", "http://127.0.0.1:8787"],
|
||||
}
|
||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
@@ -98,6 +112,7 @@ if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
# under $HOME stay untouched because those are user-installed.
|
||||
HOME_PREFIX = os.path.expanduser("~") + os.sep
|
||||
|
||||
|
||||
def is_managed(name, entry):
|
||||
if not isinstance(entry, dict):
|
||||
return False
|
||||
@@ -106,6 +121,7 @@ def is_managed(name, entry):
|
||||
return False
|
||||
return os.path.basename(command) == MANAGED_BASENAMES[name]
|
||||
|
||||
|
||||
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
|
||||
path = Path(os.environ[config_var])
|
||||
try:
|
||||
@@ -145,12 +161,56 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
path.chmod(0o600)
|
||||
PY
|
||||
|
||||
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
|
||||
# real savings (otherwise the tools sit unused and `headroom savings` stays empty). Managed,
|
||||
# idempotent block appended to the user's global CLAUDE.md; removed when headroom is disabled.
|
||||
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
|
||||
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
|
||||
if $TOKENSAVE_ENABLED; then
|
||||
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||
while IFS= read -r configured_path; do
|
||||
# Trim surrounding whitespace while preserving spaces inside paths.
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
[ -n "$configured_path" ] || continue
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if [ ! -d "$configured_path" ]; then
|
||||
bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
|
||||
repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
|
||||
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||
bashio::log.info "Synchronizing TokenSave index: ${repo_root}"
|
||||
run_as_runtime_user tokensave sync "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave sync failed for ${repo_root}"
|
||||
else
|
||||
bashio::log.info "Initializing TokenSave index: ${repo_root}"
|
||||
run_as_runtime_user tokensave init "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave initialization failed for ${repo_root}"
|
||||
fi
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
fi
|
||||
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
# real savings when transparent proxying is unavailable. Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when Headroom is disabled.
|
||||
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
if $HEADROOM_ENABLED; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
|
||||
@@ -193,14 +253,13 @@ fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
if [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk hook claude' "$HOME/.claude/settings.json"; then
|
||||
bashio::log.info "rtk Claude Code hook already configured"
|
||||
else
|
||||
bashio::log.info "Configuring rtk Claude Code hook"
|
||||
RTK_NONINTERACTIVE=1 rtk init -g || bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||
|| bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
@@ -218,7 +277,6 @@ if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "rtk is not available"
|
||||
fi
|
||||
@@ -290,7 +348,8 @@ if bashio::config.true 'install_caveman'; then
|
||||
bashio::log.info "caveman Claude Code plugin already configured"
|
||||
else
|
||||
bashio::log.info "Installing caveman Claude Code plugin"
|
||||
curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash >/dev/null || bashio::log.warning "caveman install failed (offline?)"
|
||||
curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash > /dev/null \
|
||||
|| bashio::log.warning "caveman install failed (offline?)"
|
||||
fi
|
||||
else
|
||||
bashio::log.info "Disabling caveman Claude Code plugin"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)"
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
SETTINGS_PATH="$HOME/.claude/settings.json"
|
||||
STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict|auto|bypass) ;;
|
||||
*)
|
||||
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
||||
PERMISSION_MODE="strict"
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
mode = os.environ["PERMISSION_MODE"]
|
||||
settings_path = Path(os.environ["SETTINGS_PATH"])
|
||||
state_path = Path(os.environ["STATE_PATH"])
|
||||
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||
except (OSError, json.JSONDecodeError):
|
||||
if settings_path.exists():
|
||||
settings_path.rename(settings_path.with_suffix(settings_path.suffix + ".bak"))
|
||||
settings = {}
|
||||
if not isinstance(settings, dict):
|
||||
settings = {}
|
||||
|
||||
try:
|
||||
state = json.loads(state_path.read_text()) if state_path.exists() else None
|
||||
except (OSError, json.JSONDecodeError):
|
||||
state = None
|
||||
if not isinstance(state, dict):
|
||||
state = None
|
||||
|
||||
permissions = settings.get("permissions")
|
||||
if not isinstance(permissions, dict):
|
||||
permissions = {}
|
||||
|
||||
if mode == "strict":
|
||||
# Restore the value that existed before the add-on first managed this setting.
|
||||
if state is not None:
|
||||
if state.get("previous_exists"):
|
||||
permissions["defaultMode"] = state.get("previous_value")
|
||||
else:
|
||||
permissions.pop("defaultMode", None)
|
||||
state_path.unlink(missing_ok=True)
|
||||
else:
|
||||
if state is None:
|
||||
state = {
|
||||
"previous_exists": "defaultMode" in permissions,
|
||||
"previous_value": permissions.get("defaultMode"),
|
||||
}
|
||||
state_path.write_text(json.dumps(state, indent=2) + "\n")
|
||||
state_path.chmod(0o600)
|
||||
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
|
||||
|
||||
if permissions:
|
||||
settings["permissions"] = permissions
|
||||
else:
|
||||
settings.pop("permissions", None)
|
||||
|
||||
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
||||
settings_path.chmod(0o600)
|
||||
PY
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict)
|
||||
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
||||
;;
|
||||
auto)
|
||||
bashio::log.info "Claude Code permission mode: auto (safe actions approved automatically)"
|
||||
;;
|
||||
bypass)
|
||||
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
||||
;;
|
||||
esac
|
||||
|
||||
chown -- "${PUID}:${PGID}" "$SETTINGS_PATH" 2> /dev/null || true
|
||||
if [ -e "$STATE_PATH" ]; then
|
||||
chown -- "${PUID}:${PGID}" "$STATE_PATH" 2> /dev/null || true
|
||||
fi
|
||||
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code.
|
||||
declare port=8787
|
||||
# Bind all interfaces so the dashboard is reachable on the mapped host port
|
||||
# (http://<ha-ip>:8787/dashboard). Local consumers keep using 127.0.0.1.
|
||||
declare host=0.0.0.0
|
||||
declare host=127.0.0.1
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom >/dev/null 2>&1; then
|
||||
# The dashboard is unauthenticated. Keep it container-local by default and bind all
|
||||
# interfaces only when the user explicitly opts in and maps port 8787.
|
||||
if bashio::config.true 'expose_headroom_dashboard'; then
|
||||
host=0.0.0.0
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
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
|
||||
|
||||
42
claude_desktop/rootfs/usr/local/bin/claude
Normal file
42
claude_desktop/rootfs/usr/local/bin/claude
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -o pipefail
|
||||
|
||||
REAL_CLAUDE="/usr/bin/claude"
|
||||
HEADROOM_BIN="/usr/local/bin/headroom"
|
||||
HEADROOM_URL="http://127.0.0.1:8787"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
declare -a CLAUDE_PERMISSION_ARGS=()
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
bypass)
|
||||
CLAUDE_PERMISSION_ARGS+=("--dangerously-skip-permissions")
|
||||
;;
|
||||
auto)
|
||||
CLAUDE_PERMISSION_ARGS+=("--permission-mode" "auto")
|
||||
;;
|
||||
strict|"")
|
||||
;;
|
||||
*)
|
||||
echo "claude wrapper: unknown permission_mode '${PERMISSION_MODE}', using strict mode" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -x "$REAL_CLAUDE" ]; then
|
||||
echo "claude wrapper: ${REAL_CLAUDE} is unavailable" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom' && bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
if [ -x "$HEADROOM_BIN" ] && curl -fsS --max-time 2 "${HEADROOM_URL}/health" > /dev/null 2>&1; then
|
||||
# Put /usr/bin before /usr/local/bin while Headroom resolves its upstream `claude`
|
||||
# executable; otherwise it would resolve this wrapper recursively.
|
||||
export HEADROOM_CONTEXT_TOOL="rtk"
|
||||
exec env PATH="/usr/bin:/bin:/usr/local/bin" \
|
||||
"$HEADROOM_BIN" wrap claude --no-proxy -- \
|
||||
"${CLAUDE_PERMISSION_ARGS[@]}" "$@"
|
||||
fi
|
||||
echo "claude wrapper: Headroom proxy is unavailable; launching Claude Code directly" >&2
|
||||
fi
|
||||
|
||||
exec "$REAL_CLAUDE" "${CLAUDE_PERMISSION_ARGS[@]}" "$@"
|
||||
@@ -1,34 +1,44 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Hourly rtk + headroom token-savings snapshot for the add-on log.
|
||||
# Invoked by cron (see /defaults/crontabs/root); its stdout is redirected to /proc/1/fd/1,
|
||||
# so the report appears in the add-on log. Doubles as a heartbeat: if the numbers stop
|
||||
# growing, the corresponding tool has stopped working.
|
||||
# with-contenv supplies HOME from the s6 envdir, so this honors a custom `data_location`
|
||||
# (see 20-folders.sh) instead of hardcoding /data/data; it also makes bashio::config
|
||||
# available for the install_headroom gate below.
|
||||
export NO_COLOR=1 # keep the add-on log free of ANSI color codes
|
||||
# Hourly RTK + Headroom + TokenSave token-savings snapshot for the add-on log.
|
||||
# Invoked by cron (see /defaults/crontabs/root); stdout is redirected to /proc/1/fd/1.
|
||||
# Each tool is reported independently so enabling Headroom cannot hide RTK or TokenSave data.
|
||||
# with-contenv supplies the configured persistent HOME.
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
have_rtk=false; command -v rtk >/dev/null 2>&1 && have_rtk=true
|
||||
have_headroom=false; command -v headroom >/dev/null 2>&1 && have_headroom=true
|
||||
|
||||
# headroom is pip-installed unconditionally at build time, so its binary is on PATH even
|
||||
# when install_headroom is off — gate on the same config svc-headroom checks, and only
|
||||
# fall back to have_headroom as a secondary availability guard.
|
||||
headroom_enabled=false
|
||||
if bashio::config.true 'install_headroom' && $have_headroom; then
|
||||
headroom_enabled=true
|
||||
if ! bashio::config.true 'enable_tools_health_report'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Nothing to report if neither tool is active — stay quiet.
|
||||
if ! $have_rtk && ! $headroom_enabled; then exit 0; fi
|
||||
rtk_enabled=false
|
||||
headroom_enabled=false
|
||||
tokensave_enabled=false
|
||||
|
||||
echo "===== claude gains report $(date '+%Y-%m-%d %H:%M:%S') ====="
|
||||
if bashio::config.true 'install_rtk' && command -v rtk > /dev/null 2>&1; then
|
||||
rtk_enabled=true
|
||||
fi
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
headroom_enabled=true
|
||||
fi
|
||||
if bashio::config.true 'install_tokensave' && command -v tokensave > /dev/null 2>&1; then
|
||||
tokensave_enabled=true
|
||||
fi
|
||||
|
||||
if ! $rtk_enabled && ! $headroom_enabled && ! $tokensave_enabled; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "===== claude tools report $(date '+%Y-%m-%d %H:%M:%S') ====="
|
||||
if $headroom_enabled; then
|
||||
echo "--- headroom savings ---"
|
||||
headroom savings 2>&1 || echo "[warn] headroom savings failed"
|
||||
elif $have_rtk; then
|
||||
fi
|
||||
if $rtk_enabled; then
|
||||
echo "--- rtk gain ---"
|
||||
rtk gain 2>&1 || echo "[warn] rtk gain failed"
|
||||
fi
|
||||
echo "===== end gains report ====="
|
||||
if $tokensave_enabled; then
|
||||
echo "--- tokensave gain ---"
|
||||
tokensave gain --all --range 30d 2>&1 || echo "[warn] tokensave gain failed"
|
||||
fi
|
||||
echo "===== end claude tools report ====="
|
||||
|
||||
162
claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh
Normal file
162
claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
||||
# printing MCP environment values (which may contain the Home Assistant access token).
|
||||
# shellcheck shell=bash
|
||||
set +e
|
||||
set -o pipefail
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
section() {
|
||||
printf '\n=== %s ===\n' "$1"
|
||||
}
|
||||
|
||||
section "Installed binaries"
|
||||
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
||||
if [ -n "$resolved" ]; then
|
||||
printf '%-16s %s\n' "$tool" "$resolved"
|
||||
else
|
||||
printf '%-16s %s\n' "$tool" "MISSING"
|
||||
fi
|
||||
done
|
||||
|
||||
section "Configured switches"
|
||||
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
|
||||
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
||||
done
|
||||
|
||||
section "Claude Code permission state"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("settings: MISSING")
|
||||
except Exception as exc:
|
||||
print(f"settings: INVALID: {exc}")
|
||||
else:
|
||||
permissions = data.get("permissions", {})
|
||||
if isinstance(permissions, dict):
|
||||
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
||||
else:
|
||||
print("permissions: INVALID")
|
||||
print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}")
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
paths = [
|
||||
Path.home() / ".claude.json",
|
||||
Path.home() / ".config/Claude/claude_desktop_config.json",
|
||||
]
|
||||
for path in paths:
|
||||
print(path)
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print(" MISSING")
|
||||
continue
|
||||
except Exception as exc:
|
||||
print(f" INVALID: {exc}")
|
||||
continue
|
||||
servers = data.get("mcpServers", {})
|
||||
if not isinstance(servers, dict) or not servers:
|
||||
print(" no MCP servers")
|
||||
continue
|
||||
for name, spec in sorted(servers.items()):
|
||||
if not isinstance(spec, dict):
|
||||
print(f" {name}: invalid entry")
|
||||
continue
|
||||
command = spec.get("command", "?")
|
||||
args = spec.get("args", [])
|
||||
server_type = spec.get("type", "")
|
||||
suffix = f" type={server_type}" if server_type else ""
|
||||
print(f" {name}: {command} {args}{suffix}")
|
||||
if spec.get("env"):
|
||||
print(" env: <redacted>")
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("MISSING")
|
||||
raise SystemExit(0)
|
||||
except Exception as exc:
|
||||
print(f"INVALID: {exc}")
|
||||
raise SystemExit(0)
|
||||
|
||||
hooks = data.get("hooks", {})
|
||||
if not isinstance(hooks, dict) or not hooks:
|
||||
print("no hooks")
|
||||
raise SystemExit(0)
|
||||
for event, entries in hooks.items():
|
||||
print(event)
|
||||
if not isinstance(entries, list):
|
||||
print(" invalid entries")
|
||||
continue
|
||||
for entry in entries:
|
||||
matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?"
|
||||
commands = entry.get("hooks", []) if isinstance(entry, dict) else []
|
||||
rendered = []
|
||||
for command in commands if isinstance(commands, list) else []:
|
||||
if isinstance(command, dict):
|
||||
rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))]))
|
||||
print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}")
|
||||
PY
|
||||
|
||||
section "Headroom"
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED"
|
||||
headroom mcp status || true
|
||||
headroom savings || true
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "RTK"
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
rtk gain || true
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "TokenSave"
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
tokensave doctor --agent claude || true
|
||||
tokensave gain --all --range 30d || true
|
||||
while IFS= read -r configured_path; do
|
||||
[ -n "$configured_path" ] || continue
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "${configured_path}: not a Git repository"
|
||||
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
|
||||
else
|
||||
echo "${repo_root}: NOT INITIALIZED"
|
||||
fi
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "Claude routing"
|
||||
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
|
||||
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"
|
||||
if bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
echo "PATH-based Claude Code launches are configured for Headroom wrapping."
|
||||
else
|
||||
echo "Claude Code Headroom wrapping is disabled; Headroom remains available through MCP."
|
||||
fi
|
||||
Reference in New Issue
Block a user