mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-02 18:23:30 +02:00
Merge pull request #2875 from alexbelgium/simplify/claude-desktop
fix(claude_desktop): fix silent TokenSave indexing, simplify startup, bump tokensave
This commit is contained in:
@@ -1,3 +1,16 @@
|
|||||||
|
## 1.32 (17-07-2026)
|
||||||
|
|
||||||
|
- Bump `tokensave` from 7.2.0 to 7.4.0 (`rtk` was already pinned to its current latest GitHub release, `v0.43.0`; `headroom-ai` is intentionally installed unpinned from PyPI, so it already tracks latest at every build and had nothing to bump). Reviewed the intervening 7.3.0/7.4.0 release notes against every tokensave surface this add-on drives (`install --agent claude --git-hook yes`, `uninstall --agent claude`, `sync`, `init`, `doctor --agent claude`, `gain --all --range 30d`, and the `mcp__tokensave__*` tool set granted in `settings.json`): no flag, output shape, or MCP tool name used here changed. Directly relevant fixes carried along: `tokensave sync` auto-migrates a v12 database (missing the trait-dispatch caller cache) to v13 as a normal part of syncing, which the add-on's corruption-quarantine logic won't mistake for corruption since the schema migration doesn't produce a "malformed"/"not a database" error; and `install`/`uninstall`'s JSON writer now resolves a symlinked `~/.claude/settings.json` before its atomic rename instead of replacing the symlink with a plain file, so a dotfiles-managed settings file survives untouched.
|
||||||
|
- Fix startup TokenSave repository preparation silently doing nothing: both `81-tokensave_repositories.sh` and the indexing loop in `82-claude_tools.sh` read `tokensave_project_paths` through `done < <(bashio::config ...)`, but `bashio::config` begins with a `read -d ''` heredoc that always returns non-zero, and a process substitution inherits the `errexit` enabled by the bashio wrapper itself — so the subshell died before printing and every boot iterated over an empty list (no `safe.directory` persistence, no startup `sync`/`init`; command substitutions were unaffected because subshells drop `errexit` when `inherit_errexit` is off, which is why every other option lookup worked). Repositories only got indexed when tokensave's own git hooks or a manual `tokensave init` happened to run. The list is now captured with a command substitution first and the loop reads from the captured variable (here-string); same fix applied to `claude-tools-doctor.sh`. The indexing loop also moved ahead of the MCP-registration merge, and the merge now re-tightens the 0600 mode on the token-bearing configs even on no-change boots — a first-time `tokensave init` rewrites `~/.claude.json` itself at default permissions, which previously could leave the stored Home Assistant token world-readable until the next registration change.
|
||||||
|
- Simplification pass over the startup logic: every remaining line now serves a live purpose, with no change to what gets configured — Headroom (proxy routing, MCP registration, CLAUDE.md guidance, PostToolUse auto-compression, dashboard exposure), RTK (global files + PreToolUse hook), and TokenSave (full agent integration + per-repo indexing) are still applied automatically to every new session type (terminal, Desktop cowork/dispatch, cron).
|
||||||
|
- `82-claude_tools.sh`: the three hand-rolled `~/.claude/settings.json` hook mutators (rtk add, rtk remove, headroom PostToolUse) are replaced by one shared `manage_settings_hook` helper using the proven strip-then-re-append pass (same dedup/matcher-migration semantics; additionally no longer creates an empty `settings.json` when asked to remove a hook from a machine that never had one). The two copy-pasted CLAUDE.md guidance managers (headroom, ha-api-helper) collapse into one `manage_claude_md_block` helper producing byte-identical blocks, so existing installs are recognized without a rewrite.
|
||||||
|
- `81-tokensave_repositories.sh` is merged into the TokenSave loop of `82-claude_tools.sh`: the same path list was parsed twice with identical trimming/validation only so `safe.directory` could be persisted before repository detection ran as root. Detection now runs directly as the runtime user with a one-shot `safe.directory` override (the persisted entry is still written for tokensave's git hooks and Claude sessions), removing the duplicate loop and the root-reads-abc-gitconfig coupling. The battle-tested defensive sync/init block (flock, retries, corruption quarantine, init sentinel) is unchanged.
|
||||||
|
- The `/tmp/claude-desktop-command` indirection is gone: `82-claude_tools.sh` wrote the default launch command to a file that only `defaults/autostart` read, with the identical default hardcoded as its fallback — nothing else ever wrote it. `autostart` now launches Claude Desktop directly (keyring bootstrap unchanged).
|
||||||
|
- `82-claude_tools.sh` no longer ends with its own recursive chown of `~/.claude`, `~/.claude.json`, and `~/.config/Claude`: `84-claude_runtime_ownership.sh` already reconciles exactly those paths after all Claude configuration scripts have run.
|
||||||
|
- `83-claude_permissions.sh` drops the hidden `.addon-permission-mode.json` state file in favor of the same managed-value semantics used for `ANTHROPIC_BASE_URL`: `auto`/`bypass` set `permissions.defaultMode`, `strict` removes it only while it still holds an add-on-managed value (`auto`/`bypassPermissions`), and a hand-set custom value is never deleted. The old restore-from-state behavior could resurrect a stale value recorded on the first managed boot; the stale state file is cleaned up on upgrade.
|
||||||
|
- `80-configuration.sh` sheds branches that were unreachable in this image: `apk`/`pacman` installers (the base is Debian), the `pip3` fallback (`uv` is always baked in), and the no-op timezone error path (invalid `TZ` values are now actually detected against `/usr/share/zoneinfo` before the symlink is written).
|
||||||
|
- Remove the dead `auto_update` option from `config.yaml` and the README: its schema entry was removed back in 1.22 and `81-claude_update.sh` has updated Claude Desktop unconditionally (best-effort, offline-safe) ever since; the README now states that behavior instead of documenting a switch that did nothing.
|
||||||
|
|
||||||
## 1.31 (16-07-2026)
|
## 1.31 (16-07-2026)
|
||||||
|
|
||||||
- Pin the LinuxServer selkies base image to a fixed version (`…-debianbookworm-45960cc3-ls113`) instead of the rolling `…-debianbookworm` tag. The rolling tag is rebuilt continuously (and itself installs selkies "latest" at base-build time), so the desktop/stream runtime could change under the add-on with no change to its own files — builds are now reproducible and the base only moves when this value is bumped deliberately. The pinned tags resolve to exactly the image the rolling tag currently points at (amd64 `sha256:6a4d5154…`, aarch64 `sha256:90914dfd…`).
|
- Pin the LinuxServer selkies base image to a fixed version (`…-debianbookworm-45960cc3-ls113`) instead of the rolling `…-debianbookworm` tag. The rolling tag is rebuilt continuously (and itself installs selkies "latest" at base-build time), so the desktop/stream runtime could change under the add-on with no change to its own files — builds are now reproducible and the base only moves when this value is bumped deliberately. The pinned tags resolve to exactly the image the rolling tag currently points at (amd64 `sha256:6a4d5154…`, aarch64 `sha256:90914dfd…`).
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ ARG BUILD_FROM
|
|||||||
ARG BUILD_VERSION
|
ARG BUILD_VERSION
|
||||||
ARG RTK_VERSION="v0.43.0"
|
ARG RTK_VERSION="v0.43.0"
|
||||||
ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f"
|
ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f"
|
||||||
ARG TOKENSAVE_VERSION="7.2.0"
|
ARG TOKENSAVE_VERSION="7.4.0"
|
||||||
|
|
||||||
# The upstream aarch64 release is cross-built on ubuntu-latest and requires
|
# The upstream aarch64 release is cross-built on ubuntu-latest and requires
|
||||||
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
|
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
|||||||
approval, or explicit full bypass for trusted installations.
|
approval, or explicit full bypass for trusted installations.
|
||||||
- Automatic non-root runtime enforcement for bypass mode, including root-console
|
- Automatic non-root runtime enforcement for bypass mode, including root-console
|
||||||
wrapper launches.
|
wrapper launches.
|
||||||
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
|
- Best-effort Claude Desktop update from Anthropic's apt repository at every
|
||||||
|
startup (skipped silently when offline).
|
||||||
- Optional extra apt and pip package installation (pip installs use `uv`).
|
- Optional extra apt and pip package installation (pip installs use `uv`).
|
||||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||||
`hadolint`, and `actionlint`.
|
`hadolint`, and `actionlint`.
|
||||||
@@ -95,7 +96,6 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
|||||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
| `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. |
|
|
||||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
| `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. |
|
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ options:
|
|||||||
data_location: /data/data
|
data_location: /data/data
|
||||||
additional_apps: ""
|
additional_apps: ""
|
||||||
additional_pip: ""
|
additional_pip: ""
|
||||||
auto_update: true
|
|
||||||
github_email: ""
|
github_email: ""
|
||||||
enable_ha_mcp: false
|
enable_ha_mcp: false
|
||||||
ha_mcp_url: http://homeassistant:8123/api/mcp
|
ha_mcp_url: http://homeassistant:8123/api/mcp
|
||||||
@@ -111,5 +110,5 @@ slug: claude_desktop
|
|||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.31"
|
version: "1.32"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
@@ -12,23 +12,7 @@ export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK
|
|||||||
dbus-update-activation-environment --all >/dev/null 2>&1 || true
|
dbus-update-activation-environment --all >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
|
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
|
||||||
# falling back to plaintext (and warning that the sign-in will not be saved).
|
# falling back to plaintext (and warning that the sign-in will not be saved). Headroom is
|
||||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
# intentionally not injected into the Desktop process: Claude Desktop force-overrides
|
||||||
DEFAULT_CLAUDE_DESKTOP_COMMAND="claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret"
|
# ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom MCP tools.
|
||||||
if [ -s "$CLAUDE_DESKTOP_COMMAND_FILE" ]; then
|
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
|
||||||
CLAUDE_DESKTOP_COMMAND="$(cat "$CLAUDE_DESKTOP_COMMAND_FILE")"
|
|
||||||
else
|
|
||||||
CLAUDE_DESKTOP_COMMAND="$DEFAULT_CLAUDE_DESKTOP_COMMAND"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides
|
|
||||||
# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead.
|
|
||||||
|
|
||||||
# Launch the configured command. If a custom/wrapped command fails to start, fall back to
|
|
||||||
# the plain Claude Desktop launch so the app always comes up for the user.
|
|
||||||
if ! sh -c "$CLAUDE_DESKTOP_COMMAND"; then
|
|
||||||
if [ "$CLAUDE_DESKTOP_COMMAND" != "$DEFAULT_CLAUDE_DESKTOP_COMMAND" ]; then
|
|
||||||
echo "autostart: '$CLAUDE_DESKTOP_COMMAND' failed; falling back to default Claude Desktop launch" >&2
|
|
||||||
exec sh -c "$DEFAULT_CLAUDE_DESKTOP_COMMAND"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,58 +1,48 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# shellcheck disable=SC2015
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# The image is Debian-based (apt) and always ships uv, so those are the only installers used.
|
||||||
if bashio::config.has_value 'additional_apps'; then
|
if bashio::config.has_value 'additional_apps'; then
|
||||||
bashio::log.info "Installing additional apps :"
|
bashio::log.info "Installing additional apps :"
|
||||||
NEWAPPS=$(bashio::config 'additional_apps')
|
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
|
||||||
if command -v "apt-get" &> /dev/null; then
|
for packagestoinstall in $(bashio::config 'additional_apps' | tr ',' ' '); do
|
||||||
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
|
|
||||||
fi
|
|
||||||
for packagestoinstall in ${NEWAPPS//,/ }; do
|
|
||||||
bashio::log.green "... $packagestoinstall"
|
bashio::log.green "... $packagestoinstall"
|
||||||
if command -v "apk" &> /dev/null; then
|
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || bashio::log.fatal "Error : $packagestoinstall not found"
|
||||||
apk add --no-cache "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
|
||||||
elif command -v "apt-get" &> /dev/null; then
|
|
||||||
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
|
||||||
elif command -v "pacman" &> /dev/null; then
|
|
||||||
pacman --noconfirm -S "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'additional_pip'; then
|
if bashio::config.has_value 'additional_pip'; then
|
||||||
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
|
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
|
||||||
bashio::log.green "... pip: $p"
|
bashio::log.green "... pip: $p"
|
||||||
# Prefer uv (much faster resolver/installer); fall back to pip3 when unavailable.
|
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||||
if command -v uv &> /dev/null; then
|
|
||||||
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
|
||||||
else
|
|
||||||
pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'TZ'; then
|
if bashio::config.has_value 'TZ'; then
|
||||||
TIMEZONE=$(bashio::config 'TZ')
|
TIMEZONE=$(bashio::config 'TZ')
|
||||||
bashio::log.info "Setting timezone to $TIMEZONE"
|
if [ -f "/usr/share/zoneinfo/$TIMEZONE" ]; then
|
||||||
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
|
bashio::log.info "Setting timezone to $TIMEZONE"
|
||||||
echo "$TIMEZONE" > /etc/timezone
|
ln -snf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime
|
||||||
fi || (bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html")
|
echo "$TIMEZONE" > /etc/timezone
|
||||||
|
else
|
||||||
|
bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'KEYBOARD'; then
|
if bashio::config.has_value 'KEYBOARD'; then
|
||||||
KEYBOARD=$(bashio::config 'KEYBOARD')
|
KEYBOARD=$(bashio::config 'KEYBOARD')
|
||||||
bashio::log.info "Setting keyboard to $KEYBOARD"
|
bashio::log.info "Setting keyboard to $KEYBOARD"
|
||||||
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
|
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
|
||||||
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2>/dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
|
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2> /dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
|
||||||
fi || true
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'PASSWORD'; then
|
if bashio::config.has_value 'PASSWORD'; then
|
||||||
bashio::log.info "Setting password to the value defined in options"
|
bashio::log.info "Setting password to the value defined in options"
|
||||||
PASSWORD=$(bashio::config 'PASSWORD')
|
PASSWORD=$(bashio::config 'PASSWORD')
|
||||||
passwd -d abc
|
passwd -d abc
|
||||||
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
|
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
|
||||||
elif ! bashio::config.has_value 'PASSWORD' && { [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]] }; then
|
elif [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]]; then
|
||||||
bashio::log.warning "SEVERE RISK IDENTIFIED"
|
bashio::log.warning "SEVERE RISK IDENTIFIED"
|
||||||
bashio::log.warning "You are opening an external port but your password is not defined"
|
bashio::log.warning "You are opening an external port but your password is not defined"
|
||||||
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
|
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/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=()
|
|
||||||
# bashio::config prints its result without a trailing newline, so the last record arrives
|
|
||||||
# with read returning non-zero; the extra test keeps that final path in the loop.
|
|
||||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
|
||||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
|
||||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
|
||||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
|
||||||
# bashio::config prints list options one entry per line ("null" when the key is absent);
|
|
||||||
# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here.
|
|
||||||
done < <(bashio::config 'tokensave_project_paths')
|
|
||||||
@@ -3,19 +3,125 @@
|
|||||||
set -e
|
set -e
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
|
|
||||||
# mode), so follow abc instead of re-reading the raw PUID/PGID options here.
|
|
||||||
RUNTIME_UID="$(id -u abc)"
|
|
||||||
RUNTIME_GID="$(id -g abc)"
|
|
||||||
mkdir -p "$HOME/.claude"
|
mkdir -p "$HOME/.claude"
|
||||||
|
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||||
|
|
||||||
run_as_runtime_user() {
|
run_as_runtime_user() {
|
||||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
# Managed, idempotent guidance block in the user's global CLAUDE.md, delimited by
|
||||||
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
|
# "<!-- BEGIN/END <name> (managed by claude_desktop addon) -->" markers. `add` appends the
|
||||||
printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE"
|
# block (body on stdin) unless the marker is already present; `remove` strips the whole
|
||||||
|
# block, surrounding blank padding included, and leaves everything else untouched.
|
||||||
|
manage_claude_md_block() {
|
||||||
|
local name="$1" action="$2"
|
||||||
|
local begin="<!-- BEGIN ${name} (managed by claude_desktop addon) -->"
|
||||||
|
if [ "$action" = "add" ]; then
|
||||||
|
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; }; then
|
||||||
|
bashio::log.info "Adding ${name} guidance to CLAUDE.md"
|
||||||
|
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||||
|
{
|
||||||
|
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||||
|
printf '%s\n' "$begin"
|
||||||
|
cat
|
||||||
|
printf '%s\n' "<!-- END ${name} (managed by claude_desktop addon) -->"
|
||||||
|
} >> "$CLAUDE_MD"
|
||||||
|
fi
|
||||||
|
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
|
||||||
|
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
|
||||||
|
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path(os.environ["CLAUDE_MD"])
|
||||||
|
name = re.escape(os.environ["BLOCK_NAME"])
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
pattern = re.compile(
|
||||||
|
rf"\n*<!-- BEGIN {name} \(managed by claude_desktop addon\) -->.*?"
|
||||||
|
rf"<!-- END {name} \(managed by claude_desktop addon\) -->\n?",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
new = pattern.sub("", text)
|
||||||
|
if new != text:
|
||||||
|
path.write_text(new, encoding="utf-8")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Managed hook entry in ~/.claude/settings.json (settings hooks apply to terminal, cowork,
|
||||||
|
# dispatch and cron sessions alike). The managed command is stripped everywhere first and
|
||||||
|
# re-appended when adding, so one pass handles removal, de-duplication, and matcher migration
|
||||||
|
# on upgrades; hooks owned by other tools (e.g. tokensave's own entries) are preserved, and
|
||||||
|
# the final text comparison keeps the write idempotent across boots.
|
||||||
|
manage_settings_hook() {
|
||||||
|
# manage_settings_hook <event> <matcher> <command> <add|remove>
|
||||||
|
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
|
||||||
|
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
event = os.environ["HOOK_EVENT"]
|
||||||
|
matcher = os.environ["HOOK_MATCHER"]
|
||||||
|
command = os.environ["HOOK_COMMAND"]
|
||||||
|
action = os.environ["HOOK_ACTION"]
|
||||||
|
|
||||||
|
path = Path.home() / ".claude" / "settings.json"
|
||||||
|
original = path.read_text() if path.exists() else None
|
||||||
|
if original is None and action != "add":
|
||||||
|
raise SystemExit(0)
|
||||||
|
try:
|
||||||
|
data = json.loads(original) if original is not None else {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = {}
|
||||||
|
except Exception:
|
||||||
|
if action != "add":
|
||||||
|
raise SystemExit(0)
|
||||||
|
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||||
|
original = None
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
||||||
|
entries = hooks.get(event) if isinstance(hooks.get(event), list) else []
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
for entry in entries:
|
||||||
|
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
|
||||||
|
filtered.append(entry)
|
||||||
|
continue
|
||||||
|
kept = [
|
||||||
|
item
|
||||||
|
for item in entry["hooks"]
|
||||||
|
if not (isinstance(item, dict) and item.get("command") == command)
|
||||||
|
]
|
||||||
|
if len(kept) != len(entry["hooks"]):
|
||||||
|
if not kept:
|
||||||
|
continue
|
||||||
|
entry = dict(entry)
|
||||||
|
entry["hooks"] = kept
|
||||||
|
filtered.append(entry)
|
||||||
|
entries = filtered
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
entries.append({"matcher": matcher, "hooks": [{"type": "command", "command": command}]})
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
hooks[event] = entries
|
||||||
|
else:
|
||||||
|
hooks.pop(event, None)
|
||||||
|
if hooks:
|
||||||
|
data["hooks"] = hooks
|
||||||
|
else:
|
||||||
|
data.pop("hooks", None)
|
||||||
|
|
||||||
|
serialized = json.dumps(data, indent=2) + "\n"
|
||||||
|
if serialized != original:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(serialized)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
# 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
|
# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore
|
||||||
@@ -57,6 +163,136 @@ elif command -v tokensave &> /dev/null; then
|
|||||||
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# Runs before the MCP registration merge below on purpose: a first-time `tokensave init` also
|
||||||
|
# rewrites ~/.claude.json itself (at default permissions), and the merge afterwards reconciles
|
||||||
|
# the managed entries and re-tightens the file mode around the stored HA token.
|
||||||
|
if $TOKENSAVE_ENABLED; then
|
||||||
|
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||||
|
# Capture the list BEFORE looping: bashio::config's internals trip the errexit that
|
||||||
|
# process substitution inherits from the bashio wrapper (a `read -d ''` that always
|
||||||
|
# returns non-zero), so `done < <(bashio::config ...)` silently fed the loop an EMPTY
|
||||||
|
# list — the startup index/sync never ran. Command substitution runs without errexit
|
||||||
|
# (inherit_errexit is off), making this form reliable. bashio::config prints list
|
||||||
|
# entries one per line, without a trailing newline and as "null" when the key is absent
|
||||||
|
# (bashio::config.array only exists in the repo's standalone bashio, not the real one
|
||||||
|
# here); the `|| [ -n ... ]` test keeps the final unterminated record in the loop.
|
||||||
|
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||||
|
while IFS= read -r configured_path || [ -n "$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:]]}"}"
|
||||||
|
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# The one-shot safe.directory override is used only to discover the repository root.
|
||||||
|
repo_root="$(run_as_runtime_user git -c safe.directory='*' -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
|
||||||
|
|
||||||
|
# Persist the resolved root in the runtime user's Git config so the sync/init below,
|
||||||
|
# tokensave's git hooks, and Claude sessions all pass Git's dubious-ownership check.
|
||||||
|
if ! run_as_runtime_user git config --global --get-all safe.directory \
|
||||||
|
| grep -Fxq -- "$repo_root"; then
|
||||||
|
run_as_runtime_user git config --global --add safe.directory "$repo_root"
|
||||||
|
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
||||||
|
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
||||||
|
# hiccup can never leave a broken index that fails every subsequent boot:
|
||||||
|
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
||||||
|
# post-commit/checkout sync hook that fires mid-boot); waits up to 60s for the
|
||||||
|
# other writer to finish rather than silently skipping, since a held lock clears
|
||||||
|
# itself the moment its holder exits or dies (the kernel releases flock on exit);
|
||||||
|
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
|
||||||
|
# times because SQLITE_BUSY under lock contention is transient, not corruption;
|
||||||
|
# * quarantine is reserved for sync failures whose stderr actually names database
|
||||||
|
# corruption (SQLite's own "malformed"/"not a database"/"disk image" wording) or
|
||||||
|
# a half-written index from an interrupted `init` (sentinel-flagged). Any other
|
||||||
|
# failure (permissions, disk full, missing binary, ...) leaves the existing index
|
||||||
|
# untouched and simply retries on the next start — corruption should self-heal,
|
||||||
|
# a transient environment problem should not nuke a healthy graph;
|
||||||
|
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
|
||||||
|
# as incomplete on the next start and rebuilt rather than trusted.
|
||||||
|
# All file operations run as the abc runtime user because the repo `.tokensave`
|
||||||
|
# directory is not covered by the startup ownership pass.
|
||||||
|
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
||||||
|
run_as_runtime_user bash -c '
|
||||||
|
set -o pipefail
|
||||||
|
repo_root="$1"
|
||||||
|
ts_dir="$repo_root/.tokensave"
|
||||||
|
db="$ts_dir/tokensave.db"
|
||||||
|
lock="$ts_dir/.startup.lock"
|
||||||
|
initflag="$ts_dir/.init-incomplete"
|
||||||
|
mkdir -p "$ts_dir"
|
||||||
|
exec 9>"$lock"
|
||||||
|
if ! flock -w 60 9; then
|
||||||
|
echo "TokenSave: index still locked for $repo_root after 60s; skipping startup sync" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
is_corruption() {
|
||||||
|
printf "%s" "$1" | grep -qiE "malformed|not a database|file is encrypted|disk image|database.*corrupt"
|
||||||
|
}
|
||||||
|
quarantine() {
|
||||||
|
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
bdir="$ts_dir/corrupt-$stamp"
|
||||||
|
mkdir -p "$bdir"
|
||||||
|
for f in "$db" "$db-wal" "$db-shm"; do
|
||||||
|
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
echo "TokenSave: quarantined suspect index to $bdir" >&2
|
||||||
|
}
|
||||||
|
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
|
||||||
|
attempt=1
|
||||||
|
while :; do
|
||||||
|
sync_err="$(tokensave sync "$repo_root" 2>&1 1>/dev/null)" && exit 0
|
||||||
|
[ "$attempt" -ge 3 ] && break
|
||||||
|
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if is_corruption "$sync_err"; then
|
||||||
|
echo "TokenSave: sync failed after retries for $repo_root (corruption detected); rebuilding index" >&2
|
||||||
|
quarantine
|
||||||
|
else
|
||||||
|
echo "TokenSave: sync failed after retries for $repo_root (no corruption signature); leaving index in place, will retry next start" >&2
|
||||||
|
echo "TokenSave: last sync error: $sync_err" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ -f "$db" ]; then
|
||||||
|
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
|
||||||
|
quarantine
|
||||||
|
fi
|
||||||
|
: > "$initflag"
|
||||||
|
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
|
||||||
|
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
|
||||||
|
exit 1
|
||||||
|
' _ "$repo_root" \
|
||||||
|
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
|
||||||
|
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||||
|
fi
|
||||||
|
|
||||||
HA_MCP_ENABLED=false
|
HA_MCP_ENABLED=false
|
||||||
HA_MCP_URL=""
|
HA_MCP_URL=""
|
||||||
HA_MCP_TOKEN=""
|
HA_MCP_TOKEN=""
|
||||||
@@ -157,144 +393,24 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
|||||||
elif existing is not None and is_managed(name, existing):
|
elif existing is not None and is_managed(name, existing):
|
||||||
del servers[name]
|
del servers[name]
|
||||||
changed = True
|
changed = True
|
||||||
if not changed:
|
if changed:
|
||||||
continue
|
if servers:
|
||||||
if servers:
|
data["mcpServers"] = servers
|
||||||
data["mcpServers"] = servers
|
else:
|
||||||
else:
|
data.pop("mcpServers", None)
|
||||||
data.pop("mcpServers", None)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
# The Home Assistant long-lived access token is stored here in clear text. Enforced even
|
||||||
# The Home Assistant long-lived access token is stored here in clear text.
|
# on no-change boots because tokensave's own writes can recreate the file with default
|
||||||
path.chmod(0o600)
|
# permissions between merges.
|
||||||
|
if path.exists():
|
||||||
|
path.chmod(0o600)
|
||||||
PY
|
PY
|
||||||
|
|
||||||
# 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=()
|
|
||||||
# bashio::config prints its result without a trailing newline, so the last record arrives
|
|
||||||
# with read returning non-zero; the extra test keeps that final path in the loop.
|
|
||||||
while IFS= read -r configured_path || [ -n "$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:]]}"}"
|
|
||||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
|
||||||
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
|
||||||
# hiccup can never leave a broken index that fails every subsequent boot:
|
|
||||||
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
|
||||||
# post-commit/checkout sync hook that fires mid-boot); waits up to 60s for the
|
|
||||||
# other writer to finish rather than silently skipping, since a held lock clears
|
|
||||||
# itself the moment its holder exits or dies (the kernel releases flock on exit);
|
|
||||||
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
|
|
||||||
# times because SQLITE_BUSY under lock contention is transient, not corruption;
|
|
||||||
# * quarantine is reserved for sync failures whose stderr actually names database
|
|
||||||
# corruption (SQLite's own "malformed"/"not a database"/"disk image" wording) or
|
|
||||||
# a half-written index from an interrupted `init` (sentinel-flagged). Any other
|
|
||||||
# failure (permissions, disk full, missing binary, ...) leaves the existing index
|
|
||||||
# untouched and simply retries on the next start — corruption should self-heal,
|
|
||||||
# a transient environment problem should not nuke a healthy graph;
|
|
||||||
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
|
|
||||||
# as incomplete on the next start and rebuilt rather than trusted.
|
|
||||||
# All file operations run as the abc runtime user because the repo `.tokensave`
|
|
||||||
# directory is not covered by this script's final ownership pass.
|
|
||||||
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
|
||||||
run_as_runtime_user bash -c '
|
|
||||||
set -o pipefail
|
|
||||||
repo_root="$1"
|
|
||||||
ts_dir="$repo_root/.tokensave"
|
|
||||||
db="$ts_dir/tokensave.db"
|
|
||||||
lock="$ts_dir/.startup.lock"
|
|
||||||
initflag="$ts_dir/.init-incomplete"
|
|
||||||
mkdir -p "$ts_dir"
|
|
||||||
exec 9>"$lock"
|
|
||||||
if ! flock -w 60 9; then
|
|
||||||
echo "TokenSave: index still locked for $repo_root after 60s; skipping startup sync" >&2
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
is_corruption() {
|
|
||||||
printf "%s" "$1" | grep -qiE "malformed|not a database|file is encrypted|disk image|database.*corrupt"
|
|
||||||
}
|
|
||||||
quarantine() {
|
|
||||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
||||||
bdir="$ts_dir/corrupt-$stamp"
|
|
||||||
mkdir -p "$bdir"
|
|
||||||
for f in "$db" "$db-wal" "$db-shm"; do
|
|
||||||
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
echo "TokenSave: quarantined suspect index to $bdir" >&2
|
|
||||||
}
|
|
||||||
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
|
|
||||||
attempt=1
|
|
||||||
while :; do
|
|
||||||
sync_err="$(tokensave sync "$repo_root" 2>&1 1>/dev/null)" && exit 0
|
|
||||||
[ "$attempt" -ge 3 ] && break
|
|
||||||
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
|
|
||||||
attempt=$((attempt + 1))
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
if is_corruption "$sync_err"; then
|
|
||||||
echo "TokenSave: sync failed after retries for $repo_root (corruption detected); rebuilding index" >&2
|
|
||||||
quarantine
|
|
||||||
else
|
|
||||||
echo "TokenSave: sync failed after retries for $repo_root (no corruption signature); leaving index in place, will retry next start" >&2
|
|
||||||
echo "TokenSave: last sync error: $sync_err" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [ -f "$db" ]; then
|
|
||||||
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
|
|
||||||
quarantine
|
|
||||||
fi
|
|
||||||
: > "$initflag"
|
|
||||||
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
|
|
||||||
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
|
|
||||||
exit 1
|
|
||||||
' _ "$repo_root" \
|
|
||||||
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
|
|
||||||
# bashio::config prints list options one entry per line ("null" when the key is absent);
|
|
||||||
# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here.
|
|
||||||
done < <(bashio::config 'tokensave_project_paths')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
# 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
|
# real savings when transparent proxying is unavailable.
|
||||||
# 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 $HEADROOM_ENABLED; then
|
if $HEADROOM_ENABLED; then
|
||||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
manage_claude_md_block headroom add <<'MD'
|
||||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
|
||||||
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
|
|
||||||
{
|
|
||||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
|
||||||
cat <<'MD'
|
|
||||||
<!-- BEGIN headroom (managed by claude_desktop addon) -->
|
|
||||||
## Headroom context compression
|
## Headroom context compression
|
||||||
|
|
||||||
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
||||||
@@ -304,28 +420,9 @@ search results, JSON/config dumps, big command outputs, roughly >500 tokens —
|
|||||||
the raw content. Call `mcp__headroom__headroom_retrieve` with that hash when you need the full
|
the raw content. Call `mcp__headroom__headroom_retrieve` with that hash when you need the full
|
||||||
original back. Skip compression for error/stack-trace output (Headroom deliberately protects it)
|
original back. Skip compression for error/stack-trace output (Headroom deliberately protects it)
|
||||||
and for small or one-off content. Use `mcp__headroom__headroom_stats` to check savings.
|
and for small or one-off content. Use `mcp__headroom__headroom_stats` to check savings.
|
||||||
<!-- END headroom (managed by claude_desktop addon) -->
|
|
||||||
MD
|
MD
|
||||||
} >> "$CLAUDE_MD"
|
else
|
||||||
fi
|
manage_claude_md_block headroom remove
|
||||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
|
||||||
bashio::log.info "Removing headroom usage guidance from CLAUDE.md"
|
|
||||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove headroom guidance automatically"
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(os.environ["CLAUDE_MD"])
|
|
||||||
text = path.read_text()
|
|
||||||
pattern = re.compile(
|
|
||||||
r"\n*<!-- BEGIN headroom \(managed by claude_desktop addon\) -->.*?"
|
|
||||||
r"<!-- END headroom \(managed by claude_desktop addon\) -->\n?",
|
|
||||||
re.DOTALL,
|
|
||||||
)
|
|
||||||
new = pattern.sub("", text)
|
|
||||||
if new != text:
|
|
||||||
path.write_text(new)
|
|
||||||
PY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
|
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
|
||||||
@@ -386,8 +483,7 @@ if changed:
|
|||||||
PY
|
PY
|
||||||
|
|
||||||
# Compress large tool outputs automatically in every Claude Code session via a managed
|
# Compress large tool outputs automatically in every Claude Code session via a managed
|
||||||
# PostToolUse hook (settings.json hooks apply to terminal, cowork, dispatch and cron sessions
|
# PostToolUse hook. Desktop-spawned sessions pin ANTHROPIC_BASE_URL to the production endpoint
|
||||||
# alike). Desktop-spawned sessions pin ANTHROPIC_BASE_URL to the production endpoint
|
|
||||||
# (headroom #869) so the proxy never sees their traffic, and the CLAUDE.md guidance above only
|
# (headroom #869) so the proxy never sees their traffic, and the CLAUDE.md guidance above only
|
||||||
# helps when the model remembers to call the MCP tools. The hook closes that gap: outputs over
|
# helps when the model remembers to call the MCP tools. The hook closes that gap: outputs over
|
||||||
# ~4000 chars from Bash/Grep/Glob/WebFetch are compressed with Headroom's rule-based pipeline
|
# ~4000 chars from Bash/Grep/Glob/WebFetch are compressed with Headroom's rule-based pipeline
|
||||||
@@ -405,84 +501,12 @@ if $HEADROOM_ENABLED && bashio::config.true 'headroom_auto_compress'; then
|
|||||||
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
|
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
HEADROOM_HOOK_ACTION="$HEADROOM_HOOK_ACTION" HEADROOM_HOOK_CMD="$HEADROOM_HOOK_CMD" \
|
manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD" "$HEADROOM_HOOK_ACTION"
|
||||||
HEADROOM_HOOK_MATCHER="Bash|Grep|Glob|WebFetch" \
|
|
||||||
python3 - <<'PY' || bashio::log.warning "Unable to manage the Headroom auto-compression hook"
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
action = os.environ["HEADROOM_HOOK_ACTION"]
|
|
||||||
command = os.environ["HEADROOM_HOOK_CMD"]
|
|
||||||
matcher = os.environ["HEADROOM_HOOK_MATCHER"]
|
|
||||||
|
|
||||||
path = Path.home() / ".claude" / "settings.json"
|
|
||||||
original = path.read_text() if path.exists() else None
|
|
||||||
try:
|
|
||||||
data = json.loads(original) if original is not None else {}
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
data = {}
|
|
||||||
except Exception:
|
|
||||||
if action != "add":
|
|
||||||
raise SystemExit(0)
|
|
||||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
|
||||||
original = None
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
|
||||||
entries = hooks.get("PostToolUse") if isinstance(hooks.get("PostToolUse"), list) else []
|
|
||||||
|
|
||||||
# Strip the managed command everywhere first, then re-append when enabled: the same pass
|
|
||||||
# handles removal, de-duplication, and matcher migration on version upgrades. The final
|
|
||||||
# text comparison keeps the write idempotent across boots.
|
|
||||||
filtered = []
|
|
||||||
for entry in entries:
|
|
||||||
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
|
|
||||||
filtered.append(entry)
|
|
||||||
continue
|
|
||||||
kept = [
|
|
||||||
item
|
|
||||||
for item in entry["hooks"]
|
|
||||||
if not (isinstance(item, dict) and item.get("command") == command)
|
|
||||||
]
|
|
||||||
if len(kept) != len(entry["hooks"]):
|
|
||||||
if not kept:
|
|
||||||
continue
|
|
||||||
entry = dict(entry)
|
|
||||||
entry["hooks"] = kept
|
|
||||||
filtered.append(entry)
|
|
||||||
entries = filtered
|
|
||||||
|
|
||||||
if action == "add":
|
|
||||||
entries.append({"matcher": matcher, "hooks": [{"type": "command", "command": command}]})
|
|
||||||
|
|
||||||
if entries:
|
|
||||||
hooks["PostToolUse"] = entries
|
|
||||||
else:
|
|
||||||
hooks.pop("PostToolUse", None)
|
|
||||||
if hooks:
|
|
||||||
data["hooks"] = hooks
|
|
||||||
else:
|
|
||||||
data.pop("hooks", None)
|
|
||||||
|
|
||||||
serialized = json.dumps(data, indent=2) + "\n"
|
|
||||||
if serialized != original:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(serialized)
|
|
||||||
PY
|
|
||||||
|
|
||||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
# 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
|
# `ha-cli` helper (no /config filesystem mount needed).
|
||||||
# the user's global CLAUDE.md; removed when the helper is disabled. Mirrors the headroom block.
|
|
||||||
HA_HELPER_GUIDE_BEGIN="<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->"
|
|
||||||
if bashio::config.true 'enable_ha_api_helper'; then
|
if bashio::config.true 'enable_ha_api_helper'; then
|
||||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
manage_claude_md_block ha-api-helper add <<'MD'
|
||||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
|
||||||
bashio::log.info "Adding Home Assistant API helper guidance to CLAUDE.md"
|
|
||||||
{
|
|
||||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
|
||||||
cat <<'MD'
|
|
||||||
<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->
|
|
||||||
## Configuring Home Assistant
|
## Configuring Home Assistant
|
||||||
|
|
||||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||||
@@ -502,120 +526,25 @@ Rules: run `ha-cli config` first to confirm connectivity; **read the current obj
|
|||||||
the user the intended change, then wait for confirmation** before any create/update/delete or
|
the user the intended change, then wait for confirmation** before any create/update/delete or
|
||||||
any state-changing `call`; after writing, read the object back and reload if needed
|
any state-changing `call`; after writing, read the object back and reload if needed
|
||||||
(e.g. `ha-cli call automation.reload`).
|
(e.g. `ha-cli call automation.reload`).
|
||||||
<!-- END ha-api-helper (managed by claude_desktop addon) -->
|
|
||||||
MD
|
MD
|
||||||
} >> "$CLAUDE_MD"
|
else
|
||||||
fi
|
manage_claude_md_block ha-api-helper remove
|
||||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
|
||||||
bashio::log.info "Removing Home Assistant API helper guidance from CLAUDE.md"
|
|
||||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove Home Assistant API helper guidance automatically"
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(os.environ["CLAUDE_MD"])
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
pattern = re.compile(
|
|
||||||
r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?"
|
|
||||||
r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?",
|
|
||||||
re.DOTALL,
|
|
||||||
)
|
|
||||||
new = pattern.sub("", text)
|
|
||||||
if new != text:
|
|
||||||
path.write_text(new, encoding="utf-8")
|
|
||||||
PY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.true 'install_rtk'; then
|
if bashio::config.true 'install_rtk'; then
|
||||||
if command -v rtk &> /dev/null; then
|
if command -v rtk &> /dev/null; then
|
||||||
bashio::log.info "Configuring rtk Claude Code integration"
|
bashio::log.info "Configuring rtk Claude Code integration"
|
||||||
|
# `rtk init -g` writes ~/.claude/RTK.md and its @RTK.md include in CLAUDE.md, but in
|
||||||
|
# non-interactive mode it deliberately refuses to patch settings.json, so the hook
|
||||||
|
# entry that actually rewrites Bash commands is registered here.
|
||||||
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||||
|| bashio::log.warning "rtk global files configuration failed"
|
|| bashio::log.warning "rtk global files configuration failed"
|
||||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
manage_settings_hook PreToolUse Bash "rtk hook claude" add
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
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 = {}
|
|
||||||
hooks = data.setdefault("hooks", {})
|
|
||||||
pre = hooks.setdefault("PreToolUse", [])
|
|
||||||
rtk_entry = {"matcher": "Bash", "hooks": [{"type": "command", "command": "rtk hook claude"}]}
|
|
||||||
if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance(entry, dict)):
|
|
||||||
pre.append(rtk_entry)
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
||||||
PY
|
|
||||||
else
|
else
|
||||||
bashio::log.warning "rtk is not available"
|
bashio::log.warning "rtk is not available"
|
||||||
fi
|
fi
|
||||||
elif [ -f "$HOME/.claude/settings.json" ]; then
|
else
|
||||||
bashio::log.info "Removing the add-on-managed rtk Claude Code hook"
|
manage_settings_hook PreToolUse Bash "rtk hook claude" remove
|
||||||
python3 - <<'PY' || bashio::log.warning "Unable to remove rtk hook automatically"
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path.home() / ".claude" / "settings.json"
|
|
||||||
data = json.loads(path.read_text())
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
raise TypeError("Claude settings must contain a JSON object")
|
|
||||||
|
|
||||||
hooks = data.get("hooks")
|
|
||||||
if not isinstance(hooks, dict):
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
entries = hooks.get("PreToolUse")
|
|
||||||
if not isinstance(entries, list):
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
changed = False
|
|
||||||
filtered_entries = []
|
|
||||||
for entry in entries:
|
|
||||||
if not isinstance(entry, dict) or entry.get("matcher") != "Bash":
|
|
||||||
filtered_entries.append(entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
commands = entry.get("hooks")
|
|
||||||
if not isinstance(commands, list):
|
|
||||||
filtered_entries.append(entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
filtered_commands = [
|
|
||||||
command
|
|
||||||
for command in commands
|
|
||||||
if not (
|
|
||||||
isinstance(command, dict)
|
|
||||||
and command.get("type") == "command"
|
|
||||||
and command.get("command") == "rtk hook claude"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
if len(filtered_commands) == len(commands):
|
|
||||||
filtered_entries.append(entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
changed = True
|
|
||||||
if filtered_commands:
|
|
||||||
updated_entry = dict(entry)
|
|
||||||
updated_entry["hooks"] = filtered_commands
|
|
||||||
filtered_entries.append(updated_entry)
|
|
||||||
|
|
||||||
if changed:
|
|
||||||
if filtered_entries:
|
|
||||||
hooks["PreToolUse"] = filtered_entries
|
|
||||||
else:
|
|
||||||
hooks.pop("PreToolUse", None)
|
|
||||||
if hooks:
|
|
||||||
data["hooks"] = hooks
|
|
||||||
else:
|
|
||||||
data.pop("hooks", None)
|
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
||||||
PY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.true 'install_caveman'; then
|
if bashio::config.true 'install_caveman'; then
|
||||||
@@ -631,10 +560,5 @@ else
|
|||||||
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
|
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed
|
# Ownership of everything written above is reconciled by 84-claude_runtime_ownership.sh after
|
||||||
# persistent files to the effective runtime UID/GID after all writes complete.
|
# the remaining Claude configuration scripts have run.
|
||||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
|
||||||
if [ -e "$managed_path" ]; then
|
|
||||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|||||||
@@ -3,31 +3,31 @@
|
|||||||
set -e
|
set -e
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
|
|
||||||
# mode), so follow abc instead of re-reading the raw PUID/PGID options here.
|
|
||||||
RUNTIME_UID="$(id -u abc)"
|
|
||||||
RUNTIME_GID="$(id -g abc)"
|
|
||||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||||
SETTINGS_PATH="$HOME/.claude/settings.json"
|
SETTINGS_PATH="$HOME/.claude/settings.json"
|
||||||
STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
|
|
||||||
|
|
||||||
case "$PERMISSION_MODE" in
|
case "$PERMISSION_MODE" in
|
||||||
strict|auto|bypass) ;;
|
strict | auto | bypass) ;;
|
||||||
*)
|
*)
|
||||||
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
||||||
PERMISSION_MODE="strict"
|
PERMISSION_MODE="strict"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Managed-value semantics, matching the ANTHROPIC_BASE_URL handling in 82-claude_tools.sh:
|
||||||
|
# auto/bypass set permissions.defaultMode to the add-on-managed value, and strict removes it
|
||||||
|
# only while it still holds one of those managed values — a defaultMode the user set by hand
|
||||||
|
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
|
||||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY'
|
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
MANAGED_VALUES = {"auto", "bypassPermissions"}
|
||||||
|
|
||||||
mode = os.environ["PERMISSION_MODE"]
|
mode = os.environ["PERMISSION_MODE"]
|
||||||
settings_path = Path(os.environ["SETTINGS_PATH"])
|
settings_path = Path(os.environ["SETTINGS_PATH"])
|
||||||
state_path = Path(os.environ["STATE_PATH"])
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||||
@@ -38,33 +38,14 @@ except (OSError, json.JSONDecodeError):
|
|||||||
if not isinstance(settings, dict):
|
if not isinstance(settings, dict):
|
||||||
settings = {}
|
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")
|
permissions = settings.get("permissions")
|
||||||
if not isinstance(permissions, dict):
|
if not isinstance(permissions, dict):
|
||||||
permissions = {}
|
permissions = {}
|
||||||
|
|
||||||
if mode == "strict":
|
if mode == "strict":
|
||||||
# Restore the value that existed before the add-on first managed this setting.
|
if permissions.get("defaultMode") in MANAGED_VALUES:
|
||||||
if state is not None:
|
permissions.pop("defaultMode")
|
||||||
if state.get("previous_exists"):
|
|
||||||
permissions["defaultMode"] = state.get("previous_value")
|
|
||||||
else:
|
|
||||||
permissions.pop("defaultMode", None)
|
|
||||||
state_path.unlink(missing_ok=True)
|
|
||||||
else:
|
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"
|
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
|
||||||
|
|
||||||
if permissions:
|
if permissions:
|
||||||
@@ -76,6 +57,9 @@ settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
|||||||
settings_path.chmod(0o600)
|
settings_path.chmod(0o600)
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
# Drop the state file older add-on versions used to remember the pre-add-on defaultMode.
|
||||||
|
rm -f "$HOME/.claude/.addon-permission-mode.json"
|
||||||
|
|
||||||
case "$PERMISSION_MODE" in
|
case "$PERMISSION_MODE" in
|
||||||
strict)
|
strict)
|
||||||
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
||||||
@@ -87,8 +71,3 @@ case "$PERMISSION_MODE" in
|
|||||||
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$SETTINGS_PATH" 2> /dev/null || true
|
|
||||||
if [ -e "$STATE_PATH" ]; then
|
|
||||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$STATE_PATH" 2> /dev/null || true
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ else:
|
|||||||
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
||||||
else:
|
else:
|
||||||
print("permissions: INVALID")
|
print("permissions: INVALID")
|
||||||
print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}")
|
|
||||||
PY
|
PY
|
||||||
|
|
||||||
section "MCP registrations (environment values redacted)"
|
section "MCP registrations (environment values redacted)"
|
||||||
@@ -149,6 +148,9 @@ section "TokenSave"
|
|||||||
if bashio::config.true 'install_tokensave'; then
|
if bashio::config.true 'install_tokensave'; then
|
||||||
tokensave doctor --agent claude || true
|
tokensave doctor --agent claude || true
|
||||||
tokensave gain --all --range 30d || true
|
tokensave gain --all --range 30d || true
|
||||||
|
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
|
||||||
|
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
|
||||||
|
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||||
continue
|
continue
|
||||||
@@ -161,7 +163,7 @@ if bashio::config.true 'install_tokensave'; then
|
|||||||
else
|
else
|
||||||
echo "${repo_root}: NOT INITIALIZED"
|
echo "${repo_root}: NOT INITIALIZED"
|
||||||
fi
|
fi
|
||||||
done < <(bashio::config 'tokensave_project_paths')
|
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||||
else
|
else
|
||||||
echo "disabled"
|
echo "disabled"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user