Compare commits

..
Author SHA1 Message Date
Alexandre bcc521015b Merge pull request #2876 from alexbelgium/agent/fix-free-games-claimer-build
Fix Free Games Claimer PR build context
2026-07-17 10:03:53 +02:00
Alexandre 6b46611a0b Fix Free Games Claimer PR build context 2026-07-17 09:59:48 +02:00
github-actions a72e03e88a GitHub bot: changelog [nobuild] 2026-07-17 07:48:27 +00:00
Alexandre 8042ac6786 Update config.yaml 2026-07-17 09:42:27 +02:00
Alexandre 5b7de5907f Update config.yaml 2026-07-17 09:28:29 +02:00
github-actions 0d344ecd6c Github bot : issues linked to readme 2026-07-17 07:27:38 +00:00
Alexandre f34848ae42 nobuild
Migrate Free Games Claimer to remaster upstream
2026-07-17 09:27:19 +02:00
Alexandre 61627e80bb Merge pull request #2875 from alexbelgium/simplify/claude-desktop
fix(claude_desktop): fix silent TokenSave indexing, simplify startup, bump tokensave
2026-07-17 09:26:19 +02:00
Alexandre 6f693f39dc Validate Free Games Claimer migration 2026-07-17 09:23:51 +02:00
alexbelgiumandClaude Sonnet 5 0397e2b849 fix(claude_desktop): fix silent TokenSave indexing, simplify startup logic, bump tokensave
TokenSave repository indexing was silently processing zero paths on every
boot: bashio::config's read -d '' always returns non-zero, and process
substitution inherits the errexit that the bashio wrapper enables, so
`done < <(bashio::config 'tokensave_project_paths')` died before printing
anything. Fixed by capturing with command substitution first.

Also a simplification pass over the startup scripts — three duplicated
settings.json hook mutators collapse into one helper, two duplicated
CLAUDE.md guidance managers collapse into another, 81-tokensave_repositories.sh
merges into 82's loop, and several dead code paths (apk/pacman installers,
pip3 fallback, the /tmp/claude-desktop-command indirection, a stale
auto_update option, a redundant chown pass) are removed. No change to what
gets configured — Headroom/RTK/TokenSave still auto-apply to every session
type. tokensave bumped 7.2.0 -> 7.4.0 (rtk and headroom-ai were already at
latest). See CHANGELOG.md for full detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 09:22:14 +02:00
Alexandre 865e1c8e6d Run final add-on validation 2026-07-17 09:21:32 +02:00
Alexandre 25945f5a37 Document pinned upstream policy 2026-07-17 09:20:45 +02:00
Alexandre feaf4f5c92 Document controlled upstream updates 2026-07-17 09:20:10 +02:00
Alexandre fa83755108 Pause unsafe automatic upstream bumps 2026-07-17 09:19:12 +02:00
Alexandre 101c252b71 Harden remaster build metadata 2026-07-17 09:18:58 +02:00
Alexandre 1f177e26f4 Preserve defaults for existing installs 2026-07-17 09:13:13 +02:00
Alexandre 9232156924 Fix remaster browser profile path 2026-07-17 09:11:01 +02:00
Alexandre 766abd5ec3 Trigger add-on validation 2026-07-17 09:09:59 +02:00
Alexandre 49fbbbf59c Improve legacy store mapping 2026-07-17 09:08:09 +02:00
Alexandre a55c410b9c Relax legacy config parsing 2026-07-17 09:06:57 +02:00
Alexandre e32a134ee2 Update Free Games Claimer changelog 2026-07-17 09:04:11 +02:00
Alexandre 0019b52f4a Document remaster migration 2026-07-17 09:03:54 +02:00
Alexandre a240dd8d00 Track remaster upstream 2026-07-17 09:03:20 +02:00
Alexandre 408f9ee05b Update remaster configuration template 2026-07-17 09:03:12 +02:00
Alexandre 324dc560c2 Migrate legacy claim history 2026-07-17 09:03:00 +02:00
Alexandre cc8b402942 Adapt launcher for remaster 2026-07-17 09:02:23 +02:00
Alexandre 8eb51526a1 Build Free Games Claimer Remaster 2026-07-17 09:01:50 +02:00
Alexandre 4c068c6792 Configure remaster runtime 2026-07-17 09:01:09 +02:00
Alexandre 476f02924d Switch Free Games Claimer upstream 2026-07-17 09:00:51 +02:00
github-actions 2964b762d1 Github bot : issues linked to readme 2026-07-17 06:37:57 +00:00
github-actions b942bcf336 Github bot : issues linked to readme 2026-07-17 06:37:33 +00:00
github-actions 3973e5901f GitHub bot: changelog [nobuild] 2026-07-16 21:12:59 +00:00
GitHub Actions 1b313e591d Revert "Updater bot : ente updated to 4.4.24"
This reverts commit 49dcc057d5.
2026-07-16 21:03:28 +00:00
GitHub Actions 49130bafc8 Revert "Updater bot : flexget updated to 3.19.27"
This reverts commit d9e08a447b.
2026-07-16 21:03:28 +00:00
GitHub Actions 6463cc964a Revert "Updater bot : gitea updated to 1.27.0"
This reverts commit acee58b78d.
2026-07-16 21:03:28 +00:00
GitHub Actions 3d2773db75 Revert "Updater bot : grav updated to 2.0.11"
This reverts commit 4a05b9a287.
2026-07-16 21:03:28 +00:00
GitHub Actions 6ad59a6e54 Revert "Updater bot : immich updated to 3.0.3"
This reverts commit 8d67dea5f1.
2026-07-16 21:03:28 +00:00
GitHub Actions 8ad03ce68c Revert "Updater bot : immich_cuda updated to 3.0.3"
This reverts commit 179863a7b0.
2026-07-16 21:03:28 +00:00
GitHub Actions 11de95228d Revert "Updater bot : immich_frame updated to 1.0.36.0"
This reverts commit f85151b52c.
2026-07-16 21:03:28 +00:00
GitHub Actions d6d09a0204 Revert "Updater bot : immich_noml updated to 3.0.3"
This reverts commit c3e81e8761.
2026-07-16 21:03:28 +00:00
GitHub Actions cd98281978 Revert "Updater bot : immich_openvino updated to 3.0.3"
This reverts commit 090b200cb1.
2026-07-16 21:03:28 +00:00
GitHub Actions f35f0baaeb Revert "Updater bot : jackett updated to 0.24.2226"
This reverts commit c7bc78f306.
2026-07-16 21:03:28 +00:00
GitHub Actions 5dcb1c222a Revert "Updater bot : linkwarden updated to 2.15.1"
This reverts commit 36e0800596.
2026-07-16 21:03:28 +00:00
GitHub Actions 1c981e187f Revert "Updater bot : maintainerr updated to 3.17.1"
This reverts commit 1b32956a2a.
2026-07-16 21:03:28 +00:00
GitHub Actions 3725f920e0 Revert "Updater bot : navidrome updated to 0.63.2"
This reverts commit 8ee044b697.
2026-07-16 21:03:28 +00:00
GitHub Actions e21496f18d Revert "Updater bot : nzbget updated to v26.2-ls253"
This reverts commit 6ad65abc8e.
2026-07-16 21:03:28 +00:00
GitHub Actions 4dd0e22a84 Revert "Updater bot : openproject updated to 17.6.0"
This reverts commit dcf8073734.
2026-07-16 21:03:28 +00:00
GitHub Actions fe60b6d6b1 Revert "Updater bot : plex updated to 1.43.3.10828-00f62d37d-ls315"
This reverts commit 63fcca6f74.
2026-07-16 21:03:28 +00:00
GitHub Actions f4877df9cc Revert "Update config.yaml"
This reverts commit 881821e17c.
2026-07-16 21:03:28 +00:00
GitHub Actions 8c1152501d Revert "Updater bot : prowlarr updated to nightly-2.5.1.5478-ls7"
This reverts commit 2bdf65187c.
2026-07-16 21:03:28 +00:00
GitHub Actions ac72fac79c Revert "Updater bot : social_to_mealie updated to 1.7.2"
This reverts commit 2fa921734b.
2026-07-16 21:03:28 +00:00
GitHub Actions bb51a80700 Revert "Updater bot : webtop_kde updated to 4.16-r0-ls95"
This reverts commit 1a50bbe522.
2026-07-16 21:03:28 +00:00
alexbelgium 1a50bbe522 Updater bot : webtop_kde updated to 4.16-r0-ls95 2026-07-16 23:02:41 +02:00
alexbelgium 2fa921734b Updater bot : social_to_mealie updated to 1.7.2 2026-07-16 23:01:15 +02:00
alexbelgium 2bdf65187c Updater bot : prowlarr updated to nightly-2.5.1.5478-ls7 2026-07-16 23:00:15 +02:00
Alexandre 881821e17c Update config.yaml 2026-07-16 23:00:05 +02:00
alexbelgium 63fcca6f74 Updater bot : plex updated to 1.43.3.10828-00f62d37d-ls315 2026-07-16 23:00:01 +02:00
alexbelgium dcf8073734 Updater bot : openproject updated to 17.6.0 2026-07-16 22:59:49 +02:00
alexbelgium 6ad65abc8e Updater bot : nzbget updated to v26.2-ls253 2026-07-16 22:59:39 +02:00
alexbelgium 8ee044b697 Updater bot : navidrome updated to 0.63.2 2026-07-16 22:59:21 +02:00
alexbelgium 1b32956a2a Updater bot : maintainerr updated to 3.17.1 2026-07-16 22:58:57 +02:00
alexbelgium 36e0800596 Updater bot : linkwarden updated to 2.15.1 2026-07-16 22:58:52 +02:00
alexbelgium c7bc78f306 Updater bot : jackett updated to 0.24.2226 2026-07-16 22:58:24 +02:00
alexbelgium 090b200cb1 Updater bot : immich_openvino updated to 3.0.3 2026-07-16 22:58:14 +02:00
alexbelgium c3e81e8761 Updater bot : immich_noml updated to 3.0.3 2026-07-16 22:58:08 +02:00
alexbelgium f85151b52c Updater bot : immich_frame updated to 1.0.36.0 2026-07-16 22:58:02 +02:00
alexbelgium 179863a7b0 Updater bot : immich_cuda updated to 3.0.3 2026-07-16 22:57:56 +02:00
alexbelgium 8d67dea5f1 Updater bot : immich updated to 3.0.3 2026-07-16 22:57:51 +02:00
alexbelgium 4a05b9a287 Updater bot : grav updated to 2.0.11 2026-07-16 22:57:43 +02:00
alexbelgium acee58b78d Updater bot : gitea updated to 1.27.0 2026-07-16 22:57:33 +02:00
alexbelgium d9e08a447b Updater bot : flexget updated to 3.19.27 2026-07-16 22:57:21 +02:00
alexbelgium 49dcc057d5 Updater bot : ente updated to 4.4.24 2026-07-16 22:56:54 +02:00
alexbelgium a6d93753e9 Updater bot : collabora updated to 1.3.0 2026-07-16 22:56:28 +02:00
alexbelgium 920591631d Updater bot : codex updated to 2.2.0 2026-07-16 22:56:23 +02:00
alexbelgium 15853bd661 Updater bot : changedetection.io updated to 0.55.8 2026-07-16 22:55:45 +02:00
alexbelgium cf918a2c2b Updater bot : browserless_chrome updated to 2.55.0 2026-07-16 22:55:26 +02:00
alexbelgium 680a947ced Updater bot : browser_chromium updated to version-ae9a7fca 2026-07-16 22:55:20 +02:00
alexbelgium b6fc7f2c3a Updater bot : browser_brave updated to 1.92.140-ls113 2026-07-16 22:55:00 +02:00
alexbelgium 9add537dc6 Updater bot : birdnet-go updated to 20260716 2026-07-16 22:53:52 +02:00
alexbelgium 99ccd61c14 Updater bot : autobrr updated to 1.82.1 2026-07-16 22:53:20 +02:00
Alexandre dc5c085598 Update config.yaml 2026-07-16 22:45:48 +02:00
Alexandre 778ef33359 Update version from 1.29 to 1.31 Nobuild 2026-07-16 22:16:39 +02:00
Alexandre 4804041920 Update version from 1.26 to 1.29 in config.yaml Nobuild 2026-07-16 22:00:16 +02:00
Alexandre 510bd46128 Downgrade version from 1.27 to 1.26 Nobuild 2026-07-16 21:34:56 +02:00
Alexandre 7a375df1de Change version to 1.27 nobuild
Downgrade version from 1.29 to 1.27 in config.yaml
2026-07-16 21:23:46 +02:00
Alexandre 295c3d912f Downgrade version from 1.31 to 1.29 Nobuild 2026-07-16 21:21:41 +02:00
Alexandre f4cfc359fa Merge pull request #2873 from alexbelgium/claude/claude-desktop-startup-hang-9t4brj
fix(claude_desktop): fix "waiting for stream" hang — GPU race + pin selkies base image
2026-07-16 20:37:53 +02:00
Claude 0b61865f36 fix(claude_desktop): pin selkies base image to a fixed version
build.json used the rolling ghcr.io/linuxserver/baseimage-selkies
:*-debianbookworm tag, which LinuxServer rebuilds continuously (and
which itself installs selkies "latest" at base-build time). The
desktop/stream runtime could therefore change under the add-on with no
change to its own files.

Pin both architectures to the current version (45960cc3-ls113). The
versioned tags resolve to exactly the image the rolling tag points at
today (amd64 sha256:6a4d5154..., aarch64 sha256:90914dfd...), so this is
a no-op for the current build but makes future builds reproducible; the
base now only moves when this value is bumped deliberately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iAzC9599AaN45Ko6RXtSW
2026-07-16 18:29:31 +00:00
Claude 8c763216c8 fix(claude_desktop): grant GPU access before graphical services start
The Selkies web client stayed on "waiting for stream" and Claude Desktop
never appeared, with "libEGL warning: failed to open /dev/dri/card0:
Permission denied" in the log.

The LinuxServer base image adds the desktop user (abc) to the /dev/dri
render group in its init-video s6 oneshot, but that oneshot is not a
dependency of svc-xorg/svc-selkies/svc-de. On Home Assistant those
long-running services start (via s6-setuidgid abc) before init-video has
added abc to the render group, so Xorg/Selkies/pixelflux open the render
device without permission and the video pipeline never produces frames.

Prepare the exposed DRI nodes in a new 21-gpu_permissions.sh cont-init
script: cont-init.d completes before any s6-rc service starts, so abc is
added to each node's owning group (and the node is made world read/write
as a timing-independent fallback) in time for the graphical services.
Best-effort and a no-op when no GPU is exposed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iAzC9599AaN45Ko6RXtSW
2026-07-16 17:31:31 +00:00
Alexandre dd885ceaee Update version from 1.29 to 1.30 Nobuild 2026-07-16 19:14:48 +02:00
Alexandre 51288da9d0 Nobuild 2026-07-16 19:09:42 +02:00
Alexandre 3d2f3aa193 Merge pull request #2871 from alexbelgium/feat/headroom-posttooluse-hook
feat(claude_desktop): auto-compress large tool outputs via Headroom PostToolUse hook
2026-07-16 14:53:16 +02:00
alexbelgiumandClaude Fable 5 26101a1104 fix(claude_desktop): guard env parsing and truncate file-list arrays in headroom hook
Two review findings on PR #2871:

- coderabbitai: MIN_CHARS/MIN_SAVED_TOKENS parsed with a bare int() at module
  import time, before any try/except could catch a bad value — a malformed
  env_vars passthrough would crash the hook on every matched tool call instead
  of failing open as documented. Wrapped in _int_env() with a safe fallback.

- chatgpt-codex-connector: Glob and Grep (files_with_matches mode) return a
  `filenames: string[]` field per the CLI's own output schema, which the
  hook's string-only candidate scan never touched — large file listings, the
  exact case named in the CLAUDE.md guidance this add-on installs, passed
  through uncompressed. Verified empirically that routing such an array
  through compress()/SmartCrusher (as done for JSON-blob string fields)
  silently subsamples it — 600 paths collapsed to ~15 with no visible marker,
  unsafe for paths the model needs to act on individually. Added a separate
  deterministic path: arrays over ARRAY_KEEP (40) entries are truncated in
  order with one labeled marker entry appended, full array recoverable from
  the CCR store by hash. Verified round-trip on Glob- and Grep-shaped
  payloads (600 and 200 entries); confirmed order preservation and that
  small arrays still pass through untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:46:10 +02:00
github-actions a4764364da GitHub bot: changelog [nobuild] 2026-07-16 12:37:00 +00:00
alexbelgiumandClaude Fable 5 9bbd72e70a feat(claude_desktop): auto-compress large tool outputs via Headroom PostToolUse hook
Desktop-spawned Claude Code sessions (cowork/dispatch) pin ANTHROPIC_BASE_URL
to the production endpoint (headroom #869), so the transparent proxy never
sees their traffic and compression depended on the model voluntarily calling
the headroom MCP tools. A managed PostToolUse hook now compresses
Bash/Grep/Glob/WebFetch outputs over ~4000 chars in every session type with
Headroom's rule-based pipeline, swapping them in via
hookSpecificOutput.updatedToolOutput with a retrieval marker. Originals live
in the shared CCR SQLite store, so mcp__headroom__headroom_retrieve recovers
them; savings land in the durable ledger (client "posttooluse-hook").

The hook fails open, never compresses stderr, skips sub-50-token savings, and
registers idempotently in ~/.claude/settings.json only after a --self-test
gate; new headroom_auto_compress option (default true) removes the managed
entry cleanly when disabled. Measured: 10781->2964 tokens (73%) on a
representative HA states dump, ~1.7 s hook overhead, <100 ms pass-through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:31:11 +02:00
Alexandre e345752d00 Update config.yaml 2026-07-16 14:30:21 +02:00
Alexandre 7e2c8eda5e Fix generated package-lock conflicts when merging review PRs 2026-07-16 14:28:31 +02:00
Alexandre 65fbc7dae2 Merge pull request #2870 from alexbelgium/fix/headroom-hf-home-and-bashrc-home-dedup
fix(claude_desktop): repair Headroom MCP model cache, HOME dedup, gitconfig ownership
2026-07-16 13:49:35 +02:00
Alexandre 95641d253c Update 83-github_cli.sh 2026-07-16 13:49:02 +02:00
alexbelgiumandClaude Opus 4.8 bdc6231aa3 fix(claude_desktop): repair Headroom MCP model cache, HOME dedup, gitconfig owner
Three add-on runtime-environment bugs, all found while investigating a Headroom
dashboard stuck at 0 gain.

Headroom MCP server had no HF_HOME. 1.27 fixed the Kompress model cache for the
svc-headroom proxy longrun by exporting HF_HOME there, but the MCP server is a
different process: Claude Desktop and Claude Code spawn it from the registered
mcpServers entry, so it never saw that export and kept resolving the HuggingFace
cache to ~/.cache, which this add-on symlinks to tmpfs. Its Kompress ML path
therefore never found the model, re-downloaded ~270 MB into tmpfs on every boot,
and lost it on the next one -- headroom_compress returned router:noop (output
unchanged) for prose and other unstructured content. Rule-based compression
(SmartCrusher, structured tool output) was unaffected and worked throughout,
which is why the failure only showed on some payloads. Carry env.HF_HOME on the
managed headroom entry in both claude_desktop_config.json and ~/.claude.json.

~/.gitconfig was written as root and left unreadable by abc. `git config --global`
ran as root during init and rewrites the file on every start, so 20-folders.sh's
earlier recursive chown never stuck to it; .config/gh survived abc-owned only
because the "already authenticated" branch skips rewriting it. The user that
actually runs git, gh and Claude could not read its own committer identity or the
gh credential helper: every commit failed with "Author identity unknown" and
authenticated pushes fell back to prompting. Run the git/gh setup as abc via
s6-setuidgid, matching 81-tokensave_repositories.sh, and reclaim root-owned
copies left by earlier versions before writing.

~/.bashrc accumulated stale HOME/FM_HOME exports across data_location changes.
The idempotency guard only tested for the current $LOCATION, so changing the
option and later changing it back appended a second block while leaving the first,
and the last one written won for every interactive shell. $HOME then pointed at a
directory the add-on no longer manages, so anything resolving config through it
read the wrong path -- `headroom doctor` reported "claude: not routed (no
~/.claude/settings.json)" against a correctly routed install, and bare `headroom`
invocations created a stray .headroom tree under the old location. Make the block
marker-delimited and rewrite it from scratch each boot.

Verified on a running add-on: headroom_compress now reports 1909 -> 1122 tokens
(41.2%, router:mixed) through the live MCP server; `headroom doctor` reports
"claude: routed via /data/data/.claude/settings.json"; and git commits work as abc
without a repo-local identity override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:36:05 +02:00
github-actions ae2c29b977 GitHub bot: changelog [nobuild] 2026-07-16 06:54:55 +00:00
Alexandre 3114a6cc94 Update config.yaml 2026-07-16 08:41:05 +02:00
Alexandre 297102e908 Update Dockerfile 2026-07-16 08:40:49 +02:00
github-actions 39efd5602f GitHub bot: changelog [nobuild] 2026-07-16 06:26:26 +00:00
Alexandre 8b3db6e325 Merge pull request #2869 from alexbelgium/fix/headroom-cowork-routing-ml
fix(claude_desktop): Headroom zero savings — cowork session routing + Kompress activation
2026-07-16 08:26:14 +02:00
Alexandre e70b8db0fa Update config.yaml 2026-07-16 08:24:28 +02:00
Alexandre 4872bd89c9 Remove healthcheck from Dockerfile 2026-07-16 08:24:18 +02:00
alexbelgiumandClaude Fable 5 b3c27024d8 fix(claude_desktop): remove blocking Kompress pre-warm, use proxy's own background loader
Codex flagged that the synchronous pre-warm (up to 300s) blocked the proxy port bind, defeating the terminal wrapper's health-check fallback and, combined with the new settings-managed ANTHROPIC_BASE_URL, could send terminal Claude Code launches to a proxy that was not listening yet.

The proxy already has a non-blocking answer to a cold cache: content_router.py calls compressor.ensure_background_load() on first use and passes the request through uncompressed until the model lands, so the port always binds immediately. Persisting HF_HOME alone is enough -- Kompress self-heals within the first couple of requests on a cold boot and loads instantly (eager preload) on every boot after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:20:56 +02:00
alexbelgiumandClaude Fable 5 b8c7cc3815 fix(claude_desktop): address CodeRabbit findings on TokenSave startup hardening
flock -n silently skipped TokenSave prep on lock contention with no retry until next restart; wait up to 60s instead (kernel releases flock the instant its owner exits, so only a truly stuck lock can't clear within that window).

Quarantine fired on any sync failure after 3 retries, including transient causes (permissions, disk full, missing binary) unrelated to corruption. Now only quarantines when stderr names actual database corruption (SQLite malformed/not-a-database/disk-image wording); other failures leave the index untouched and retry next start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 22:19:14 +02:00
alexbelgiumandClaude Fable 5 3354af026f fix(claude_desktop): make Headroom actually save tokens (cowork routing + Kompress)
Headroom kept reporting zero savings for two independent reasons:

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 20:55:27 +02:00
github-actions 3bff6b65de GitHub bot : README updated 2026-07-15 17:28:14 +00:00
Alexandre efc659f452 Merge pull request #2868 from alexbelgium/claude/claude-desktop-permissions-cm4k7w
fix(claude_desktop): align abc runtime identity so Claude Desktop can start
2026-07-15 19:15:23 +02:00
Claude 2f245c77e1 fix(claude_desktop): keep the final tokensave path from bashio::config
bashio::config prints its result via printf without a trailing newline, so
a plain while-read loop drops the last (often only) configured project path
and no TokenSave repository would be initialized. Use the
read || [ -n ... ] idiom in the three path loops so the final unterminated
record is still processed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KN8i26JrKSaBdvTrpVEyQ6
2026-07-15 16:49:56 +00:00
Claude 2347cb9eae style(claude_desktop): use explicit if for the null path guard (SC2015)
Codacy flagged the `A && B || continue` short-circuit pattern in the three
tokensave path loops; rewrite it as an explicit if so the fallback can never
run when both tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KN8i26JrKSaBdvTrpVEyQ6
2026-07-15 16:47:00 +00:00
Claude 0cfe28a405 fix(claude_desktop): align abc runtime identity so Claude Desktop can start
Version 1.25 chowned the data location to a hardcoded 1000:1000 but never
mapped the shared abc desktop user to that UID: during cont-init abc was
still the image default (911), so TokenSave, RTK, nginx, PulseAudio, the
Mesa shader cache, and Claude Desktop itself failed with Permission denied.
The base image's init-adduser then remapped abc to root mid-startup because
it reads PUID/PGID from add-on options (fallback 0) where they were never
defined, which additionally made Claude Code reject bypass mode.

- Add PUID/PGID add-on options (default 1000:1000) and remap abc to that
  identity at the top of 20-folders.sh, before any ownership pass and
  before any service resolves the user; pin init-adduser to the same
  effective identity so it cannot diverge mid-startup.
- In permission_mode bypass, fall back from a configured PUID 0 to UID
  1000, since Claude Code refuses bypass permissions as root.
- Replace the nonexistent bashio::config.array (only present in the repo's
  standalone bashio) with bashio::config in the TokenSave repository setup,
  tools configuration, and claude-tools-doctor.sh.
- Chown managed Claude configuration files to the effective abc identity
  instead of the raw configured PUID/PGID, which fell back to root.
- Pre-create /tmp/.X11-unix (sticky 1777) so Xorg running as non-root abc
  on the tmpfs /tmp can create its socket.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KN8i26JrKSaBdvTrpVEyQ6
2026-07-15 16:17:53 +00:00
Alexandre 26c26e3668 Update ownership to user 'abc' in 20-folders.sh
Change ownership of specified directories to user 'abc'.
2026-07-15 17:29:54 +02:00
github-actions 64c7f2dd5b GitHub bot: changelog [nobuild] 2026-07-15 15:24:28 +00:00
Alexandre 259517289a build 2026-07-15 17:12:15 +02:00
github-actions b26da2e161 GitHub bot: changelog [nobuild] 2026-07-15 15:08:23 +00:00
Alexandre 9ec6c44f03 Update config.yaml 2026-07-15 17:06:02 +02:00
Alexandre ff43c4eeba Update CHANGELOG.md 2026-07-15 17:05:51 +02:00
Alexandre a1e68ee807 Merge pull request #2867 from crazyrokr/feature/gitea-ssl-healthcheck
Fix Gitea HEALTHCHECK in case of SSL setup
2026-07-15 17:05:04 +02:00
Maksim Kashapov 2a1412d957 Fix Gitea HEALTHCHECK in case of SSL setup 2026-07-15 16:10:54 +02:00
github-actions b328ae242f Github bot : issues linked to readme 2026-07-15 14:01:27 +00:00
Alexandre 9c0521da49 Merge pull request #2865 from alexbelgium/fix/claude-wrapper-headroom-path
fix(claude_desktop): headroom wrapper path + TokenSave startup corruption hardening
2026-07-15 14:58:12 +02:00
alexbelgiumandClaude Fable 5 7d2c6eb9b2 fix(claude_desktop): self-heal TokenSave graph against corruption on startup
The startup indexer chose init vs sync purely on whether
.tokensave/tokensave.db existed, so an interrupted init or a hard
add-on stop mid-write could leave a partial or malformed SQLite graph
that every subsequent boot then ran `sync` against, failing (and
staying broken) forever.

Prepare each configured repo defensively instead:
- serialize the operation under a startup-scoped flock so an
  overlapping restart or a mid-boot git post-commit/checkout sync hook
  can't write the same DB concurrently;
- refresh an existing index with a retried incremental sync, since
  SQLITE_BUSY from lock contention is transient, not corruption;
- quarantine a genuinely unreadable index (sync still failing after
  retries) or a half-written one (an interrupted init, detected via a
  sentinel file) to .tokensave/corrupt-<timestamp>/ and rebuild from
  scratch, so the graph self-heals rather than propagating corruption.

All file operations run as the abc runtime user because the repo
.tokensave directory is outside this script's final ownership pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:42:41 +02:00
alexbelgiumandClaude Fable 5 f990177df8 fix(claude_desktop): resolve headroom binary via PATH in claude wrapper
The /usr/local/bin/claude wrapper hardcoded HEADROOM_BIN as
/usr/local/bin/headroom, but the binary is installed at
/usr/bin/headroom (symlink to /lsiopy/bin/headroom). The -x check
therefore always failed and terminal Claude Code sessions never
routed through the Headroom proxy at 127.0.0.1:8787.

Resolve the binary with "command -v headroom" instead; an empty
result still fails the -x check safely and falls back to launching
Claude Code directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:32:35 +02:00
Alexandre 005275315a Merge pull request #2863 from alexbelgium/feat/claude_desktop-ha-api-helper nobuild
feat(claude_desktop): add ha-cli Core-API helper for configuring Home Assistant
2026-07-15 14:25:21 +02:00
alexbelgiumandClaude Sonnet 5 0223cc3511 fix(claude_desktop): address ha-cli review findings
- Check ha_mcp_token before SUPERVISOR_TOKEN: this add-on always sets
  homeassistant_api, so the admin-equivalent Supervisor token was always
  present and silently shadowed a user's deliberately scoped-down
  ha_mcp_token, defeating the documented scoping path (Codex P1).
- Make ha-cli itself refuse to run when enable_ha_api_helper is false,
  instead of only removing the CLAUDE.md guidance text — disabling the
  option now actually disables the helper (Codex P2).
- Normalize HA_BASE_URL to include /api when the user omits it, so REST
  calls don't 404 (CodeRabbit).
- Read/write CLAUDE.md with explicit UTF-8 in the ha-api-helper removal
  block, matching the emoji/special characters Claude tends to write
  there (CodeRabbit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 14:24:17 +02:00
alexbelgiumandClaude Opus 4.8 e5983f4718 feat(claude_desktop): add ha-cli Core-API helper for configuring Home Assistant
Ship a `ha-cli` command that lets Claude configure Home Assistant
(automations, scripts, scenes, helpers, dashboards, registries, service
calls) through the Home Assistant Core API instead of a /config filesystem
mount, so secrets.yaml and other add-ons' credentials stay out of reach.

It authenticates automatically with the add-on's SUPERVISOR_TOKEN via the
Supervisor Core-API proxy (no token setup), with optional HA_TOKEN /
ha_mcp_token overrides for a scoped Home Assistant user. A managed guidance
block in ~/.claude/CLAUDE.md tells Claude Code to use the helper and to
confirm before writes. Gated by the new enable_ha_api_helper option
(default on). Adds the websockets dependency for the WebSocket subcommand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:05:29 +02:00
Alexandre 587121cb1b Update config.yaml 2026-07-15 13:29:35 +02:00
Alexandre 29f2cfd198 Update 81-claude_update.sh 2026-07-15 13:29:07 +02:00
Alexandre 3329c4b40d nobuild 2026-07-15 13:17:03 +02:00
github-actions 9471dffcca GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-15 09:18:15 +00:00
Alexandre 215327e439 Merge pull request #2862 from alexbelgium/agent/claude-bypass-nonroot
Fix Claude bypass permissions under root PUID
2026-07-15 11:17:27 +02:00
Alexandre 8ecbfccdcd Document Claude bypass root fix 2026-07-15 11:05:49 +02:00
Alexandre 5a2efc4f9e Document non-root Claude bypass runtime 2026-07-15 11:04:27 +02:00
Alexandre ef0aecbde6 Bump Claude Desktop add-on version 2026-07-15 11:02:55 +02:00
Alexandre 7a6ad72617 Diagnose Claude bypass runtime identity 2026-07-15 11:02:23 +02:00
Alexandre b5985f230e Restore effective Claude runtime ownership 2026-07-15 11:01:22 +02:00
Alexandre d48d1f4d8d Drop root for Claude bypass launches 2026-07-15 11:01:06 +02:00
Alexandre 6bf379bffc Use effective Claude runtime ownership 2026-07-15 10:59:49 +02:00
Alexandre 5d577ad954 Run Claude bypass mode as non-root 2026-07-15 10:59:09 +02:00
GitHub Actions 85dab2ae6e Revert "Update config.yaml"
This reverts commit ad50abc365.
2026-07-15 08:27:28 +00:00
Alexandre ad50abc365 Update config.yaml 2026-07-15 10:25:14 +02:00
github-actions dc280a5caf GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-15 08:19:42 +00:00
Alexandre c0c1df8c27 Merge pull request #2861 from alexbelgium/agent/claude-tools-hardening
Improve Claude Desktop optimization tooling
2026-07-15 10:18:44 +02:00
Alexandre a38bc75f95 Report Claude permission mode in diagnostics 2026-07-15 10:11:00 +02:00
Alexandre 8073317150 Document Claude permission modes 2026-07-15 10:09:44 +02:00
Alexandre 15505a265f Trust configured TokenSave repositories 2026-07-15 10:08:34 +02:00
Alexandre d1ceafebe8 Fix actionlint amd64 asset mapping 2026-07-15 10:08:08 +02:00
Alexandre c76b257f5d Persist Claude permission mode 2026-07-15 10:05:29 +02:00
Alexandre 321f2fde73 Apply Claude permission mode in wrapper 2026-07-15 10:04:48 +02:00
Alexandre 45f88307d4 Add Claude permission modes 2026-07-15 10:04:23 +02:00
Alexandre 7cd82b2758 Fix validator release asset lookup 2026-07-15 09:20:43 +02:00
Alexandre 66d3886b80 Improve Claude optimization tooling 2026-07-15 09:17:05 +02:00
Alexandre bdd56b8057 Improve Claude optimization tooling 2026-07-15 09:16:08 +02:00
Alexandre 9c55193c38 Improve Claude optimization tooling 2026-07-15 09:15:18 +02:00
Alexandre 6b6233e1fa Improve Claude optimization tooling 2026-07-15 09:14:23 +02:00
Alexandre 22951ac4f5 Improve Claude optimization tooling 2026-07-15 09:12:44 +02:00
Alexandre 006f3052ac Improve Claude optimization tooling 2026-07-15 09:12:14 +02:00
Alexandre 0ff2e8f783 Improve Claude optimization tooling 2026-07-15 09:11:58 +02:00
Alexandre 3c8a32199e Improve Claude optimization tooling 2026-07-15 09:11:46 +02:00
Alexandre c8d706c9de Improve Claude optimization tooling 2026-07-15 09:11:30 +02:00
Alexandre c04f3e288c Improve Claude optimization tooling 2026-07-15 09:11:20 +02:00
github-actions f6c2bef0be GitHub bot: changelog [nobuild] 2026-07-14 17:59:29 +00:00
Alexandre f8c447ed19 Update config.yaml 2026-07-14 19:46:49 +02:00
Alexandre bd82e72ffa Remove gnome-keyring from Dockerfile
Removed gnome-keyring from the list of packages to install.
2026-07-14 19:46:37 +02:00
github-actions a3ecb38fea GitHub bot : README updated 2026-07-14 17:26:53 +00:00
Alexandre eada7a0ba0 Merge pull request #2860 from alexbelgium/feat/claude-desktop-only-v2
fix(claude_desktop): correct HA MCP endpoint, harden config perms and chmod scope
2026-07-14 16:52:58 +02:00
alexbelgiumandClaude Sonnet 5 e7921b822e fix(claude_desktop): correct HA MCP endpoint, harden config perms and chmod scope
Home Assistant's MCP Server integration serves stateless Streamable HTTP at
/api/mcp; mcp-proxy defaults to SSE, so the previous registration (SSE at
/mcp_server/sse) could never attach. Pass --transport=streamablehttp
--stateless and default ha_mcp_url to /api/mcp.

Match managed MCP entries by binary basename outside $HOME so a base-image
path change still updates them, while user-installed binaries under $HOME
remain untouched. Resolve tokensave via command -v like the others.

Write Claude config files 0600 (they hold the HA long-lived token in clear
text) and scope the build-time chmod +x pass to the shipped script dirs.

Docs: dashboard reachability wording, stale /config/data HOME, and the
custom-script filename (claude_desktop.sh, per the $slug.sh template).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 16:52:15 +02:00
Alexandre aa1486c1d8 nobuild 2026-07-14 16:39:19 +02:00
GitHub Actions b4008c4db9 Revert "update"
This reverts commit fa2328f741.
2026-07-14 14:37:19 +00:00
Alexandre fa2328f741 update 2026-07-14 16:36:25 +02:00
Alexandre e8bb55682b Merge pull request #2859 from alexbelgium/bump-builder-2026.06.0
ci: bump builder build-image action to 2026.06.0
2026-07-14 16:35:01 +02:00
Alexandre 7fdf95940b Merge pull request #2858 from alexbelgium/feat/claude-desktop-only
feat(claude_desktop): desktop-only architecture, fix dashboard + dispatch
2026-07-14 16:33:19 +02:00
alexbelgiumandClaude Fable 5 583c5e655a ci: bump home-assistant/builder build-image action to 2026.06.0
Legacy monolithic builder was removed upstream in 2026.06.0; this repo
already uses the modular build-image action, so only the pin moves
(2026.03.2 -> 2026.06.0). Action inputs/outputs unchanged upstream —
drop-in compatible. Also strips trailing whitespace at EOF (yamllint).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:31:24 +02:00
github-actions 50150a4775 GitHub bot: changelog [nobuild] 2026-07-14 14:26:50 +00:00
github-actions 6814088369 GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-14 14:19:56 +00:00
87 changed files with 2564 additions and 1224 deletions

No files matched your search

+15
View File
@@ -93,6 +93,21 @@ jobs:
- name: ↩️ Checkout
uses: actions/checkout@v7
- name: Copy templates into addon build context
env:
ADDON: ${{ matrix.addon }}
run: |
set -euo pipefail
TEMPLATES_DIR=".templates"
ADDON_DIR="./$ADDON"
# Keep PR builds aligned with the production builder.
for script in ha_automodules.sh ha_autoapps.sh ha_entrypoint.sh bashio-standalone.sh ha_lsio.sh; do
if [ -f "$TEMPLATES_DIR/$script" ]; then
cp "$TEMPLATES_DIR/$script" "$ADDON_DIR/$script"
fi
done
- name: ℹ️ Gather addon info
id: information
uses: frenck/action-addon-information@v1.4
+1 -2
View File
@@ -300,7 +300,7 @@ jobs:
- name: Build ${{ matrix.addon }} add-on
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
uses: home-assistant/builder/actions/build-image@2026.03.2
uses: home-assistant/builder/actions/build-image@2026.06.0
with:
arch: ${{ matrix.arch }}
cache-gha: "false"
@@ -433,4 +433,3 @@ jobs:
done
git push origin HEAD:master
+4 -1
View File
@@ -258,13 +258,15 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![ingress][ingress-badge]
&#10003; ![image](https://api.iconify.design/mdi/robot-happy.svg) [Claude Desktop](claude_desktop/) : Claude Desktop and a persistent Claude Code web terminal
&#10003; ![image](https://api.iconify.design/mdi/robot-happy.svg) [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom, RTK, and TokenSave optimization
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fclaude_desktop%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fclaude_desktop%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]
&#10003; ![image](https://api.iconify.design/mdi/movie-search.svg) [Cleanuparr](cleanuparr/) : Automatically removes stuck and unwanted downloads from your *arr and download clients
@@ -313,6 +315,7 @@ If you want to do add the repository manually, please follow the procedure highl
&#10003; [Elasticsearch server](elasticsearch/) : Free and Open, Distributed, RESTful Search Engine
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Felasticsearch%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Felasticsearch%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
+3
View File
@@ -1,4 +1,7 @@
## 1.82.1 (2026-07-16)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.81.0 (2026-07-04)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.80.0 (2026-06-05)
+1 -1
View File
@@ -16,7 +16,7 @@
ARG BUILD_FROM
ARG BUILD_VERSION
ARG BUILD_UPSTREAM="1.81.0"
ARG BUILD_UPSTREAM="1.82.1"
FROM ${BUILD_FROM}
##################
+1 -1
View File
@@ -108,4 +108,4 @@ schema:
slug: autobrr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/autobrr
version: "1.81.0"
version: "1.82.1"
+2 -2
View File
@@ -1,8 +1,8 @@
{
"last_update": "2026-07-04",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "autborr",
"source": "github",
"upstream_repo": "autobrr/autobrr",
"upstream_version": "1.81.0"
"upstream_version": "1.82.1"
}
+6
View File
@@ -1,3 +1,9 @@
## source-20260717 (17-07-2026)
- Minor bugs fixed
## source-20260716.02 (16-07-2026)
- Minor bugs fixed
## source-20260716 (16-07-2026)
- Minor bugs fixed
## source-20260714 (14-07-2026)
- Minor bugs fixed
## source-20260709 (09-07-2026)
+1 -1
View File
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
udev: true
url: https://github.com/alexbelgium/hassio-addons
usb: true
version: "source-20260714"
version: "source-20260717"
video: true
+21 -4
View File
@@ -77,10 +77,27 @@ for entry in "${prs[@]}"; do
# Fetch the PR head commit by number; works unauthenticated for public repos.
git fetch --no-tags origin "refs/pull/${number}/head"
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
git merge --abort || true
exit 1
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
# package-lock.json is generated content and stacked PRs can carry an
# older copy even when their source changes merge cleanly. Keep the
# lockfile already assembled from upstream and earlier PRs, but only
# when it is the sole conflict. Any source conflict remains fatal.
if [ "${#conflicted_files[@]}" -eq 1 ] \
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
git checkout --ours -- frontend/package-lock.json
git add frontend/package-lock.json
git commit --no-edit
else
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
if [ "${#conflicted_files[@]}" -gt 0 ]; then
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
fi
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
git merge --abort || true
exit 1
fi
fi
done
+3
View File
@@ -1,4 +1,7 @@
## 20260716 (2026-07-16)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## 20260712 (2026-07-13)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## nightly-20260615-4 (09-07-2026)
+1 -1
View File
@@ -128,4 +128,4 @@ slug: birdnet-go
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go
usb: true
version: "20260712"
version: "20260716"
+2 -2
View File
@@ -2,10 +2,10 @@
"github_beta": true,
"github_exclude": "-4",
"github_fulltag": true,
"last_update": "2026-07-13",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "birdnet-go",
"source": "github",
"upstream_repo": "tphakala/birdnet-go",
"upstream_version": "20260712"
"upstream_version": "20260716"
}
+3
View File
@@ -1,4 +1,7 @@
## 1.92.140-ls113 (2026-07-16)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.139-ls112 (2026-07-11)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
+1 -1
View File
@@ -69,5 +69,5 @@ slug: brave
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.92.139-ls112"
version: "1.92.140-ls113"
video: true
+2 -2
View File
@@ -1,9 +1,9 @@
{
"github_fulltag": "true",
"last_update": "2026-07-11",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "brave",
"source": "github",
"upstream_repo": "linuxserver/docker-brave",
"upstream_version": "1.92.139-ls112"
"upstream_version": "1.92.140-ls113"
}
+3
View File
@@ -1,4 +1,7 @@
## version-ae9a7fca (2026-07-16)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-30a7c401 (2026-07-11)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
+1 -1
View File
@@ -71,5 +71,5 @@ slug: chromium
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "version-30a7c401"
version: "version-ae9a7fca"
video: true
+2 -2
View File
@@ -1,9 +1,9 @@
{
"github_fulltag": "true",
"last_update": "2026-07-11",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "chromium",
"source": "github",
"upstream_repo": "linuxserver/docker-chromium",
"upstream_version": "version-30a7c401"
"upstream_version": "version-ae9a7fca"
}
+3
View File
@@ -1,4 +1,7 @@
## 2.55.0 (2026-07-16)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.2 (2026-07-04)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
+1 -1
View File
@@ -86,5 +86,5 @@ schema:
slug: browserless_chrome
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
version: "2.54.2"
version: "2.55.0"
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
+2 -2
View File
@@ -1,9 +1,9 @@
{
"github_tagfilter": "v",
"last_update": "2026-07-04",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "browserless_chrome",
"source": "github",
"upstream_repo": "browserless/chrome",
"upstream_version": "2.54.2"
"upstream_version": "2.55.0"
}
+3
View File
@@ -1,3 +1,6 @@
## 0.55.8 (2026-07-16)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
## 0.55.7 (2026-05-30)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
+1 -1
View File
@@ -34,4 +34,4 @@ schema:
slug: changedetection.io
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/changedetection.io
version: "0.55.7"
version: "0.55.8"
+2 -2
View File
@@ -1,9 +1,9 @@
{
"github_fulltag": "false",
"last_update": "2026-05-30",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "changedetection.io",
"source": "github",
"upstream_repo": "linuxserver/docker-changedetection.io",
"upstream_version": "0.55.7"
"upstream_version": "0.55.8"
}
-10
View File
@@ -1,10 +0,0 @@
## 0.144.3-2 (14-07-2026)
- Initial ChatGPT Codex add-on.
- Added a persistent, administrator-only Home Assistant ingress terminal backed by tmux.
- Made `headroom wrap codex` the default launch path.
- Configured Docker builds to install the latest stable Codex, Headroom, RTK, ttyd, and Rust toolchain versions without hard-coded tool version pins.
- Added RTK native Codex initialization and savings reporting.
- Added device-code authentication and direct Codex fallback helpers.
- Added persistent configuration, GitHub CLI integration, mount support, and safe defaults.
- Made the default workspace follow a custom `data_location`.
-157
View File
@@ -1,157 +0,0 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM
ARG BUILD_VERSION
FROM rust:bookworm AS rtk-builder
RUN set -eux; \
rtk_version="$(git ls-remote --tags --refs --sort=-v:refname \
https://github.com/rtk-ai/rtk.git 'refs/tags/v*' \
| awk -F/ '$3 ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ { print $3; exit }')"; \
test -n "$rtk_version"; \
git clone --depth 1 --branch "$rtk_version" https://github.com/rtk-ai/rtk.git /src/rtk; \
cd /src/rtk; \
cargo build --release --locked; \
install -D -m 0755 target/release/rtk /out/rtk; \
/out/rtk --version
FROM ${BUILD_FROM}
##################
# 2 Modify Image #
##################
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_SERVICES_GRACETIME=0 \
HEADROOM_CONTEXT_TOOL=rtk
USER root
VOLUME [ "/sys/fs/cgroup" ]
ARG TEMPLATE_BASE_URL="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates"
##################
# 3 Install apps #
##################
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/s6-overlay /usr/local/bin \
-type f \( -name "*.sh" -o -name "run" -o -name "finish" -o -path "/usr/local/bin/*" \) \
-print -exec chmod +x {} \;
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
gh \
jq \
less \
nano \
openssh-client \
python3-pip \
ripgrep \
tmux && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install the latest stable official Codex static binary for the target architecture.
RUN set -eux; \
case "$(dpkg --print-architecture)" in \
amd64) codex_arch="x86_64" ;; \
arm64) codex_arch="aarch64" ;; \
*) echo "Unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
esac; \
archive="/tmp/codex.tar.gz"; \
curl -fsSL --retry 3 --retry-delay 2 \
-o "$archive" \
"https://github.com/openai/codex/releases/latest/download/codex-${codex_arch}-unknown-linux-musl.tar.gz"; \
tar -xzf "$archive" -C /tmp; \
install -m 0755 "/tmp/codex-${codex_arch}-unknown-linux-musl" /usr/local/bin/codex; \
rm -f "$archive" "/tmp/codex-${codex_arch}-unknown-linux-musl"; \
codex --version
# Install the latest stable ttyd binary for the Home Assistant ingress terminal.
RUN set -eux; \
case "$(dpkg --print-architecture)" in \
amd64) ttyd_arch="x86_64" ;; \
arm64) ttyd_arch="aarch64" ;; \
*) echo "Unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
esac; \
curl -fsSL --retry 3 --retry-delay 2 \
-o /usr/local/bin/ttyd \
"https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${ttyd_arch}"; \
chmod 0755 /usr/local/bin/ttyd; \
ttyd --version
COPY --from=rtk-builder /out/rtk /usr/local/bin/rtk
RUN rtk --version && \
pip3 install --upgrade --break-system-packages --no-cache-dir "headroom-ai[proxy,code,mcp]" && \
headroom --version
ARG MODULES="00-banner.sh 00-global_var.sh 01-custom_script.sh 00-local_mounts.sh 00-smb_mounts.sh 90-dns_set.sh"
RUN curl -fsSL --retry 3 --retry-delay 2 \
-o /ha_automodules.sh "${TEMPLATE_BASE_URL}/ha_automodules.sh" && \
chmod 744 /ha_automodules.sh && \
/ha_automodules.sh "$MODULES" && \
rm /ha_automodules.sh
################
# 4 Entrypoint #
################
RUN curl -fsSL --retry 3 --retry-delay 2 \
-o /ha_entrypoint.sh "${TEMPLATE_BASE_URL}/ha_entrypoint.sh" && \
curl -fsSL --retry 3 --retry-delay 2 \
-o /usr/local/lib/bashio-standalone.sh "${TEMPLATE_BASE_URL}/bashio-standalone.sh" && \
chmod 0777 /ha_entrypoint.sh && \
chmod 0755 /usr/local/lib/bashio-standalone.sh
ENTRYPOINT [ "/usr/bin/env" ]
CMD [ "/ha_entrypoint.sh" ]
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/master/chatgpt_codex/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
CMD curl -fsS http://127.0.0.1:7681/ > /dev/null || exit 1
-112
View File
@@ -1,112 +0,0 @@
# Home Assistant add-on: ChatGPT Codex
![Supports aarch64 Architecture][aarch64-shield]
![Supports amd64 Architecture][amd64-shield]
![Project Maintenance][maintenance-shield]
Run the official OpenAI Codex CLI in a persistent Home Assistant ingress terminal. The optimized path uses `headroom wrap codex`, with RTK handling command-output compression before results reach Codex.
> The repository already contains an unrelated add-on named **Codex** for comic archives. This coding-agent add-on therefore uses the slug `chatgpt_codex`.
## Features
- Latest stable Codex, Headroom, RTK, ttyd, and Rust toolchain versions are resolved during every Docker build; tool versions are not pinned in the Dockerfile.
- Official Codex CLI static binary for `amd64` and `aarch64`.
- Home Assistant authenticated, administrator-only ingress; no unauthenticated terminal port is exposed.
- Persistent `$HOME`, Codex authentication, settings, sessions, Headroom state, and RTK statistics.
- Persistent `tmux` session that survives browser disconnects.
- `headroom wrap codex` as the default launch path.
- Baked-in RTK with native Codex initialization.
- Optional Headroom output shaping and code-aware compression.
- Direct Codex fallback for troubleshooting.
- Device-code login helper designed for a remote or headless container.
- Baked-in Git, GitHub CLI, ripgrep, jq, SSH client, and common terminal tools.
- Optional GitHub CLI authentication and Git author configuration.
- Optional extra apt and pip packages.
- Local and SMB mount support through the repository standard modules.
## Installation and first login
1. Install **ChatGPT Codex** from this add-on repository.
2. Keep the default `data_location` and `workspace`, or select writable mounted paths.
3. Start the add-on and open its web UI.
4. Codex starts automatically through Headroom.
5. When prompted to authenticate, follow the device-code instructions. You can also exit Codex and run:
```shell
codex-login
```
Codex supports ChatGPT sign-in and API-key authentication. The device-code flow is the recommended option for this headless add-on.
## Launch commands
Optimized default:
```shell
codex-headroom
```
This runs:
```shell
headroom wrap codex
```
Headroom starts its local proxy, configures Codex routing and MCP support, and uses RTK as the CLI context tool.
Direct troubleshooting path:
```shell
codex-direct
```
Check optimization status and measured savings:
```shell
headroom doctor
headroom perf
rtk gain
```
## Persistence
The terminal attaches every browser connection to the same `tmux` session. Closing the browser detaches the client but does not stop Codex or commands running in the session.
Persistent data is stored below `data_location`:
- Codex state: `~/.codex`
- Headroom state and metrics: `~/.headroom`
- RTK state: its normal paths below the persistent home
- Default workspace: `~/workspace`
## Options
| Option | Default | Description |
| --- | --- | --- |
| `data_location` | `/data/data` | Persistent home. Must be below `/data`, `/share`, `/media`, `/config`, or `/mnt`. |
| `workspace` | `<data_location>/workspace` | Initial project directory. Leave empty to follow `data_location`. |
| `PUID` / `PGID` | `0` / `0` | Runtime user and group used by the LinuxServer `abc` account. |
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
| `auto_start_codex` | `true` | Start Codex automatically when the tmux session is first created. |
| `use_headroom` | `true` | Use `headroom wrap codex`; disabling this starts Codex directly. |
| `headroom_output_shaper` | `true` | Enable Headroom output-token shaping. |
| `headroom_code_aware` | `true` | Enable Headroom AST-aware code compression. |
| `github_token` | | Authenticate GitHub CLI and Git operations. |
| `github_username` / `github_email` | | Configure the global Git author. |
| `additional_apps` | | Comma-separated Debian packages installed at startup. |
| `additional_pip` | | Comma-separated Python packages installed at startup. |
| `localdisks` / `networkdisks` | | Optional local-disk and SMB mounts supported by the repository modules. |
| `env_vars` | `[]` | Additional environment variables exported in the container. |
Configuration changes affecting the launch command apply to a newly created tmux session. To recreate it, exit Codex and run `tmux kill-session -t codex`, then reopen the add-on web UI.
## Security
The add-on deliberately does not enable Codex approval or sandbox bypass flags. Codex can execute commands and edit files available inside the configured workspace, so only mount locations you intend it to access.
The terminal is exposed only through Home Assistant administrator-only ingress. Do not add an unauthenticated direct port mapping. Treat `github_token`, Codex authentication data, and the persistent home as secrets and include them only in trusted backups.
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg
-6
View File
@@ -1,6 +0,0 @@
{
"build_from": {
"aarch64": "ghcr.io/linuxserver/baseimage-debian:arm64v8-bookworm",
"amd64": "ghcr.io/linuxserver/baseimage-debian:amd64-bookworm"
}
}
-68
View File
@@ -1,68 +0,0 @@
arch:
- aarch64
- amd64
description: "Persistent OpenAI Codex web terminal optimized with Headroom and RTK"
devices:
- /dev/fuse
environment:
HOME: /data/data
PGID: "0"
PUID: "0"
TERM: xterm-256color
image: ghcr.io/alexbelgium/chatgpt_codex-{arch}
ingress: true
ingress_port: 7681
ingress_stream: true
init: false
map:
- addon_config:rw
- share:rw
- media:rw
- ssl
name: ChatGPT Codex
options:
env_vars: []
DNS_server: 8.8.8.8
data_location: /data/data
workspace: ""
PUID: 0
PGID: 0
auto_start_codex: true
use_headroom: true
headroom_output_shaper: true
headroom_code_aware: true
github_token: ""
github_username: ""
github_email: ""
additional_apps: ""
additional_pip: ""
panel_icon: mdi:code-braces-box
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
DNS_server: str?
data_location: str?
workspace: str?
PUID: int
PGID: int
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
auto_start_codex: bool
use_headroom: bool
headroom_output_shaper: bool
headroom_code_aware: bool
github_token: password?
github_username: str?
github_email: str?
additional_apps: str?
additional_pip: str?
localdisks: str?
networkdisks: str?
slug: chatgpt_codex
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "0.144.3-3"
@@ -1,46 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PUID="$(bashio::config 'PUID')"
PGID="$(bashio::config 'PGID')"
LOCATION="$(bashio::config 'data_location')"
if [ -z "$LOCATION" ] || [ "$LOCATION" = "null" ]; then
LOCATION="/data/data"
fi
case "$LOCATION" in
/data/* | /share/* | /media/* | /config/* | /mnt/*)
;;
*)
bashio::log.fatal "data_location must be below /data, /share, /media, /config, or /mnt"
exit 1
;;
esac
if [ -L "$LOCATION" ]; then
bashio::log.fatal "data_location must not be a symbolic link"
exit 1
fi
bashio::log.info "Using persistent home: $LOCATION"
install -d -m 0750 -o "$PUID" -g "$PGID" "$LOCATION"
install -d -m 0750 -o "$PUID" -g "$PGID" "$LOCATION/.codex" "$LOCATION/.headroom"
install -d -m 0755 /tmp/cache /run/s6/container_environment
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
for variable in HOME CODEX_HOME HEADROOM_WORKSPACE_DIR XDG_CACHE_HOME; do
case "$variable" in
HOME) value="$LOCATION" ;;
CODEX_HOME) value="$LOCATION/.codex" ;;
HEADROOM_WORKSPACE_DIR) value="$LOCATION/.headroom" ;;
XDG_CACHE_HOME) value="/tmp/cache" ;;
esac
printf '%s' "$value" > "/run/s6/container_environment/$variable"
done
chown -R "$PUID:$PGID" "$LOCATION/.codex" "$LOCATION/.headroom"
chown "$PUID:$PGID" "$LOCATION"
@@ -1,33 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
if bashio::config.has_value 'additional_apps'; then
packages="$(bashio::config 'additional_apps')"
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10
for package in ${packages//,/ }; do
bashio::log.info "Installing apt package: $package"
apt-get install -y --no-install-recommends "$package"
done
apt-get clean
rm -rf /var/lib/apt/lists/*
fi
if bashio::config.has_value 'additional_pip'; then
packages="$(bashio::config 'additional_pip')"
for package in ${packages//,/ }; do
bashio::log.info "Installing pip package: $package"
pip3 install --break-system-packages "$package"
done
fi
if bashio::config.has_value 'TZ'; then
timezone="$(bashio::config 'TZ')"
if [ ! -e "/usr/share/zoneinfo/$timezone" ]; then
bashio::log.fatal "Invalid timezone: $timezone"
exit 1
fi
ln -snf "/usr/share/zoneinfo/$timezone" /etc/localtime
printf '%s\n' "$timezone" > /etc/timezone
fi
@@ -1,55 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PUID="$(bashio::config 'PUID')"
PGID="$(bashio::config 'PGID')"
if ! command -v codex > /dev/null 2>&1; then
bashio::log.fatal "Codex CLI is not available"
exit 1
fi
if ! command -v headroom > /dev/null 2>&1; then
bashio::log.fatal "Headroom is not available"
exit 1
fi
if ! command -v rtk > /dev/null 2>&1; then
bashio::log.fatal "RTK is not available"
exit 1
fi
bashio::log.info "Codex: $(codex --version 2>&1 | head -n 1)"
bashio::log.info "Headroom: $(headroom --version 2>&1 | head -n 1)"
bashio::log.info "RTK: $(rtk --version 2>&1 | head -n 1)"
# Configure RTK's native Codex integration ahead of the first wrapped session.
if ! s6-setuidgid abc env \
HOME="$HOME" \
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" \
PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" \
RTK_NONINTERACTIVE=1 \
rtk init -g --codex; then
bashio::log.warning "RTK Codex initialization failed; Headroom will retry when wrapping Codex"
fi
for key in CODEX_AUTO_START CODEX_USE_HEADROOM HEADROOM_OUTPUT_SHAPER HEADROOM_CODE_AWARE_ENABLED; do
case "$key" in
CODEX_AUTO_START)
bashio::config.true 'auto_start_codex' && value="1" || value="0"
;;
CODEX_USE_HEADROOM)
bashio::config.true 'use_headroom' && value="1" || value="0"
;;
HEADROOM_OUTPUT_SHAPER)
bashio::config.true 'headroom_output_shaper' && value="1" || value="0"
;;
HEADROOM_CODE_AWARE_ENABLED)
bashio::config.true 'headroom_code_aware' && value="1" || value="0"
;;
esac
printf '%s' "$value" > "/run/s6/container_environment/$key"
done
printf '%s' 'rtk' > /run/s6/container_environment/HEADROOM_CONTEXT_TOOL
chown -R "$PUID:$PGID" "$HOME/.codex" "$HOME/.headroom"
@@ -1,29 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
if bashio::config.has_value 'github_username'; then
s6-setuidgid abc git config --global user.name "$(bashio::config 'github_username')"
fi
if bashio::config.has_value 'github_email'; then
s6-setuidgid abc git config --global user.email "$(bashio::config 'github_email')"
fi
if bashio::config.has_value 'github_token'; then
token="$(bashio::config 'github_token')"
if s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
bashio::log.info "GitHub CLI is already authenticated"
else
bashio::log.info "Authenticating GitHub CLI"
printf '%s\n' "$token" | s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN \
gh auth login --hostname github.com --with-token || \
bashio::log.warning "GitHub CLI authentication failed"
fi
s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN \
gh auth setup-git --hostname github.com || \
bashio::log.warning "GitHub CLI git credential setup failed"
else
bashio::log.info "Set github_token to authenticate gh and Git operations"
fi
@@ -1,54 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
port=7681
workspace="$(bashio::config 'workspace')"
if [ -z "$workspace" ] || [ "$workspace" = "null" ]; then
workspace="$HOME/workspace"
fi
if [[ "$workspace" != /* ]]; then
bashio::log.fatal "workspace must be an absolute path"
exec sleep infinity
fi
if [ -L "$workspace" ]; then
bashio::log.fatal "workspace must not be a symbolic link"
exec sleep infinity
fi
workspace="$(realpath -m -- "$workspace")"
case "$workspace" in
"$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*)
;;
*)
bashio::log.fatal "workspace must be below the persistent home, /share, /media, /mnt, /data, or /config"
exec sleep infinity
;;
esac
if [ ! -e "$workspace" ]; then
install -d -m 0750 -o abc -g abc "$workspace"
elif [ ! -d "$workspace" ]; then
bashio::log.fatal "workspace is not a directory: $workspace"
exec sleep infinity
fi
if ! s6-setuidgid abc test -r "$workspace" || \
! s6-setuidgid abc test -w "$workspace" || \
! s6-setuidgid abc test -x "$workspace"; then
bashio::log.fatal "workspace must be readable, writable, and searchable by user abc: $workspace"
exec sleep infinity
fi
export CODEX_TERMINAL_WORKSPACE="$workspace"
bashio::log.info "Starting persistent Codex terminal on Home Assistant ingress port $port"
exec s6-setuidgid abc ttyd \
-p "$port" \
-W \
-O \
-t disableLeaveAlert=true \
-t fontSize=14 \
/usr/local/bin/codex-terminal-shell
@@ -1 +0,0 @@
longrun
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
exec codex "$@"
@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
export HEADROOM_CONTEXT_TOOL="rtk"
if ! command -v headroom > /dev/null 2>&1; then
echo "Headroom is unavailable; launching Codex directly." >&2
exec codex "$@"
fi
if [ "$#" -eq 0 ]; then
exec headroom wrap codex
fi
exec headroom wrap codex -- "$@"
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
exec codex login --device-auth "$@"
@@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export SHELL="/bin/bash"
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
workspace="${CODEX_TERMINAL_WORKSPACE:-${HOME}/workspace}"
session_name="${CODEX_TMUX_SESSION:-codex}"
if [ ! -d "$workspace" ]; then
echo "Codex workspace does not exist: $workspace" >&2
exit 1
fi
new_session=0
if ! tmux has-session -t "$session_name" 2> /dev/null; then
tmux new-session -d -s "$session_name" -c "$workspace" /bin/bash -l
new_session=1
fi
if [ "$new_session" -eq 1 ]; then
tmux send-keys -t "$session_name" \
"printf '\\nChatGPT Codex add-on\\n login: codex-login\\n optimized: codex-headroom\\n direct: codex-direct\\n savings: rtk gain && headroom perf\\n\\n'" C-m
if [ "${CODEX_AUTO_START:-1}" = "1" ]; then
if [ "${CODEX_USE_HEADROOM:-1}" = "1" ]; then
tmux send-keys -t "$session_name" "codex-headroom" C-m
else
tmux send-keys -t "$session_name" "codex-direct" C-m
fi
fi
fi
exec tmux attach-session -t "$session_name"
-12
View File
@@ -1,12 +0,0 @@
{
"github_beta": false,
"github_fulltag": false,
"github_havingasset": true,
"github_tagfilter": "rust-v",
"last_update": "2026-07-14",
"repository": "alexbelgium/hassio-addons",
"slug": "chatgpt_codex",
"source": "github",
"upstream_repo": "openai/codex",
"upstream_version": "0.144.3"
}
+77 -2
View File
@@ -1,4 +1,79 @@
## 1.17 (14-07-2026)
## 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)
- 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…`).
- Fix Claude Desktop never appearing — the Selkies web client stayed on "waiting for stream" forever with `libEGL warning: failed to open /dev/dri/card0: Permission denied` in the log. The LinuxServer base image grants the desktop user (`abc`) access to the `/dev/dri` render nodes in its `init-video` s6 oneshot, but that oneshot is not a dependency of `svc-xorg`/`svc-selkies`/`svc-de`, so on Home Assistant those long-running services regularly start (via `s6-setuidgid abc`) *before* `abc` has been added to the render group. Xorg/Selkies/pixelflux then open the render device without permission, the video pipeline produces no frames, and the stream never starts. Prepare the exposed DRI nodes in a new `21-gpu_permissions.sh` cont-init script instead: `cont-init.d` runs to completion before any s6-rc service starts, so `abc` is added to each node's owning group (and the node is made world read/write as a timing-independent fallback) in time for the graphical services to use the GPU. Best-effort and a no-op on hosts that expose no GPU.
## 1.30 (16-07-2026)
- Compress large tool outputs automatically in every Claude Code session with a managed `PostToolUse` hook (new `headroom_auto_compress` option, enabled by default). Desktop-spawned sessions (cowork/dispatch) pin `ANTHROPIC_BASE_URL` to the production endpoint (headroom #869), so the transparent proxy never sees their traffic and compression there depended entirely on the model remembering to call the `headroom` MCP tools per the CLAUDE.md guidance — in practice most large outputs went uncompressed. The new `/usr/local/bin/headroom-posttooluse-compress.py` hook fires on `Bash`/`Grep`/`Glob`/`WebFetch` results over ~4000 characters, compresses them with Headroom's rule-based pipeline (SmartCrusher and friends; the Kompress ML path is disabled because its background model load can never complete inside a short-lived hook process), and swaps the result in via `hookSpecificOutput.updatedToolOutput` with a retrieval marker appended. Originals are stored in the shared CCR SQLite store (`~/.headroom/ccr_store.db` — the same one the headroom MCP server reads), so `mcp__headroom__headroom_retrieve` always recovers the full output; savings are recorded to the durable ledger (client `posttooluse-hook`) and show up in the existing gains report. The hook fails open (any error leaves the tool output untouched), never touches `stderr` fields so error text reaches the model verbatim, skips anything below a 50-token savings floor, and is registered idempotently in `~/.claude/settings.json` only after a `--self-test` confirms the interpreter can import headroom; disabling the option (or Headroom) removes the managed entry without touching user-defined hooks. Measured on a representative Home Assistant `states` dump: 10781 -> 2964 tokens (73% saved) at ~1.7 s hook overhead, with sub-100 ms pass-through for small outputs.
## 1.29 (16-07-2026)
- Point the Headroom MCP server at the persistent Kompress model cache. 1.27 set `HF_HOME` on the `svc-headroom` proxy longrun only, but the MCP server is a separate process spawned by Claude Desktop / Claude Code from the registered `mcpServers` entry, so it never inherited that export and kept resolving the HuggingFace cache to `~/.cache` — symlinked to tmpfs here and wiped on every restart. Its Kompress ML path therefore never found the model, re-downloaded ~270 MB into tmpfs on each boot, and lost it again on the next one; `headroom_compress` fell back to `router:noop` (unchanged output) on prose and other unstructured content. The managed `headroom` entry in both `claude_desktop_config.json` and `~/.claude.json` now carries `env.HF_HOME` pointing at the same `~/.headroom/hf` cache the proxy warms. Rule-based compression (SmartCrusher, structured tool output) was unaffected and worked throughout.
- Fix `~/.gitconfig` being written as `root` and left unreadable by the `abc` runtime user, which broke git for the user that actually runs it: every commit failed with `Author identity unknown` and the `gh` credential helper was invisible to authenticated pushes. `git config --global` ran as root during init and rewrites the file on every start, so `20-folders.sh`'s earlier recursive chown never stuck to it (`.config/gh` survived abc-owned only because the "already authenticated" branch skips rewriting it). The git/gh setup now runs as `abc` via `s6-setuidgid`, matching `81-tokensave_repositories.sh`, and reclaims any root-owned copies left by an earlier version before writing.
- Fix `~/.bashrc` accumulating stale `HOME`/`FM_HOME` exports when `data_location` changes. The idempotency guard only tested for the *current* `$LOCATION`, so changing the option and later changing it back appended a second block while leaving the first, and the last one written won for every interactive shell — leaving `$HOME` pointing at a directory the add-on no longer manages. Any tool that resolves config through `$HOME` then read the wrong path (`headroom doctor` reported `claude: not routed (no ~/.claude/settings.json)` against a correctly routed install, and bare `headroom` invocations created a stray `.headroom` tree under the old location). The block is now marker-delimited and rewritten from scratch on every boot, so it is idempotent across any number of `data_location` changes.
## 1.27 (15-07-2026)
- Route Claude Desktop cowork/local-agent-mode sessions through the Headroom proxy. Desktop spawns its bundled Claude Code binary at an absolute path (bypassing the add-on's PATH wrapper) with `ANTHROPIC_BASE_URL` pinned to the production endpoint, so those sessions never produced proxy savings. The add-on now manages `env.ANTHROPIC_BASE_URL` in `~/.claude/settings.json` — settings `env` entries replace inherited environment values at CLI startup — gated on `headroom_wrap_claude_code` and never overwriting a user-customized endpoint.
- Fix Headroom's Kompress compression engine never activating, which made even proxied traffic record zero token savings (e.g. 175 requests, 0 saved). The proxy's startup preload is deliberately cache-only, but the HuggingFace model cache defaulted to `~/.cache` — tmpfs in this add-on, wiped every restart — so the ONNX model (plus the separately fetched `answerdotai/ModernBERT-base` tokenizer) was never cached and the engine idled in "deferred" mode forever, misleadingly logged as `Kompress: not installed`. `svc-headroom` now points `HF_HOME` at persistent storage (`~/.headroom/hf`, ~270 MB); the proxy's own request path already downloads a missing model in the background on first use and passes requests through uncompressed until it lands, so no blocking startup pre-warm is needed — the port binds immediately either way, and Kompress activates within the first couple of requests on the first boot, then loads instantly on every boot after. The already-installed `proxy` extra's ONNX runtime is sufficient — the multi-gigabyte PyTorch `ml` extra is deliberately not installed.
## 1.26 (15-07-2026)
- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`.
- Add `PUID`/`PGID` add-on options (default `1000:1000`) and remap `abc` to that identity at the very start of folder setup, before any ownership is applied and before any service resolves the user. The base image's `init-adduser` is pinned to the same effective identity so it can no longer remap `abc` mid-startup.
- In `permission_mode: bypass`, a configured `PUID: 0` automatically falls back to UID `1000` (Claude Code refuses bypass permissions as root), retaining the configured group.
- Fix `bashio::config.array: command not found` in the TokenSave repository setup, tools configuration, and `claude-tools-doctor.sh`: the function only exists in the repo's standalone bashio, not in the real bashio shipped in the image. Use `bashio::config`, which prints list entries one per line.
- Return managed Claude configuration files to the effective `abc` identity instead of the raw configured `PUID`/`PGID` (which previously fell back to `0` and left the files root-owned).
- Pre-create `/tmp/.X11-unix` with the standard sticky mode so Xorg, which runs as the non-root `abc` user on a tmpfs `/tmp`, no longer fails to create its socket directory (`_XSERVTransmkdir: euid != 0`).
## 1.25 (15-07-2026)
- Minor bugs fixed
## 1.24 (15-07-2026)
- Fix the `/usr/local/bin/claude` wrapper never routing terminal Claude Code sessions through the Headroom proxy: it hardcoded `HEADROOM_BIN="/usr/local/bin/headroom"` while the binary is installed at `/usr/bin/headroom`, so the executable check always failed and the wrapper fell back to launching Claude Code directly. Resolve the binary with `command -v headroom` instead.
- Harden startup TokenSave indexing so an interrupted `init`/`sync` or a hard add-on stop can no longer leave a corrupt semantic graph that fails every subsequent boot. Each configured repository is now prepared under a startup-scoped `flock` (serialised against overlapping restarts and mid-boot git sync hooks); an existing index is refreshed with a retried incremental `sync` (transient `SQLITE_BUSY` no longer looks like corruption); and only a genuinely unreadable index — or a half-written one flagged by an `init` sentinel — is quarantined to `.tokensave/corrupt-<timestamp>/` and rebuilt from scratch, so the graph self-heals instead of propagating corruption.
## 1.23 (15-07-2026)
- Add a `ha-cli` helper that lets Claude configure Home Assistant (automations, scripts, scenes, helpers, dashboards, area/label/floor/entity registries, and service calls) through the Home Assistant Core API instead of a filesystem mount. It authenticates automatically with the add-on's `SUPERVISOR_TOKEN` via the Supervisor Core-API proxy (no token setup), and deliberately cannot reach `configuration.yaml`/`secrets.yaml` or other add-ons' credentials. Toggle with the new `enable_ha_api_helper` option (default on), which also controls a managed guidance block appended to `~/.claude/CLAUDE.md`.
## 1.21 (15-07-2026)
- Fix Claude Code bypass permissions being rejected when the add-on uses its default root `PUID`.
- In `permission_mode: bypass`, remap the shared `abc` Desktop runtime to an unused non-root UID before storage ownership and Selkies startup, while retaining its configured primary group for mounted-path access.
- Make folder setup and final Claude configuration ownership follow the effective `abc` identity instead of the configured root UID.
- Drop root console invocations of the add-on's `/usr/local/bin/claude` wrapper to the non-root `abc` runtime before passing `--dangerously-skip-permissions`.
- Extend `claude-tools-doctor.sh` with configured/effective UID and GID checks for bypass mode.
## 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)
- **Breaking:** remove the standalone Claude Code web terminal (ttyd/tmux service, port `7681`, and the `enable_terminal`, `terminal_username`, `terminal_password`, `terminal_workspace` options). The add-on is now built purely around Claude Desktop; Claude Code remains installed and powers Desktop cowork/dispatch sessions with the RTK hook, Caveman, and MCP servers intact. If the add-on refuses to start after the update, open its Configuration tab and re-save to drop the removed options.
- Remove the `claude-direct` and `claude-headroom` terminal wrapper scripts and the unused `ha_smart_context` and `dangerously_skip_permissions` options.
@@ -15,7 +90,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)
+41 -10
View File
@@ -11,7 +11,7 @@ ARG BUILD_FROM
ARG BUILD_VERSION
ARG RTK_VERSION="v0.43.0"
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
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
@@ -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" -o -name "ha-cli" \) -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 && \
@@ -92,15 +95,41 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
claude-desktop \
claude-code \
python3-pip \
gnome-keyring \
libsecret-1-0 \
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
ARG HADOLINT_VERSION=v2.14.0
ARG ACTIONLINT_VERSION=v1.7.12
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; \
curl -fsSL --retry 3 --retry-delay 2 \
-o /usr/local/bin/hadolint \
"https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-linux-${hadolint_arch}"; \
chmod 0755 /usr/local/bin/hadolint; \
curl -fsSL --retry 3 --retry-delay 2 \
-o /tmp/actionlint.tar.gz \
"https://github.com/rhysd/actionlint/releases/download/${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION#v}_linux_${actionlint_arch}.tar.gz"; \
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
@@ -108,11 +137,14 @@ 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
# installer used for the additional_pip option).
# plus mcp-proxy (stdio->HTTP bridge for the Home Assistant MCP server) and uv (fast
# installer used for the additional_pip option). The `proxy` extra already ships the ONNX
# runtime + transformers needed by the Kompress compressor — the `ml` extra (full PyTorch,
# ~5 GB with CUDA wheels) is deliberately NOT installed; svc-headroom pre-warms the ONNX
# model into the persistent HF cache instead.
RUN apt-get update && \
apt-get install -y --no-install-recommends nodejs && \
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv && \
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /root/.cache
@@ -160,7 +192,6 @@ CMD [ "/ha_entrypoint.sh" ]
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
+191 -35
View File
@@ -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,40 @@ 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.
- The shared `abc` desktop account runs under the configured `PUID`/`PGID`
(default `1000:1000`). When `permission_mode: bypass` is selected while
`PUID` is `0`, the add-on automatically falls back to UID `1000` before
Selkies and Claude Desktop start, because Claude Code refuses bypass mode
under an effective root UID.
- **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,65 +66,154 @@ 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.
- 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`.
- Configurable Claude Code permissions: strict prompts, automatic safe-action
approval, or explicit full bypass for trusted installations.
- Automatic non-root runtime enforcement for bypass mode, including root-console
wrapper launches.
- 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`).
- 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, runtime identity,
and gains.
- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
## Options
| Option | Default | Description |
| ------ | ------- | ----------- |
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. |
| `PUID` / `PGID` | `1000` / `1000` | Numeric user and group of the shared `abc` desktop account that owns the data location and runs Claude Desktop. In bypass mode, a root `PUID` is automatically replaced at runtime by UID `1000` while the configured group is retained. |
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
| `KEYBOARD` | | Optional Selkies keyboard layout. |
| `PASSWORD` | | Optional password for direct Selkies ports. |
| `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. |
| `headroom_auto_compress` | `true` | Auto-compress large tool outputs in every Claude Code session via a managed `PostToolUse` hook. |
| `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. |
| `enable_ha_api_helper` | `true` | Ship the `ha-cli` Core-API helper and add guidance so Claude can configure Home Assistant without a `/config` mount. |
| `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.
Claude Code does not permit bypass mode when its effective UID is `0`. If the
add-on is configured with `PUID: 0`, selecting `bypass` runs the shared `abc`
runtime account as UID `1000` instead, before storage ownership and Desktop
startup. Its configured primary GID is retained, so group-based access to
mounted Home Assistant paths remains available. Strict and auto modes keep the
configured identity unchanged.
A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to
the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still
bypasses the add-on wrapper and will be rejected by Claude Code.
`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. Mounted paths must remain
accessible to the effective non-root UID or its retained group.
### 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:
With `headroom_auto_compress` enabled (the default), a managed Claude Code
`PostToolUse` hook additionally compresses large `Bash`/`Grep`/`Glob`/`WebFetch`
outputs (over ~4000 characters) in **every** session type — terminal, Desktop
cowork, dispatch, and cron — without the model having to remember to call the
MCP tools. The original output is kept in Headroom's local store for one hour
and can always be recovered with `mcp__headroom__headroom_retrieve` using the
hash printed in the compression marker. Error text (`stderr`) is never
compressed, and plain prose passes through unchanged; the savings come from
structured output such as JSON dumps, search results, and logs.
```text
http://<home-assistant-host>:8787/dashboard
The dashboard is disabled externally by default. To expose it:
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, configured and
effective runtime identities, 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
@@ -115,6 +229,45 @@ The add-on bridges Claude to the integration's stateless Streamable HTTP
endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home
Assistant instance is not reachable as `homeassistant:8123` from add-ons.
## Configuring Home Assistant (API helper)
When `enable_ha_api_helper` is on (the default), the add-on ships a `ha-cli`
command and tells Claude — via a managed block in `~/.claude/CLAUDE.md` — that
it can configure Home Assistant through the Home Assistant **Core API** rather
than a filesystem mount. This is deliberately more contained than mapping
`/config`: the API cannot read `configuration.yaml`, `secrets.yaml`, or any
other add-on's stored credentials.
`ha-cli` authenticates automatically with the add-on's `SUPERVISOR_TOKEN`
through the Supervisor Core-API proxy (the add-on already sets
`homeassistant_api: true`), so there is nothing to configure. It can create and
edit automations, scripts, and scenes; call any service; read entity states;
and, over WebSocket, manage helpers, dashboards, and the area/label/floor/entity
registries. Run `ha-cli --help` inside the add-on for the full command
reference.
```bash
ha-cli config # connectivity check
ha-cli get config/automation/config/<id> # read one automation
ha-cli post config/automation/config/<id> @new.json # create/update it
ha-cli call automation.reload # apply YAML-mode changes
ha-cli ws '{"type":"config/area_registry/list"}'
```
Security notes:
- The Supervisor proxy token grants **admin-equivalent** Core API access (it can
call any service and edit any UI-managed configuration), but it cannot reach
the raw YAML files or other add-ons' data. For a tighter scope, set
`HA_BASE_URL`/`HA_TOKEN` (or the `ha_mcp_token` option) to a limited Home
Assistant user's long-lived token — `ha-cli` prefers those when present.
- The guidance instructs Claude to read each object and show you the intended
change before writing, but Claude Code's own tool-permission prompts remain
the real gate: each `ha-cli` call still needs your approval unless
`permission_mode` is set to `bypass`.
- Set `enable_ha_api_helper: false` to remove both the guidance block and the
helper's registration if you do not want Claude configuring Home Assistant.
## Custom scripts
The add-on includes the repository standard custom-script executor. On first
@@ -129,9 +282,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 -2
View File
@@ -1,6 +1,6 @@
{
"build_from": {
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm",
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm"
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm-45960cc3-ls113",
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm-45960cc3-ls113"
}
}
+29 -11
View File
@@ -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
@@ -13,8 +13,8 @@ environment:
AUTO_GPU: "1"
FM_HOME: /data/data
HOME: /data/data
PGID: "0"
PUID: "0"
PGID: "1000"
PUID: "1000"
SELKIES_FRAMERATE: "30"
START_DOCKER: "false"
TITLE: Claude Desktop
@@ -34,31 +34,37 @@ name: Claude Desktop
options:
env_vars: []
DNS_server: 8.8.8.8
PGID: 1000
PUID: 1000
data_location: /data/data
PGID: 0
PUID: 0
additional_apps: ""
additional_pip: ""
auto_update: true
github_email: ""
enable_ha_mcp: false
ha_mcp_url: http://homeassistant:8123/api/mcp
ha_mcp_token: ""
enable_ha_api_helper: true
github_token: ""
github_username: ""
install_caveman: true
enable_tools_health_report: true
expose_headroom_dashboard: false
headroom_auto_compress: true
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
@@ -76,21 +82,33 @@ schema:
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
additional_apps: str?
additional_pip: str?
auto_update: bool?
cifsdomain: str?
cifspassword: str?
cifsusername: str?
localdisks: str?
networkdisks: str?
github_email: str?
enable_ha_mcp: bool?
ha_mcp_url: str?
ha_mcp_token: password?
enable_ha_api_helper: bool?
github_token: password?
github_username: str?
enable_tools_health_report: bool
expose_headroom_dashboard: bool
headroom_auto_compress: 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.17"
version: "1.32"
video: true
+4 -20
View File
@@ -12,23 +12,7 @@ export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK
dbus-update-activation-environment --all >/dev/null 2>&1 || true
# --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).
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
DEFAULT_CLAUDE_DESKTOP_COMMAND="claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret"
if [ -s "$CLAUDE_DESKTOP_COMMAND_FILE" ]; then
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
# falling back to plaintext (and warning that the sign-in will not be saved). Headroom is
# intentionally not injected into the Desktop process: Claude Desktop force-overrides
# ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom MCP tools.
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
+1 -1
View File
@@ -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
@@ -3,9 +3,35 @@
# shellcheck disable=SC2046
set -e
# Define user
PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID")
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is
# what lets the tokensave/rtk/git setup in the 8x scripts run under the final identity.
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '1000'; fi)"
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)"
# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode
# always needs a non-root desktop user.
if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0"
PUID=1000
fi
groupmod -o -g "$PGID" abc 2> /dev/null || true
usermod -o -u "$PUID" abc 2> /dev/null || true
if [ "$(id -u abc)" -ne "$PUID" ] || [ "$(id -g abc)" -ne "$PGID" ]; then
PUID="$(id -u abc)"
PGID="$(id -g abc)"
bashio::log.warning "Unable to remap the abc desktop user; continuing with its current identity ${PUID}:${PGID}"
fi
# The base image's init-adduser reads PUID/PGID from the raw add-on options (default 0) and
# runs mid-startup, racing the services. Pin it to the effective identity chosen above so it
# can never remap abc away from the ownership applied below.
ADDUSER_RUN="/etc/s6-overlay/s6-rc.d/init-adduser/run"
if [ -f "$ADDUSER_RUN" ]; then
sed -i "s|^PUID=.*|PUID=${PUID}|;s|^PGID=.*|PGID=${PGID}|" "$ADDUSER_RUN"
fi
# Check data location
LOCATION="$(bashio::config 'data_location')"
@@ -59,10 +85,23 @@ printf "%s" "$LOCATION" > "$S6_ENVDIR/HOME"
printf "%s" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
grep -qxF "export HOME=\"$LOCATION\"" ~/.bashrc 2>/dev/null || {
# Re-derived on every boot rather than gated on a "does it already say $LOCATION" grep: that
# guard only ever recognized the CURRENT $LOCATION, so a user who changed data_location and
# later changed it back left two stale HOME/FM_HOME exports in ~/.bashrc, with the last one
# (not necessarily the correct one) winning for every interactive shell. The marker makes this
# idempotent regardless of how many times $LOCATION has changed: strip any previously managed
# block, then append one that reflects the current value.
BASHRC_HOME_BEGIN="# --- BEGIN ADDON HOME (managed) ---"
BASHRC_HOME_END="# --- END ADDON HOME (managed) ---"
if [ -f ~/.bashrc ]; then
sed -i "/^${BASHRC_HOME_BEGIN}\$/,/^${BASHRC_HOME_END}\$/d" ~/.bashrc
fi
{
printf "%s\n" "$BASHRC_HOME_BEGIN"
printf "%s\n" "export HOME=\"$LOCATION\""
printf "%s\n" "export FM_HOME=\"$LOCATION\""
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
printf "%s\n" "$BASHRC_HOME_END"
} >> ~/.bashrc
bashio::log.info "Creating $LOCATION"
@@ -70,6 +109,11 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
chmod 755 /tmp/cache
chmod 700 "$XDG_RUNTIME_DIR"
# /tmp is a tmpfs and Xorg runs as the non-root abc user, which cannot create the X11 socket
# directory itself (_XSERVTransmkdir: euid != 0). Pre-create it with the standard sticky mode.
mkdir -p /tmp/.X11-unix
chmod 1777 /tmp/.X11-unix
# Pre-create the Selkies joystick log so the base image's "chmod 777 /tmp/selkies*"
# calls (in init-selkies-config and svc-de) never fail on an empty glob.
touch /tmp/selkies_js.log
@@ -81,7 +125,7 @@ fi
ln -sfn /tmp/cache "$LOCATION/.cache"
bashio::log.info "Setting ownership to $PUID:$PGID"
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
chown -R "${PUID}:${PGID}" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
chmod -R 700 "$LOCATION"
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
@@ -0,0 +1,52 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Grant the shared desktop user (abc) access to the exposed GPU render nodes *before* the
# graphical services start.
#
# The LinuxServer base image already sets up /dev/dri group access, but it does so in its
# init-video s6 oneshot, which is NOT a dependency of svc-xorg/svc-selkies/svc-de. On Home
# Assistant those long-running services routinely start (as abc, via s6-setuidgid) before
# init-video has added abc to the render group, so Xorg/Selkies/pixelflux open the render
# device with the wrong credentials and fail:
#
# libEGL warning: failed to open /dev/dri/card0: Permission denied
#
# With no usable render node the video pipeline never produces frames, so the Selkies web
# client stays on "waiting for stream" indefinitely and Claude Desktop never appears.
#
# cont-init.d runs to completion before any s6-rc service is started, so preparing the DRI
# nodes here wins that race. Everything is best-effort: a host that exposes no GPU simply has
# no nodes to touch and this is a no-op.
shopt -s nullglob
dri_nodes=(/dev/dri/card* /dev/dri/render*)
if [ "${#dri_nodes[@]}" -eq 0 ]; then
bashio::log.info "No /dev/dri render nodes exposed; skipping GPU permission setup"
exit 0
fi
for node in "${dri_nodes[@]}"; do
[ -e "$node" ] || continue
# Mirror the base image's init-video logic (add abc to the node's owning group, creating
# the group when the GID is unnamed) but early enough that the s6-setuidgid at service
# start picks the membership up.
gid="$(stat -c '%g' "$node")"
gname="$(getent group "$gid" | awk -F: '{print $1}')"
if [ -z "$gname" ]; then
gname="dri${gid}"
groupadd -o -g "$gid" "$gname" 2> /dev/null || true
fi
if ! id -G abc 2> /dev/null | tr ' ' '\n' | grep -qx "$gid"; then
usermod -a -G "$gname" abc 2> /dev/null || true
fi
# Guarantee access even where group propagation is unreliable inside the add-on sandbox:
# this is a single-user desktop container, so world read/write on the local render node
# is acceptable and removes any dependency on group-membership timing.
chmod o+rw "$node" 2> /dev/null || true
bashio::log.info "GPU: prepared ${node} (group ${gname}:${gid}) for the desktop user"
done
@@ -1,58 +1,48 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2015
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
bashio::log.info "Installing additional apps :"
NEWAPPS=$(bashio::config 'additional_apps')
if command -v "apt-get" &> /dev/null; then
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
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
for packagestoinstall in $(bashio::config 'additional_apps' | tr ',' ' '); do
bashio::log.green "... $packagestoinstall"
if command -v "apk" &> /dev/null; then
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
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || bashio::log.fatal "Error : $packagestoinstall not found"
done
fi
if bashio::config.has_value 'additional_pip'; then
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
bashio::log.green "... pip: $p"
# Prefer uv (much faster resolver/installer); fall back to pip3 when unavailable.
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
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
done
fi
if bashio::config.has_value 'TZ'; then
TIMEZONE=$(bashio::config 'TZ')
bashio::log.info "Setting timezone to $TIMEZONE"
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
echo "$TIMEZONE" > /etc/timezone
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")
if [ -f "/usr/share/zoneinfo/$TIMEZONE" ]; then
bashio::log.info "Setting timezone to $TIMEZONE"
ln -snf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime
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
KEYBOARD=$(bashio::config '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
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2>/dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
fi || true
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2> /dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
fi
if bashio::config.has_value 'PASSWORD'; then
bashio::log.info "Setting password to the value defined in options"
PASSWORD=$(bashio::config 'PASSWORD')
passwd -d 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 "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"
@@ -2,11 +2,9 @@
# shellcheck shell=bash
set -e
if bashio::config.true 'auto_update'; then
bashio::log.info "Checking for Claude Desktop updates..."
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
else
bashio::log.warning "Update check failed (offline?), keeping current version"
fi
bashio::log.info "Checking for Claude Desktop updates..."
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
else
bashio::log.warning "Update check failed (offline?), keeping current version"
fi
@@ -3,19 +3,130 @@
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)"
mkdir -p "$HOME/.claude"
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
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"
run_as_runtime_user() {
s6-setuidgid abc env HOME="$HOME" "$@"
}
# 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.
# Managed, idempotent guidance block in the user's global CLAUDE.md, delimited by
# "<!-- BEGIN/END <name> (managed by claude_desktop addon) -->" markers. `add` appends the
# 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
# 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 +149,148 @@ 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
# 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
@@ -63,6 +312,7 @@ if bashio::config.true 'enable_ha_mcp'; then
fi
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
@@ -80,7 +330,15 @@ 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"],
# The MCP server is a separate process from the svc-headroom proxy longrun and does
# not inherit its HF_HOME export, so Kompress falls back to the default (tmpfs, wiped
# every restart) cache dir, never finds the model, and silently no-ops every
# compression request. Point it at the same persistent cache the proxy warms.
"env": {"HF_HOME": os.environ["HEADROOM_HF_HOME"]},
}
if os.environ["TOKENSAVE_ENABLED"] == "true":
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
if os.environ["HA_MCP_ENABLED"] == "true":
@@ -98,6 +356,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 +365,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:
@@ -133,31 +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):
del servers[name]
changed = True
if not changed:
continue
if servers:
data["mcpServers"] = servers
else:
data.pop("mcpServers", None)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
# The Home Assistant long-lived access token is stored here in clear text.
path.chmod(0o600)
if changed:
if servers:
data["mcpServers"] = servers
else:
data.pop("mcpServers", None)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
# The Home Assistant long-lived access token is stored here in clear text. Enforced even
# on no-change boots because tokensave's own writes can recreate the file with default
# permissions between merges.
if path.exists():
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.
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
if bashio::config.true 'install_headroom'; 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"
{
[ -s "$CLAUDE_MD" ] && printf '\n'
cat <<'MD'
<!-- BEGIN headroom (managed by claude_desktop addon) -->
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
# real savings when transparent proxying is unavailable.
if $HEADROOM_ENABLED; then
manage_claude_md_block headroom add <<'MD'
## Headroom context compression
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
@@ -167,40 +420,32 @@ 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
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.
<!-- END headroom (managed by claude_desktop addon) -->
MD
} >> "$CLAUDE_MD"
fi
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
else
manage_claude_md_block headroom remove
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"
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
# ~/.claude/settings.json. Claude Code writes settings `env` entries into the process
# environment at startup, replacing inherited values — this is the only supported way to reach
# Desktop cowork/local-agent-mode sessions, which spawn the bundled CLI at an absolute path
# (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the production endpoint
# (headroom #869). Managed-value semantics: only set or remove the variable when it is absent
# or already equals the add-on-managed proxy URL, so a user-customized endpoint is never
# clobbered. The svc-headroom longrun is s6-supervised, so a crashed proxy restarts within
# seconds; the terminal wrapper's per-launch health check remains as an extra safety net.
if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
HEADROOM_ROUTE_ACTION="add"
else
HEADROOM_ROUTE_ACTION="remove"
fi
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
import json
import os
from pathlib import Path
MANAGED_URL = "http://127.0.0.1:8787"
path = Path.home() / ".claude" / "settings.json"
try:
data = json.loads(path.read_text()) if path.exists() else {}
@@ -210,79 +455,96 @@ 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")
env = data.get("env")
if not isinstance(env, dict):
env = {}
current = env.get("ANTHROPIC_BASE_URL")
changed = False
if os.environ["HEADROOM_ROUTE_ACTION"] == "add":
if current is None or current == MANAGED_URL:
if current != MANAGED_URL:
env["ANTHROPIC_BASE_URL"] = MANAGED_URL
changed = True
else:
print(f"Claude settings env already sets ANTHROPIC_BASE_URL={current}; leaving it untouched")
elif current == MANAGED_URL:
del env["ANTHROPIC_BASE_URL"]
changed = True
if changed:
if env:
data["env"] = env
elif "env" in data:
del data["env"]
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
fi
# Compress large tool outputs automatically in every Claude Code session via a managed
# PostToolUse hook. 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
# 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
# and swapped in through hookSpecificOutput.updatedToolOutput, with the original kept in the
# shared CCR store so the model can fetch it back with mcp__headroom__headroom_retrieve. The
# script fails open (any error leaves the tool output untouched) and its --self-test gate
# keeps a broken interpreter path from registering a hook that would warn on every tool call.
HEADROOM_HOOK_CMD="/usr/local/bin/headroom-posttooluse-compress.py"
HEADROOM_HOOK_ACTION="remove"
if $HEADROOM_ENABLED && bashio::config.true 'headroom_auto_compress'; then
if run_as_runtime_user "$HEADROOM_HOOK_CMD" --self-test; then
HEADROOM_HOOK_ACTION="add"
bashio::log.info "Registering the Headroom PostToolUse auto-compression hook"
else
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
fi
fi
manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD" "$HEADROOM_HOOK_ACTION"
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed).
if bashio::config.true 'enable_ha_api_helper'; then
manage_claude_md_block ha-api-helper add <<'MD'
## Configuring Home Assistant
You can configure this Home Assistant instance through its Core API using the `ha-cli`
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
What is editable this way: automations, scripts, and scenes
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
say so instead of working around it.
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
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
(e.g. `ha-cli call automation.reload`).
MD
else
manage_claude_md_block ha-api-helper remove
fi
if bashio::config.true 'install_rtk'; then
if command -v rtk &> /dev/null; then
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 \
|| bashio::log.warning "rtk global files configuration failed"
manage_settings_hook PreToolUse Bash "rtk hook claude" add
else
bashio::log.warning "rtk is not available"
fi
elif [ -f "$HOME/.claude/settings.json" ]; then
bashio::log.info "Removing the add-on-managed rtk Claude Code hook"
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
else
manage_settings_hook PreToolUse Bash "rtk hook claude" remove
fi
if bashio::config.true 'install_caveman'; then
@@ -290,17 +552,13 @@ 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"
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
fi
# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed
# persistent files to the configured runtime UID/GID after all writes complete.
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
if [ -e "$managed_path" ]; then
chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
fi
done
# Ownership of everything written above is reconciled by 84-claude_runtime_ownership.sh after
# the remaining Claude configuration scripts have run.
@@ -0,0 +1,73 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PERMISSION_MODE="$(bashio::config 'permission_mode')"
SETTINGS_PATH="$HOME/.claude/settings.json"
case "$PERMISSION_MODE" in
strict | auto | bypass) ;;
*)
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
PERMISSION_MODE="strict"
;;
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")"
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
import json
import os
from pathlib import Path
MANAGED_VALUES = {"auto", "bypassPermissions"}
mode = os.environ["PERMISSION_MODE"]
settings_path = Path(os.environ["SETTINGS_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 = {}
permissions = settings.get("permissions")
if not isinstance(permissions, dict):
permissions = {}
if mode == "strict":
if permissions.get("defaultMode") in MANAGED_VALUES:
permissions.pop("defaultMode")
else:
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
# 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
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
@@ -17,25 +17,54 @@ if ! command -v gh > /dev/null 2>&1; then
exit 0
fi
# Everything below writes into the abc runtime user's HOME, so it must run AS abc. cont-init
# runs as root with HOME already pointing at the persistent data location, so plain
# `git config --global` recreated ~/.gitconfig owned by root:root on every start — and because
# that file is rewritten each boot, 20-folders.sh's earlier recursive chown never stuck to it.
# The user who actually runs git, gh and Claude was then unable to read its own committer
# identity or the gh credential helper, so every commit failed with "Author identity unknown"
# and authenticated pushes fell back to prompting. 20-folders.sh already remapped abc to the
# effective runtime identity (never root in bypass mode), so follow abc rather than re-reading
# the raw PUID/PGID options here.
RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)"
run_as_runtime_user() {
s6-setuidgid abc env HOME="$HOME" "$@"
}
# Reclaim any root-owned copies left by an earlier add-on version before writing as abc:
# these paths are not covered by 82-claude_tools.sh's ownership pass, and a root-owned
# ~/.gitconfig would make the first `git config` below fail outright under `set -e`.
mkdir -p "$HOME/.config"
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$HOME/.config"
for managed_path in "$HOME/.gitconfig" "$HOME/.config/gh"; 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
if bashio::config.has_value 'github_username'; then
git config --global user.name "$(bashio::config 'github_username')"
run_as_runtime_user git config --global user.name "$(bashio::config 'github_username')"
fi
if bashio::config.has_value 'github_email'; then
git config --global user.email "$(bashio::config 'github_email')"
run_as_runtime_user git config --global user.email "$(bashio::config 'github_email')"
fi
if bashio::config.has_value 'github_token'; then
token="$(bashio::config 'github_token')"
mkdir -p "$HOME/.config/gh"
chmod 700 "$HOME/.config/gh"
if env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
run_as_runtime_user mkdir -p "$HOME/.config/gh"
run_as_runtime_user chmod 700 "$HOME/.config/gh"
if run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
bashio::log.info "GitHub CLI already authenticated for github.com"
else
bashio::log.info "Configuring GitHub CLI authentication for github.com"
printf '%s\n' "$token" | env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
printf '%s\n' "$token" | run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
fi
env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
else
bashio::log.info "GitHub CLI available. Set github_token to authenticate gh and git operations."
fi
@@ -0,0 +1,17 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
# root). Reconcile ownership with that identity after all Claude configuration writes are
# complete, as a safety net in case any intermediate step re-owned a managed path.
RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)"
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 effective runtime ownership on $managed_path"
fi
done
@@ -1,11 +1,30 @@
#!/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
# Kompress (the ONNX compression engine) needs its model in the local HF cache: the
# proxy's startup preload is deliberately cache-only, and the default HF cache lands
# under ~/.cache, which the add-on points at tmpfs (/tmp/cache) — wiped on every
# restart. Without a warm persistent cache the proxy ran forever in "deferred" mode
# and recorded zero compression savings. Point the cache at persistent storage;
# nothing else is needed here — the proxy's own request path already downloads a
# missing model in the background on first use (ensure_background_load) and passes
# requests through uncompressed until it lands, so this self-heals within a couple of
# requests on the first boot and loads instantly (eager preload) on every boot after.
# A synchronous pre-warm was tried here and removed: it blocked the port bind for up
# to the download's duration, which left the settings-managed ANTHROPIC_BASE_URL
# (see 82-claude_tools.sh) pointing at a proxy that wasn't listening yet.
export HF_HOME="${HOME}/.headroom/hf"
mkdir -p "$HF_HOME"
chown abc:abc "$HF_HOME" 2> /dev/null || true
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
@@ -0,0 +1,53 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -o pipefail
REAL_CLAUDE="/usr/bin/claude"
HEADROOM_BIN="$(command -v headroom || true)"
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
# Claude Code rejects bypass mode when the effective UID is 0. Normal Desktop sessions run
# as abc, which startup remaps to a non-root UID when bypass is selected. Also handle a user
# invoking this wrapper directly from a root container console by dropping to abc here.
if [ "$PERMISSION_MODE" = "bypass" ] && [ "$(id -u)" -eq 0 ]; then
if command -v s6-setuidgid > /dev/null 2>&1 && [ "$(id -u abc)" -ne 0 ]; then
exec s6-setuidgid abc "$0" "$@"
fi
echo "claude wrapper: bypass mode requires a non-root runtime user, but abc is still UID 0" >&2
exit 1
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 ====="
+178
View File
@@ -0,0 +1,178 @@
#!/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 "Runtime identity"
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
if [ "$(id -u abc)" -eq 0 ]; then
echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root"
else
echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID"
fi
fi
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")
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
# 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
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
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 <<< "$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
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""ha-cli — talk to the local Home Assistant Core API from inside the add-on.
Lets Claude Code configure Home Assistant (automations, scripts, scenes,
helpers, dashboards, areas/labels, service calls) through the API, without any
`/config` filesystem mount. `secrets.yaml` and other add-ons' credentials are
therefore never reachable.
Authentication and the base URL are resolved automatically, in this order:
1. $HA_BASE_URL + $HA_TOKEN explicit override (advanced/scoped)
2. `ha_mcp_token` in /data/options.json scoped user long-lived token -> :8123
3. $SUPERVISOR_TOKEN Supervisor Core-API proxy fallback
(admin-equivalent; needs
homeassistant_api: true, which this
add-on sets — zero setup)
The scoped token is checked before the Supervisor fallback so setting
`ha_mcp_token` actually narrows access instead of being shadowed by the
always-present admin-equivalent Supervisor token.
Subcommands:
ha-cli get <path> GET e.g. get config/automation/config/1700000000
ha-cli post <path> [BODY] POST BODY = inline JSON, @file, or - (stdin)
ha-cli delete <path> DELETE
ha-cli call <domain.service> [BODY] call a service (BODY = JSON service data)
ha-cli states [entity_id] all states, or one entity
ha-cli config GET /config (sanity check / core info)
ha-cli ws <BODY> one WebSocket command (BODY = JSON, @file, or -)
`<path>` is relative to the REST API root; a leading slash and/or `api/` prefix
are optional. Responses are printed as formatted JSON. Exit code is non-zero on
HTTP or API errors.
Use the WebSocket subcommand for things the REST API does not expose:
ha-cli ws '{"type":"config/area_registry/list"}'
ha-cli ws '{"type":"input_boolean/create","name":"Guest mode","icon":"mdi:account"}'
ha-cli ws '{"type":"lovelace/config","url_path":null}'
"""
import json
import os
import sys
import urllib.error
import urllib.request
def _load_option(name):
"""Read a single option from the add-on's /data/options.json, if present."""
try:
with open("/data/options.json", encoding="utf-8") as handle:
return json.load(handle).get(name)
except (OSError, ValueError):
return None
def _helper_enabled():
"""Mirror config.yaml's enable_ha_api_helper default (true) when unset."""
value = _load_option("enable_ha_api_helper")
return value is not False
def resolve_endpoint():
"""Return (rest_base, ws_url, token) for the best available auth path."""
base = os.environ.get("HA_BASE_URL")
token = os.environ.get("HA_TOKEN")
if base and token:
rest = base.rstrip("/")
if not rest.endswith("/api"):
rest += "/api"
ws = rest.replace("http", "ws", 1).rsplit("/api", 1)[0] + "/api/websocket"
return rest, ws, token
# Checked before SUPERVISOR_TOKEN: this add-on always sets homeassistant_api,
# so the admin-equivalent Supervisor token is otherwise always present and
# would shadow a user's deliberately scoped-down ha_mcp_token.
token = _load_option("ha_mcp_token")
if token:
return (
"http://homeassistant:8123/api",
"ws://homeassistant:8123/api/websocket",
token,
)
token = os.environ.get("SUPERVISOR_TOKEN")
if token:
return "http://supervisor/core/api", "ws://supervisor/core/websocket", token
sys.exit(
"ha-cli: no credentials. Expected ha_mcp_token in the add-on options "
"(scoped user), $SUPERVISOR_TOKEN (admin-equivalent fallback, default "
"inside the add-on), or an explicit $HA_BASE_URL+$HA_TOKEN override."
)
def _url(base, path):
path = path.lstrip("/")
if path.startswith("api/"):
path = path[len("api/"):]
return base.rstrip("/") + "/" + path
def _read_body(arg):
"""Resolve an inline-JSON / @file / - (stdin) body argument to a dict/list."""
if arg is None:
return None
if arg == "-":
raw = sys.stdin.read()
elif arg.startswith("@"):
with open(arg[1:], encoding="utf-8") as handle:
raw = handle.read()
else:
raw = arg
raw = raw.strip()
if not raw:
return None
try:
return json.loads(raw)
except ValueError as exc:
sys.exit(f"ha-cli: body is not valid JSON: {exc}")
def _print(obj):
if isinstance(obj, (dict, list)):
print(json.dumps(obj, indent=2, ensure_ascii=False))
elif obj not in (None, ""):
print(obj)
def rest(method, base, token, path, body=None):
data = None
headers = {"Authorization": f"Bearer {token}"}
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
text = resp.read().decode("utf-8")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace").strip()
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
except urllib.error.URLError as exc:
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
try:
return json.loads(text) if text.strip() else None
except ValueError:
return text
def ws_command(ws_url, token, command):
try:
import asyncio
import websockets
except ImportError:
sys.exit(
"ha-cli: the 'websockets' Python package is required for the ws "
"subcommand. REST subcommands work without it."
)
async def run():
async with websockets.connect(ws_url, max_size=None) as sock:
hello = json.loads(await sock.recv())
if hello.get("type") != "auth_required":
raise SystemExit(f"ha-cli: unexpected WS greeting: {hello}")
await sock.send(json.dumps({"type": "auth", "access_token": token}))
if json.loads(await sock.recv()).get("type") != "auth_ok":
raise SystemExit("ha-cli: WebSocket authentication failed")
payload = dict(command)
payload["id"] = 1
await sock.send(json.dumps(payload))
while True:
msg = json.loads(await sock.recv())
if msg.get("id") == 1 and msg.get("type") == "result":
return msg
result = asyncio.run(run())
if not result.get("success", True):
_print(result.get("error", result))
sys.exit(1)
return result.get("result", result)
def main(argv):
if not argv or argv[0] in ("-h", "--help", "help"):
print(__doc__)
return 0
if not _helper_enabled():
sys.exit(
"ha-cli: disabled (enable_ha_api_helper is false in the add-on "
"options). Enable it there to let Claude configure Home Assistant."
)
rest_base, ws_url, token = resolve_endpoint()
cmd, args = argv[0], argv[1:]
if cmd == "get":
if len(args) != 1:
sys.exit("usage: ha-cli get <path>")
_print(rest("GET", rest_base, token, args[0]))
elif cmd == "post":
if not args:
sys.exit("usage: ha-cli post <path> [BODY]")
body = _read_body(args[1]) if len(args) > 1 else None
_print(rest("POST", rest_base, token, args[0], body))
elif cmd == "delete":
if len(args) != 1:
sys.exit("usage: ha-cli delete <path>")
_print(rest("DELETE", rest_base, token, args[0]))
elif cmd == "call":
if not args or "." not in args[0]:
sys.exit("usage: ha-cli call <domain.service> [BODY]")
domain, service = args[0].split(".", 1)
body = _read_body(args[1]) if len(args) > 1 else None
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
elif cmd == "states":
path = f"states/{args[0]}" if args else "states"
_print(rest("GET", rest_base, token, path))
elif cmd == "config":
_print(rest("GET", rest_base, token, "config"))
elif cmd == "ws":
if len(args) != 1:
sys.exit("usage: ha-cli ws <BODY>")
command = _read_body(args[0])
if not isinstance(command, dict) or "type" not in command:
sys.exit('ha-cli: ws BODY must be a JSON object with a "type" field')
_print(ws_command(ws_url, token, command))
else:
sys.exit(f"ha-cli: unknown subcommand '{cmd}' (try: ha-cli --help)")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,218 @@
#!/lsiopy/bin/python3
"""Claude Code PostToolUse hook: auto-compress large tool outputs through Headroom.
Registered in ~/.claude/settings.json by 82-claude_tools.sh (managed entry, matcher
"Bash|Grep|Glob|WebFetch"). Desktop-spawned Claude Code sessions cannot be routed
through the Headroom proxy (the Electron app pins ANTHROPIC_BASE_URL to the
production endpoint, headroom #869), so compression there used to depend on the
model voluntarily calling the headroom MCP tools. This hook makes it automatic for
every session type: when a matched tool returns a large output, the hook compresses
it with Headroom's rule-based pipeline and replaces the tool output via
hookSpecificOutput.updatedToolOutput, appending a retrieval marker. The original is
stored in Headroom's shared CCR store (SQLite at ~/.headroom/ccr_store.db — the
same store the headroom MCP server reads), so the model can always get the full
output back with mcp__headroom__headroom_retrieve.
Design constraints:
- Fail open: any error or non-compressible payload exits 0 with no output, leaving
the tool result untouched. A hook crash must never break a session.
- Fast path first: the payload is inspected before importing headroom (~0.6 s);
small outputs never pay the import cost.
- ML text compression (Kompress) is disabled: its model loads in the background,
which never completes inside a short-lived hook process. The rule-based
transforms (SmartCrusher for JSON, search/log/diff/tabular compressors) carry
the savings on tool output anyway; plain prose passes through unchanged.
- stderr fields are never compressed — error text must reach the model verbatim
(matching Headroom's own error-protection policy).
- File-list arrays (Glob's `filenames`, Grep's `filenames` in files_with_matches
mode — both typed `string[]` by the CLI's own output schema) are handled
separately from prose/JSON-blob fields: Headroom's SmartCrusher subsamples
JSON arrays for informational dumps, which is fine for e.g. a list of sensor
states but silently drops most paths from a file listing the model needs to
act on. Those fields are truncated deterministically instead (keep the first
N entries, append one marker string) so the model always sees a labeled cut
point rather than a shorter list it might mistake for the complete result.
"""
import json
import os
import sys
def _int_env(name: str, default: str) -> int:
try:
return int(os.environ.get(name, default))
except (TypeError, ValueError):
return int(default)
MIN_CHARS = _int_env("HEADROOM_HOOK_MIN_CHARS", "4000")
MIN_SAVED_TOKENS = _int_env("HEADROOM_HOOK_MIN_SAVED_TOKENS", "50")
ARRAY_KEEP = _int_env("HEADROOM_HOOK_ARRAY_KEEP", "40")
TTL_SECONDS = 3600 # matches the headroom MCP server's session TTL
SKIP_KEYS = {"stderr"}
def self_test() -> int:
"""Exit 0 when the interpreter can import headroom (used at registration time)."""
try:
import headroom # noqa: F401
return 0
except Exception:
return 1
def main() -> int:
if os.environ.get("HEADROOM_HOOK_DISABLE"):
return 0
try:
payload = json.load(sys.stdin)
except Exception:
return 0
if not isinstance(payload, dict):
return 0
response = payload.get("tool_response")
# Find big string/array fields before paying the headroom import cost.
def is_string_array(value):
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value)
if isinstance(response, str):
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
array_candidates = []
elif isinstance(response, dict):
string_candidates = [
key
for key, value in response.items()
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS
]
array_candidates = [
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value)
]
else:
string_candidates = []
array_candidates = []
if not string_candidates and not array_candidates:
return 0
# Keep Kompress's cache probe away from the tmpfs-backed ~/.cache default.
os.environ.setdefault("HF_HOME", os.path.expanduser("~/.headroom/hf"))
from headroom import savings_ledger
from headroom.cache.compression_store import get_compression_store
from headroom.compress import compress
store = None
totals = [0, 0] # tokens before, tokens after (only for rewritten fields)
def shrink(text):
nonlocal store
result = compress(
[{"role": "tool", "content": text}],
protect_recent=0,
kompress_model="disabled",
)
compressed = result.messages[0].get("content")
if not isinstance(compressed, str):
compressed = json.dumps(compressed)
saved = result.tokens_before - result.tokens_after
if saved < MIN_SAVED_TOKENS:
return None
if store is None:
store = get_compression_store()
hash_key = store.store(
original=text,
compressed=compressed,
original_tokens=result.tokens_before,
compressed_tokens=result.tokens_after,
compression_strategy="posttooluse_hook",
ttl=TTL_SECONDS,
)
totals[0] += result.tokens_before
totals[1] += result.tokens_after
return (
f"{compressed}\n"
f"[headroom: output compressed {result.tokens_before}->{result.tokens_after} tokens; "
f"call mcp__headroom__headroom_retrieve with hash={hash_key} if you need the full original]"
)
def shrink_array(items):
nonlocal store
original_json = json.dumps(items)
if len(original_json) < MIN_CHARS:
return None
kept = items[:ARRAY_KEEP]
truncated_json = json.dumps(kept)
# No ML/token-counter call needed for a plain truncation decision; a char/4
# estimate is the same fallback Headroom's own cost estimator uses and is
# only used here to decide eligibility and annotate the marker.
tokens_before = max(1, len(original_json) // 4)
tokens_after = max(1, len(truncated_json) // 4)
if tokens_before - tokens_after < MIN_SAVED_TOKENS:
return None
if store is None:
store = get_compression_store()
hash_key = store.store(
original=original_json,
compressed=truncated_json,
original_tokens=tokens_before,
compressed_tokens=tokens_after,
compression_strategy="posttooluse_hook_array_truncate",
ttl=TTL_SECONDS,
)
totals[0] += tokens_before
totals[1] += tokens_after
remaining = len(items) - len(kept)
marker = (
f"[headroom: {remaining} more of {len(items)} entries omitted "
f"({tokens_before}->{tokens_after} tokens); call mcp__headroom__headroom_retrieve "
f"with hash={hash_key} for the complete list]"
)
return kept + [marker]
updated = None
if isinstance(response, str):
updated = shrink(response)
else:
rewritten = dict(response)
changed = False
for key in string_candidates:
new_value = shrink(rewritten[key])
if new_value is not None:
rewritten[key] = new_value
changed = True
for key in array_candidates:
new_value = shrink_array(rewritten[key])
if new_value is not None:
rewritten[key] = new_value
changed = True
if changed:
updated = rewritten
if updated is None:
return 0
savings_ledger.record_savings_event(
tokens_before=totals[0],
tokens_after=totals[1],
client="posttooluse-hook",
source="hook",
)
json.dump(
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"updatedToolOutput": updated,
}
},
sys.stdout,
)
return 0
if __name__ == "__main__":
if "--self-test" in sys.argv:
sys.exit(self_test())
try:
sys.exit(main())
except Exception:
sys.exit(0)
+3
View File
@@ -1,4 +1,7 @@
## 2.2.0 (2026-07-16)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.1.2 (2026-07-11)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
+1 -1
View File
@@ -101,4 +101,4 @@ schema:
slug: codex
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "2.1.2"
version: "2.2.0"
+2 -2
View File
@@ -1,9 +1,9 @@
{
"github_beta": "true",
"last_update": "2026-07-11",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "codex",
"source": "github",
"upstream_repo": "ajslater/codex",
"upstream_version": "2.1.2"
"upstream_version": "2.2.0"
}
+3
View File
@@ -1,4 +1,7 @@
## 1.3.0 (2026-07-16)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
## 1.2.2 (2026-07-04)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
+1 -1
View File
@@ -45,5 +45,5 @@ schema:
username: str
slug: collabora
url: https://github.com/alexbelgium/hassio-addons
version: "1.2.2"
version: "1.3.0"
webui: "[PROTO:ssl]://[HOST]:[PORT:9980]/browser/dist/admin/admin.html"
+2 -2
View File
@@ -1,8 +1,8 @@
{
"last_update": "2026-07-04",
"last_update": "2026-07-16",
"repository": "alexbelgium/hassio-addons",
"slug": "collabora",
"source": "github",
"upstream_repo": "CollaboraOnline/online",
"upstream_version": "1.2.2"
"upstream_version": "1.3.0"
}
+2
View File
@@ -1,3 +1,5 @@
## 1.4.0-2 (16-07-2026)
- Minor bugs fixed
## 1.4.0 (2026-06-20)
- Update to latest version from gtsteffaniak/filebrowser (changelog : https://github.com/gtsteffaniak/filebrowser/releases)
+2 -1
View File
@@ -115,4 +115,5 @@ LABEL \
# 6 Healthcheck #
#################
# Upstream
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1
+1 -1
View File
@@ -114,4 +114,4 @@ schema:
slug: filebrowser_quantum
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.4.0"
version: "1.4.0.2"
+49 -4
View File
@@ -1,20 +1,60 @@
- Added support for configuring extra environment variables via the `env_vars` add-on option alongside config.yaml. See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
## 2.0.1 (2026-07-17)
- Aligned the pull-request build context with the production builder by copying
the shared Home Assistant helper scripts before building the add-on.
- Fixed aarch64 and amd64 PR validation failing on unresolved Dockerfile
`COPY` instructions. Runtime and migration behavior are unchanged.
## 2.0.0 (2026-07-17)
- Replaced the abandoned `vogler/free-games-claimer` upstream with
`P-Adamiec/Free-Games-Claimer-Remaster`.
- Reworked the image build for the remaster's Python, Chromium, TurboVNC, and
noVNC runtime on both amd64 and aarch64.
- Preserved the previous default one-shot behavior and Epic, Prime Gaming, and
GOG store selection.
- Kept noVNC on port 6080 for upgrade compatibility.
- Added `RUN_ONCE` and `STORES` options while retaining `CMD_ARGUMENTS` as a
deprecated compatibility input.
- Added an automatic, idempotent migration of legacy Epic, Prime Gaming, and
GOG JSON claim history into the remaster SQLite database.
- Preserved legacy files for rollback and documented the required one-time
Chromium login when a Firefox session cannot be migrated.
- Pinned the reviewed upstream source commit and paused generic automatic
updates so the add-on's independent `2.x` version cannot regress to `1.x`.
- Updated the configuration template, upstream metadata, and documentation.
- Added support for configuring extra environment variables via the `env_vars`
add-on option.
## 1.8 (2025-05-17)
- Update to latest version from vogler/free-games-claimer (changelog : https://github.com/vogler/free-games-claimer/releases)
- Update to latest version from vogler/free-games-claimer (changelog: https://github.com/vogler/free-games-claimer/releases)
## 1.7 (2025-03-08)
- Update to latest version from vogler/free-games-claimer (changelog : https://github.com/vogler/free-games-claimer/releases)
- Update to latest version from vogler/free-games-claimer (changelog: https://github.com/vogler/free-games-claimer/releases)
## 1.6-6 (2024-12-29)
- Minor bugs fixed
## 1.6-5 (2024-12-13)
- Minor bugs fixed
## 1.6-4 (2024-12-07)
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/free_games_claimer in a folder only accessible from my Filebrowser addon called /addon_configs/db21ed7f_free_games_claimer. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
- Major change: switch to the new Home Assistant add-on configuration logic.
Configuration files were migrated from
`/config/hassio_addons/free_games_claimer` to the private add-on configuration
directory available through compatible file browser add-ons.
## 1.6-3 (2024-12-05)
- Minor bugs fixed
## 1.6-2 (2024-12-05)
- Minor bugs fixed
## 1.6 (2023-12-30)
@@ -28,18 +68,23 @@
## 1.4 (2023-05-27)
- Update to latest version from vogler/free-games-claimer
## 1.4-5 (2023-05-26)
- Minor bugs fixed
## 1.4-4 (2023-05-26)
- Minor bugs fixed
## 1.4-3 (2023-05-26)
- Minor bugs fixed
## 1.4-2 (2023-05-25)
- Minor bugs fixed
## NOT_WORKING (2023-05-22)
- Minor bugs fixed
+132 -94
View File
@@ -1,91 +1,14 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
# _.------.
# _.-` ('>.-`"""-.
# '.--'` _'` _ .--.)
# -' '-.-';` `
# ' - _.' ``'--.
# '---` .-'""`
# /`
#=== Home Assistant Addon ===#
#=== Home Assistant Add-on ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM
ARG BUILD_VERSION
ARG BUILD_FROM="debian:bookworm-slim"
FROM ${BUILD_FROM}
##################
# 2 Modify Image #
##################
# Set S6 wait time
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_SERVICES_GRACETIME=0
##################
# 3 Install apps #
##################
# Add rootfs
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
# 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
# Modules
ARG MODULES="00-banner.sh 01-custom_script.sh"
# Automatic modules download
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES=""
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
################
# 4 Entrypoint #
################
# Add entrypoint
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
RUN \
# Change data folder
sed -i "s|/fgc|/data|g" /usr/local/bin/docker-entrypoint.sh && \
# Run scripts only once
sed -i "1a if [ -f /done ]; then exit 0; fi && touch /done" /ha_entrypoint.sh && \
sed -i "s=x11vnc=[[ \"\$(ps aux | grep \"x11vnc\" | grep -v grep)\" ]] || x11vnc=g" /usr/local/bin/docker-entrypoint.sh && \
# Update playwright
npx playwright install-deps && \
npx playwright install
WORKDIR /data
ENTRYPOINT [ "/usr/bin/env" ]
CMD [ "/ha_entrypoint.sh" ]
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
############
# 5 Labels #
############
ARG DEBIAN_FRONTEND="noninteractive"
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
@@ -93,28 +16,143 @@ ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
ARG UPSTREAM_REPOSITORY="P-Adamiec/Free-Games-Claimer-Remaster"
ARG UPSTREAM_REF="cca4992354215cef8807b748575af797606627f4"
ENV S6_CMD_WAIT_FOR_SERVICES="1" \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME="0" \
S6_SERVICES_GRACETIME="0" \
VNC_PORT="5900" \
NOVNC_PORT="6080" \
WIDTH="1280" \
HEIGHT="720" \
DEPTH="24" \
SHOW="1" \
COMMIT="${UPSTREAM_REF}" \
BRANCH="main" \
NOW="${BUILD_DATE}"
# Install the dependencies used by Free Games Claimer Remaster. The upstream
# Dockerfile supports both amd64 (Google Chrome) and arm64 (Chromium), so the
# add-on keeps its existing multi-architecture support.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
dos2unix \
gnupg \
gzip \
jq \
python3 \
python3-pip \
tar \
tini \
&& mkdir -p /etc/apt/keyrings \
&& curl --proto "=https" --tlsv1.2 -fsSL https://packagecloud.io/dcommander/virtualgl/gpgkey \
| gpg --dearmor -o /etc/apt/trusted.gpg.d/VirtualGL.gpg \
&& curl --proto "=https" --tlsv1.2 -fsSL https://packagecloud.io/dcommander/turbovnc/gpgkey \
| gpg --dearmor -o /etc/apt/trusted.gpg.d/TurboVNC.gpg \
&& curl --proto "=https" --tlsv1.2 -fsSL https://raw.githubusercontent.com/VirtualGL/repo/main/VirtualGL.list \
> /etc/apt/sources.list.d/VirtualGL.list \
&& curl --proto "=https" --tlsv1.2 -fsSL https://raw.githubusercontent.com/TurboVNC/repo/main/TurboVNC.list \
> /etc/apt/sources.list.d/TurboVNC.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
novnc \
ratpoison \
turbovnc \
virtualgl \
websockify \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libatspi2.0-0 \
libcairo2 \
libcups2 \
libgbm1 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxkbcommon0 \
&& if [ "$(dpkg --print-architecture)" = "amd64" ]; then \
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
| gpg --dearmor -o /etc/apt/keyrings/google-chrome.gpg; \
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list; \
apt-get update; \
apt-get install -y --no-install-recommends google-chrome-stable; \
else \
apt-get install -y --no-install-recommends chromium; \
fi \
&& ln -sf /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html \
&& ln -sf /usr/bin/python3 /usr/bin/python \
&& apt-get purge -y gnupg \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/* /var/tmp/* /tmp/* /usr/share/doc/*
# Install a deterministic snapshot of the replacement upstream. Updating the
# upstream reference is intentionally explicit so image contents cannot change
# without an add-on version bump.
WORKDIR /fgc
RUN curl --proto "=https" --tlsv1.2 -fsSL \
"https://github.com/${UPSTREAM_REPOSITORY}/archive/${UPSTREAM_REF}.tar.gz" \
-o /tmp/free-games-claimer-remaster.tar.gz \
&& tar -xzf /tmp/free-games-claimer-remaster.tar.gz --strip-components=1 -C /fgc \
&& python3 -m pip install --no-cache-dir --break-system-packages -r requirements.txt \
&& dos2unix ./*.sh \
&& chmod +x ./*.sh \
&& cp docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh \
&& chmod 0755 /usr/local/bin/docker-entrypoint.sh \
&& rm -f /tmp/free-games-claimer-remaster.tar.gz \
&& rm -rf /fgc/data \
&& ln -s /data /fgc/data
# Add Home Assistant integration files and shared helper modules.
COPY rootfs/ /
RUN find /etc/cont-init.d /usr/local/bin -type f -name "*.sh" -exec chmod 0755 {} + \
&& chmod 0755 /usr/local/bin/migrate_vogler_data.py
ARG MODULES="00-banner.sh 01-custom_script.sh"
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 0755 /ha_automodules.sh \
&& /ha_automodules.sh "${MODULES}" \
&& rm /ha_automodules.sh
ENV PACKAGES=""
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 0755 /ha_autoapps.sh \
&& /ha_autoapps.sh "${PACKAGES}" \
&& rm /ha_autoapps.sh
ENV S6_STAGE2_HOOK="/ha_entrypoint.sh"
COPY ha_entrypoint.sh /ha_entrypoint.sh
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /ha_entrypoint.sh /usr/local/lib/bashio-standalone.sh
EXPOSE 5900 6080
ENTRYPOINT ["/usr/bin/env"]
CMD ["/ha_entrypoint.sh"]
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
io.hass.version="${BUILD_VERSION}" \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.licenses="AGPL-3.0" \
org.opencontainers.image.url="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
####################
# 6 HealthcheckNOT #
####################
# Can't be implemented as container is optimized for memory usage, so the webserver and Node are spun down during idle
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/master/free_games_claimer/README.md" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${BUILD_REF}" \
org.opencontainers.image.version="${BUILD_VERSION}" \
io.hass.upstream="https://github.com/${UPSTREAM_REPOSITORY}/tree/${UPSTREAM_REF}"
+108 -72
View File
@@ -1,128 +1,164 @@
## &#9888; VNC not working on several machines. Please use config.env to execute the script
# Home Assistant add-on: Free Games Claimer
# Home assistant add-on: Free Games Claimer
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
If this add-on saves you time or makes your setup easier, I would be very grateful for your support!
I maintain this and other Home Assistant add-ons in my free time. Keeping up
with upstream changes, Home Assistant changes, and testing on real hardware
takes a significant amount of time.
[![Buy me a coffee][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
[![Donate via PayPal][paypal-badge]](https://www.paypal.com/donate/?hosted_button_id=DZFULJZTP3UQA)
## Addon informations
## Add-on information
![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fconfig.yaml)
![Ingress](https://img.shields.io/badge/dynamic/yaml?label=Ingress&query=%24.ingress&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fconfig.yaml)
![Arch](https://img.shields.io/badge/dynamic/yaml?color=success&label=Arch&query=%24.arch&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fconfig.yaml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9c6cf10bdbba45ecb202d7f579b5be0e)](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
[![GitHub Super-Linter](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/weekly-supelinter.yaml?label=Lint%20code%20base)](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
[![Builder](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/onpush_builder.yaml?label=Builder)](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
[donation-badge]: https://img.shields.io/badge/Buy%20me%20a%20coffee-%23d32f2f?logo=buy-me-a-coffee&style=flat&logoColor=white
[paypal-badge]: https://img.shields.io/badge/Donate%20via%20PayPal-0070BA?logo=paypal&style=flat&logoColor=white
_Thanks to everyone having starred my repo! To star it click on the image below, then it will be on top right. Thanks!_
[![Stargazers repo roster for @alexbelgium/hassio-addons](https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.github/stars2.svg)](https://github.com/alexbelgium/hassio-addons/stargazers)
![downloads evolution](https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/free_games_claimer/stats.png)
## About
[Free Games Claimer](https://github.com/vogler/free-games-claimer) : Claims free games periodically on
This add-on is based on
[Free Games Claimer Remaster](https://github.com/P-Adamiec/Free-Games-Claimer-Remaster).
It can claim free games from:
- Epic Games Store
- Amazon Prime Gaming
- GOG
- Live Games with Gold - planned
- Steam
- GamerPower-supported stores, when explicitly enabled
This addon is based on the docker image https://github.com/vogler/free-games-claimer
For compatibility with previous add-on releases, the default store selection
remains Epic Games, Prime Gaming, and GOG.
## Configuration
## Web interface
Webui can be found at <http://homeassistant:6080> (NoVNC interface - currently has issues on some machines).
The noVNC interface remains available on port `6080`:
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `CMD_ARGUMENTS` | str | `node epic-games ; node prime-gaming ; node gog` | Commands to run for claiming games |
| `CONFIG_LOCATION` | str | `/config/config.env` | Location of the configuration file |
### Example Configuration
```yaml
CMD_ARGUMENTS: "node epic-games ; node prime-gaming ; node gog"
CONFIG_LOCATION: "/config/config.env"
```text
http://homeassistant:6080
```
### Environment Configuration
It can be used for initial sign-in, CAPTCHA handling, or other manual browser
interaction. Set `VNC_PASSWORD` in `config.env` to protect the VNC session.
All main configuration is done via the `config.env` file located at `/config/addons_config/free_games_claimer/config.env`.
## Add-on options
If this file doesn't exist, it will be created at first boot with default settings.
| Option | Default | Description |
|--------|---------|-------------|
| `CONFIG_LOCATION` | `/config/config.env` | Persistent environment configuration file |
| `RUN_ONCE` | `true` | Run all selected claimers once, then stop the add-on as previous releases did |
| `STORES` | empty | Optional comma-separated override, such as `epic,prime,gog,steam` |
| `CMD_ARGUMENTS` | `node epic-games ; node prime-gaming ; node gog` | Deprecated compatibility option; recognized legacy command names are converted to `STORES` |
| `env_vars` | `[]` | Additional environment variables passed to the add-on |
### Required Environment Variables
### Run modes
Add these to your `config.env` file:
With `RUN_ONCE: true`, the add-on performs one claiming pass and stops. This is
the default and preserves the behavior of the former vogler-based add-on.
With `RUN_ONCE: false`, the remaster remains running and uses its internal
scheduler. Set `SCHEDULER_HOURS` in `config.env` to control the interval.
## Environment configuration
The add-on keeps its configuration in `CONFIG_LOCATION`, which defaults to
`/config/config.env`. From Home Assistant this is stored in the add-on's
private `addon_configs` directory and can be edited with a compatible file
browser add-on.
A template is created on first start. Common examples are:
```env
# Epic Games Store
# Preserve the former default selection
STORES=epic,prime,gog
# Epic Games
EG_EMAIL=your-email@example.com
EG_PASSWORD=your-password
EG_OTPKEY=
# Amazon Prime Gaming
PG_EMAIL=your-amazon-email@example.com
PG_PASSWORD=your-amazon-password
PG_PASSWORD=your-password
PG_OTPKEY=
# GOG (optional)
# GOG
GOG_EMAIL=your-gog-email@example.com
GOG_PASSWORD=your-gog-password
GOG_PASSWORD=your-password
# Notifications (optional)
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
EMAIL_USER=notifications@example.com
EMAIL_PASS=your-app-password
EMAIL_TO=recipient@example.com
# Optional Steam support
STEAM_USERNAME=your-steam-username
STEAM_PASSWORD=your-password
# Optional notifications
NOTIFY=tgram://bot-token/chat-id
# DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
```
### Additional Options
Existing variables such as `EG_EMAIL`, `EG_PASSWORD`, `PG_EMAIL`,
`PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`, `SHOW`, `WIDTH`,
`HEIGHT`, `TIMEOUT`, `LOGIN_TIMEOUT`, `DRYRUN`, and `NOTIFY` remain compatible.
See the
[upstream configuration reference](https://github.com/P-Adamiec/Free-Games-Claimer-Remaster#configuration)
for all available settings.
For complete configuration options and advanced settings, see: https://github.com/vogler/free-games-claimer#configuration--options
## Upgrade from version 1.8
### Important Notes
Version 2.0 changes the application engine from
`vogler/free-games-claimer` (Node.js, Playwright, and Firefox) to
`P-Adamiec/Free-Games-Claimer-Remaster` (Python, nodriver, and Chromium).
The add-on performs the following migration automatically on first start:
- **VNC Issues**: The NoVNC web interface is currently not working reliably on several machines
- **Recommended**: Use the `config.env` file for configuration instead of the web interface
- **Security**: Store credentials securely and consider using app-specific passwords where available
1. The existing `config.env` remains at the same configured location.
2. Legacy `epic-games.json`, `prime-gaming.json`, and `gog.json` claim history
is imported into the remaster SQLite database at `/data/fgc.db`.
3. Existing database rows are detected and are not duplicated if migration is
retried.
4. A pre-migration database backup is created when an existing `fgc.db` is
present.
5. All old files remain under `/data/data` for rollback or manual recovery.
### Custom Scripts and Environment Variables
Browser sessions cannot be converted because the old add-on used a shared
Firefox profile while the remaster uses separate Chromium profiles per store.
Credentials remain available through `config.env`, but accounts that require
interactive authentication may need a one-time login through noVNC after the
upgrade. The old Firefox profile is retained and is never deleted.
This addon supports custom scripts and environment variables through the `addon_config` mapping:
The external noVNC port remains `6080`, although the standalone remaster image
normally uses port `7080`.
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
## Upstream update policy
The image is built from an explicit upstream commit in the Dockerfile. This
keeps amd64 and aarch64 images reproducible and prevents an upstream branch or
container tag from changing without an add-on review and version bump.
The repository updater is intentionally paused for this add-on because the
add-on uses its own `2.x` version series while the replacement upstream uses a
`1.x` version series. An automatic replacement would risk a Home Assistant
version regression and would not safely update the pinned commit. A maintainer
upstream update must therefore update `UPSTREAM_REF`, `upstream_version`, the
add-on version, and `CHANGELOG.md` together.
## Installation
The installation of this add-on is pretty straightforward and not different in comparison to installing any other add-on.
1. Add this add-on repository to the Home Assistant add-on store.
2. Install **Free Games Claimer**.
3. Configure the add-on options as needed.
4. Start the add-on and review its log.
5. Open noVNC if an account needs manual authentication.
1. Add my add-ons repository to your home assistant instance (in supervisor addons store at top right, or click button below if you have configured my HA)
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
1. Install this add-on.
1. Click the `Save` button to store your configuration.
1. Set the add-on options to your preferences
1. Start the add-on.
1. Check the logs of the add-on to see if everything went well.
1. Open the webUI and adapt the software options
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
## Custom scripts and environment variables
- [Running custom scripts in add-ons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- [Passing environment variables to an add-on](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2)
## Support
Create an issue on github
[repository]: https://github.com/alexbelgium/hassio-addons
Open an issue in the
[add-on repository](https://github.com/alexbelgium/hassio-addons/issues).
+2 -2
View File
@@ -1,6 +1,6 @@
{
"build_from": {
"aarch64": "ghcr.io/vogler/free-games-claimer:latest",
"amd64": "ghcr.io/vogler/free-games-claimer:latest"
"aarch64": "debian:bookworm-slim",
"amd64": "debian:bookworm-slim"
}
}
+10 -7
View File
@@ -1,9 +1,8 @@
# Free Games Claimer Remaster
arch:
- aarch64
- amd64
description:
automatically claims free games on the Epic Games Store, Amazon Prime
Gaming and GOG
description: "Automatically claims free games from Epic Games Store, Amazon Prime Gaming, GOG, Steam, and optional GamerPower-supported stores"
devices:
- /dev/dri
- /dev/dri/card0
@@ -73,25 +72,29 @@ init: false
map:
- addon_config:rw
- homeassistant_config:rw
name: Free Games Claimer (NoVNC not working)
name: Free Games Claimer
options:
env_vars: []
CMD_ARGUMENTS: node epic-games ; node prime-gaming ; node gog
STORES: ""
RUN_ONCE: true
CONFIG_LOCATION: /config/config.env
ports:
5900/tcp: null
6080/tcp: 6080
ports_description:
5900/tcp: VNC port
6080/tcp: NOVNC port
6080/tcp: noVNC web interface
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
CMD_ARGUMENTS: str
CMD_ARGUMENTS: str?
STORES: str?
RUN_ONCE: bool
CONFIG_LOCATION: str
slug: free_games_claimer
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.8"
version: "2.0.1"
webui: "[PROTO:ssl]://[HOST]:[PORT:6080]"
@@ -1,85 +1,152 @@
#!/usr/bin/env bashio
# shellcheck shell=bash
set -e
set -eo pipefail
##############
# Initialize #
##############
CONFIG_FILE="/config/config.env"
if bashio::config.has_value 'CONFIG_LOCATION'; then
CONFIG_FILE="$(bashio::config 'CONFIG_LOCATION')"
fi
CONFIG_DIR="$(dirname "${CONFIG_FILE}")"
RUNTIME_CONFIG="/data/config.env"
CONFIG_HOME="$(bashio::config "CONFIG_LOCATION")"
CONFIG_HOME="$(dirname "$CONFIG_HOME")"
mkdir -p "${CONFIG_DIR}" /data
# Use new config file
if [ ! -f "$CONFIG_HOME/config.env" ]; then
# Copy default config.env
cp /templates/config.env "$CONFIG_HOME/"
chmod 755 "$CONFIG_HOME/config.env"
bashio::log.warning "A default config.env file was copied to $CONFIG_HOME. Please customize according to https://github.com/vogler/free-games-claimer/tree/main#configuration--options and restart the add-on"
# Recover from an old add-on bug that could create config.env as a directory.
if [ -d "${CONFIG_FILE}" ]; then
bashio::log.warning "Found a directory at ${CONFIG_FILE}; replacing it with a configuration file"
rm -rf "${CONFIG_FILE}"
fi
if [ ! -f "${CONFIG_FILE}" ]; then
install -m 0600 /templates/config.env "${CONFIG_FILE}"
bashio::log.warning \
"Created ${CONFIG_FILE}. Add account credentials there and restart the add-on if automatic login is required."
else
bashio::log.info "Using existing config.env file in $CONFIG_HOME. Please customize according to https://github.com/vogler/free-games-claimer/tree/main#configuration--options and restart the add-on"
bashio::log.info "Using configuration from ${CONFIG_FILE}"
fi
# Remove erroneous folder named config.env (bug fix for looping issue)
if [ -d "$CONFIG_HOME/config.env" ]; then
bashio::log.warning "Found directory named config.env, deleting it..."
rm -rf "$CONFIG_HOME/config.env" # Fix: Ensures directory removal even if it exists
cp /templates/config.env "$CONFIG_HOME/config.env" # Recreate as a valid file
chmod 755 "$CONFIG_HOME/config.env"
fi
# The remaster reads /fgc/data/config.env. /fgc/data is linked to Home
# Assistant's persistent /data volume by the Dockerfile.
install -m 0600 "${CONFIG_FILE}" "${RUNTIME_CONFIG}"
sed -i 's/\r$//' "${RUNTIME_CONFIG}"
# Copy new file
mkdir -p /data/data
cp "$CONFIG_HOME/config.env" /data/data/
# Permissions
chmod -R 755 "$CONFIG_HOME"
# Export variables
# Export values needed by the VNC entrypoint as well as by the Python app.
set -a
echo ""
bashio::log.info "Sourcing variables from $CONFIG_HOME/config.env"
cp "$CONFIG_HOME"/config.env /config.env
# Remove previous instance
sed -i "s|export ||g" /config.env
# Add export for non empty lines
sed -i '/\S/s/^/export /' /config.env
# Delete lines starting with #
sed -i '/export #/d' /config.env
# Get variables
# shellcheck source=/dev/null
source /config.env
rm /config.env
source "${RUNTIME_CONFIG}"
set +a
##############
# Launch App #
##############
# The Home Assistant port mapping is intentionally kept at 6080 for a seamless
# upgrade from the previous add-on, even though the new upstream defaults to 7080.
if [ -n "${NOVNC_PORT:-}" ] && [ "${NOVNC_PORT}" != "6080" ]; then
bashio::log.warning "NOVNC_PORT=${NOVNC_PORT} is not supported by the add-on port mapping; using 6080"
fi
export NOVNC_PORT="6080"
export VNC_PORT="5900"
# Go to folder
cd /data || true
# Absolute paths from the former image pointed to its Firefox profile. The
# replacement uses Chromium profiles and must start with a separate directory.
if [ "${BROWSER_DIR:-}" = "/data/data/browser" ]; then
bashio::log.warning "Remapping legacy Firefox BROWSER_DIR to the remaster Chromium profile directory"
export BROWSER_DIR="data/browser"
fi
if [ "${SCREENSHOTS_DIR:-}" = "/data/data/screenshots" ]; then
export SCREENSHOTS_DIR="/fgc/data/screenshots"
fi
# Fetch commands
CMD_ARGUMENTS="$(bashio::config "CMD_ARGUMENTS")"
IFS=';'
read -ar strarr <<< "$CMD_ARGUMENTS"
append_store() {
local store="${1}"
local current="${2}"
# Sanitizes commands
trim() {
local var="$*"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
printf '%s' "$var"
if [[ ",${current}," == *",${store},"* ]]; then
printf '%s' "${current}"
elif [ -n "${current}" ]; then
printf '%s,%s' "${current}" "${store}"
else
printf '%s' "${store}"
fi
}
# Add docker-entrypoint command
for val in "${strarr[@]}"; do
val="$(trim "$val")"
echo " "
bashio::log.info "Starting the app with arguments \"$val\""
echo " "
echo "$val" | xargs docker-entrypoint.sh || true
done
legacy_commands_to_stores() {
local commands="${1}"
local selected=""
local command=""
local normalized=""
local command_list=()
bashio::log.info "All actions concluded. Stopping in 10 seconds."
sleep 10
bashio::addon.stop
IFS=';' read -ra command_list <<< "${commands}"
for command in "${command_list[@]}"; do
normalized="${command,,}"
case "${normalized}" in
*epic-games*|*epicgames*|*" epic"*|epic*)
selected="$(append_store "epic" "${selected}")"
;;
*prime-gaming*|*primegaming*|*" prime"*|prime*|*" amazon"*|amazon*)
selected="$(append_store "prime" "${selected}")"
;;
*steam-games*|*" steam"*|steam*)
selected="$(append_store "steam" "${selected}")"
;;
*gamerpower*)
selected="$(append_store "gamerpower" "${selected}")"
;;
*" gog"*|gog*)
selected="$(append_store "gog" "${selected}")"
;;
esac
done
printf '%s' "${selected}"
}
# A non-empty STORES add-on option takes priority. Otherwise retain a STORES
# value from config.env, then fall back to translating the legacy commands.
STORES_OPTION=""
if bashio::config.has_value 'STORES'; then
STORES_OPTION="$(bashio::config 'STORES')"
fi
if [ -n "${STORES_OPTION}" ]; then
export STORES="${STORES_OPTION}"
elif [ -z "${STORES:-}" ]; then
CMD_ARGUMENTS=""
if bashio::config.has_value 'CMD_ARGUMENTS'; then
CMD_ARGUMENTS="$(bashio::config 'CMD_ARGUMENTS')"
fi
STORES="$(legacy_commands_to_stores "${CMD_ARGUMENTS}")"
export STORES="${STORES:-epic,prime,gog}"
fi
bashio::log.info "Enabled stores: ${STORES:-epic,prime,gog}"
# Import claim history from vogler/free-games-claimer once. Legacy files and
# browser data are retained under /data/data for rollback and manual recovery.
/usr/local/bin/migrate_vogler_data.py
APP_COMMAND=(python3 /fgc/main.py)
RUN_ONCE="true"
if bashio::config.has_value 'RUN_ONCE' && ! bashio::config.true 'RUN_ONCE'; then
RUN_ONCE="false"
fi
if [ "${RUN_ONCE}" = "true" ]; then
APP_COMMAND+=(--once)
bashio::log.info "Starting a single claiming run (legacy-compatible mode)"
set +e
/usr/local/bin/docker-entrypoint.sh "${APP_COMMAND[@]}"
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
bashio::log.error "Free Games Claimer exited with status ${exit_code}"
else
bashio::log.info "Claiming run completed"
fi
bashio::log.info "Stopping the add-on"
sleep 2
bashio::addon.stop
exit "${exit_code}"
fi
bashio::log.info "Starting the built-in scheduler"
exec /usr/local/bin/docker-entrypoint.sh "${APP_COMMAND[@]}"
+37 -5
View File
@@ -1,7 +1,39 @@
HEIGHT=1280
LOGIN_TIMEOUT=180
NOTIFY_TITLE='Free Games Claimer'
# List of environment variables, see : https://github.com/vogler/free-games-claimer#configuration--options
# Free Games Claimer Remaster configuration
# Complete option reference:
# https://github.com/P-Adamiec/Free-Games-Claimer-Remaster#configuration
# Browser and noVNC
SHOW=1
TIMEOUT=60
WIDTH=1280
HEIGHT=720
TIMEOUT=60
LOGIN_TIMEOUT=180
VNC_LOGIN_TIMEOUT=180
NOVNC_PORT=6080
# Keep the previous add-on's default stores. Add steam or gamerpower if wanted.
STORES=epic,prime,gog
# Used only when RUN_ONCE is disabled in the add-on options.
SCHEDULER_HOURS=12
# Common credentials can be used as fallbacks for all stores.
# EMAIL=
# PASSWORD=
# Store-specific credentials take priority over EMAIL/PASSWORD.
# EG_EMAIL=
# EG_PASSWORD=
# EG_OTPKEY=
# EG_PARENTALPIN=
# PG_EMAIL=
# PG_PASSWORD=
# PG_OTPKEY=
# GOG_EMAIL=
# GOG_PASSWORD=
# STEAM_USERNAME=
# STEAM_PASSWORD=
# Notifications (Apprise or Discord)
# NOTIFY=
# DISCORD_WEBHOOK=
@@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""Migrate legacy vogler/free-games-claimer JSON history to the remaster DB."""
from __future__ import annotations
import json
import os
import shutil
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterator
DATA_DIR = Path(os.environ.get("FGC_DATA_DIR", "/data"))
LEGACY_DIR = DATA_DIR / "data"
DATABASE = DATA_DIR / "fgc.db"
MARKER = DATA_DIR / ".vogler-remaster-migrated-v1.json"
BACKUP = DATA_DIR / "fgc.db.pre-vogler-migration"
SOURCES = {
"epic": "epic-games.json",
"prime": "prime-gaming.json",
"gog": "gog.json",
}
def log(message: str) -> None:
print(f"[legacy migration] {message}", flush=True)
def locate_source(filename: str) -> Path | None:
"""Prefer the old nested data directory, with a root fallback."""
for candidate in (LEGACY_DIR / filename, DATA_DIR / filename):
if candidate.is_file():
return candidate
return None
def normalize_timestamp(value: Any) -> str:
if isinstance(value, str) and value.strip():
raw = value.strip().replace("Z", "+00:00")
try:
parsed = datetime.fromisoformat(raw)
if parsed.tzinfo is not None:
parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
return parsed.isoformat(sep=" ", timespec="seconds")
except ValueError:
pass
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat(
sep=" ", timespec="seconds"
)
def clean(value: Any, *, default: str = "", limit: int | None = None) -> str:
result = default if value is None else str(value)
if limit is not None:
result = result[:limit]
return result
def iter_records(store: str, payload: Any, source: Path) -> Iterator[dict[str, Any]]:
if not isinstance(payload, dict):
raise ValueError(f"{source} does not contain a JSON object")
for user, games in payload.items():
if not isinstance(games, dict):
continue
for legacy_id, value in games.items():
record = value if isinstance(value, dict) else {}
title = clean(record.get("title"), default=clean(legacy_id), limit=512)
game_id = clean(legacy_id, default=title, limit=256)
if not game_id:
game_id = title[:256] or "unknown"
legacy_store = record.get("store")
extra = {
"migration_source": str(source),
"legacy_store": legacy_store,
"legacy_time": record.get("time") or record.get("timestamp"),
}
extra = {key: val for key, val in extra.items() if val not in (None, "")}
yield {
"store": store,
"user": clean(user, default="unknown", limit=128) or "unknown",
"game_id": game_id,
"title": title or game_id,
"url": clean(record.get("url"), limit=None) or None,
"status": clean(record.get("status"), default="unknown", limit=64)
or "unknown",
"code": clean(record.get("code"), limit=128) or None,
"extra": json.dumps(extra, ensure_ascii=False) if extra else None,
"created_at": normalize_timestamp(
record.get("time") or record.get("timestamp")
),
}
def ensure_schema(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS claimed_games (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
store VARCHAR(32) NOT NULL,
user VARCHAR(128) NOT NULL,
game_id VARCHAR(256) NOT NULL,
title VARCHAR(512) NOT NULL,
url TEXT,
status VARCHAR(64) NOT NULL DEFAULT 'unknown',
code VARCHAR(128),
extra TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS ix_claimed_games_store ON claimed_games (store);
CREATE INDEX IF NOT EXISTS ix_claimed_games_user ON claimed_games (user);
CREATE INDEX IF NOT EXISTS ix_claimed_games_game_id ON claimed_games (game_id);
"""
)
def migrate() -> int:
DATA_DIR.mkdir(parents=True, exist_ok=True)
if MARKER.exists():
log("Legacy claim history was already migrated")
return 0
sources = {
store: source
for store, filename in SOURCES.items()
if (source := locate_source(filename)) is not None
}
legacy_browser = LEGACY_DIR / "browser"
if not sources:
if legacy_browser.exists():
log(
"Legacy Firefox browser data was found but cannot be converted to "
"the remaster Chromium profile; a one-time login may be required"
)
MARKER.write_text(
json.dumps(
{
"migrated_at": datetime.now(timezone.utc).isoformat(),
"imported": 0,
"sources": [],
},
indent=2,
),
encoding="utf-8",
)
log("No legacy claim-history files were found")
return 0
if DATABASE.exists() and not BACKUP.exists():
shutil.copy2(DATABASE, BACKUP)
log(f"Backed up the existing database to {BACKUP}")
imported = 0
skipped = 0
errors: list[str] = []
try:
with sqlite3.connect(DATABASE) as connection:
ensure_schema(connection)
for store, source in sources.items():
try:
payload = json.loads(source.read_text(encoding="utf-8"))
source_imported = 0
for record in iter_records(store, payload, source):
exists = connection.execute(
"""
SELECT 1 FROM claimed_games
WHERE store = ? AND user = ? AND game_id = ?
LIMIT 1
""",
(record["store"], record["user"], record["game_id"]),
).fetchone()
if exists:
skipped += 1
continue
connection.execute(
"""
INSERT INTO claimed_games (
store, user, game_id, title, url, status, code,
extra, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
record["store"],
record["user"],
record["game_id"],
record["title"],
record["url"],
record["status"],
record["code"],
record["extra"],
record["created_at"],
record["created_at"],
),
)
imported += 1
source_imported += 1
connection.commit()
log(f"Imported {source_imported} record(s) from {source}")
except (OSError, ValueError, json.JSONDecodeError, sqlite3.Error) as err:
connection.rollback()
message = f"Failed to import {source}: {err}"
errors.append(message)
log(message)
except sqlite3.Error as err:
log(f"Database migration failed: {err}")
return 1
if legacy_browser.exists():
log(
"Legacy Firefox browser data remains in /data/data/browser. It is "
"not compatible with Chromium, so use noVNC for a one-time login if needed."
)
if errors:
log("Migration was incomplete and will be retried on the next start")
return 0
MARKER.write_text(
json.dumps(
{
"migrated_at": datetime.now(timezone.utc).isoformat(),
"imported": imported,
"skipped_existing": skipped,
"sources": [str(path) for path in sources.values()],
},
indent=2,
),
encoding="utf-8",
)
log(f"Migration complete: {imported} imported, {skipped} already present")
return 0
if __name__ == "__main__":
sys.exit(migrate())
+5 -4
View File
@@ -1,10 +1,11 @@
{
"dockerhub_by_date": true,
"dockerhub_by_date": false,
"dockerhub_list_size": 2,
"last_update": "17-05-2025",
"last_update": "17-07-2026",
"paused": true,
"repository": "alexbelgium/hassio-addons",
"slug": "free_games_claimer",
"source": "github",
"upstream_repo": "vogler/free-games-claimer",
"upstream_version": "1.8"
"upstream_repo": "P-Adamiec/Free-Games-Claimer-Remaster",
"upstream_version": "1.1"
}
+2
View File
@@ -1,3 +1,5 @@
## 1.27 (15-07-2026)
- Fix the Gitea add-on HEALTHCHECK to work correctly when SSL is enabled, so Home Assistant can accurately report the add-on's health status regardless of whether the instance uses HTTP or HTTPS.
## 1.26.4 (2026-06-23)
- Update to latest version from go-gitea/gitea (changelog : https://github.com/go-gitea/gitea/releases)
+1 -1
View File
@@ -133,4 +133,4 @@ HEALTHCHECK \
--retries=5 \
--start-period=30s \
--timeout=25s \
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
CMD curl -A "HealthCheck: Docker/1.0" -s -f -k --http1.1 "$(cat /run/health_protocol 2>/dev/null || echo http)://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
+1 -1
View File
@@ -97,5 +97,5 @@ schema:
slug: gitea
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/gitea
version: "1.26.4"
version: "1.27"
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]"
+1
View File
@@ -55,6 +55,7 @@ for file in /config/app.ini /etc/templates/app.ini; do
PROTOCOL=http
sed -i "/server/a PROTOCOL=http" "$file"
fi
echo -n "${PROTOCOL}" > /run/health_protocol
##################
# ADAPT ROOT_URL #
+1 -1
View File
@@ -1,4 +1,4 @@
## &#9888; Open Issue : [🐛 [qBittorrent] Cant update to 5.2.3 (opened 2026-07-09)](https://github.com/alexbelgium/hassio-addons/issues/2836) by [@tschoehuijs](https://github.com/tschoehuijs)
## &#9888; Open Request : [✨ [REQUEST] qbittorrent (opened 2025-10-11)](https://github.com/alexbelgium/hassio-addons/issues/2139) by [@nyok92](https://github.com/nyok92)
# Hass.io Add-ons: Tor with bridges
[![Donate][donation-badge]](https://www.buymeacoffee.com/alexbelgium)