* birdnet-go-dev: add merge-prs.sh --check to catch build-breaking PR conflicts
The add-on build merges every open non-draft fork PR onto an upstream-synced
main. GitHub's `mergeable` field answers a different question: it compares a PR
against its *own* base ref, which for the stacked dashboard PRs is another
feature branch - sometimes one belonging to an already-closed PR. So a PR can
read MERGEABLE/CLEAN and still fail the build.
That is how run 34101694542 broke: PR #57 is MERGEABLE against the frozen branch
of closed PR #56, but conflicts with main on DetectionCard.svelte.
--check replays the exact same merge sequence, skips past conflicts instead of
stopping at the first one, and reports every offender as
!!! CONFLICT pr=#N conflicts-with=<main|accumulated> files=... title=...
conflicts-with is probed in a throwaway worktree against the pristine synced
main, so it separates a PR that is merely stale (fixable in its own branch) from
one that only clashes with another open PR (needs a cross-PR decision).
Build behaviour is unchanged; --check is a no-op unless asked for, so no version
bump.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* birdnet-go-dev: fix --check misclassification, arg handling and temp leak
Review findings on #3053, all reproduced against git fixtures before fixing.
1. The probe did not apply the build's own merge policy (Codex).
The real merge treats a *sole* frontend/package-lock.json conflict as
non-fatal, but merges_onto_main() did a raw merge. A PR whose only clash
with main is the generated lockfile was therefore reported as
conflicts-with=main, which prints "the PR is stale against main; merge main
into its branch" when in truth it merges onto main fine and only clashes
with another open PR — the exact opposite remediation, from the feature
whose entire job is to say which one it is.
Extracted the policy into resolve_sole_lockfile() and routed both the real
merge and the probe through it, so the two cannot drift apart again.
2. A second positional operand silently won (CodeRabbit).
`merge-prs.sh a b` ran against b, where the pre-flag script used "${1}".
A stray argument would have cloned into the wrong directory. Now exits 64.
3. mktemp parent directory leaked (CodeRabbit, Copilot).
probe="$(mktemp -d)/probe" and only the child was removed, leaking one
empty dir per checked conflict. Measured 3 leaked dirs over 3 calls; now 0.
Verified with throwaway repos: a lockfile-only clash now classifies as
"accumulated" (was "main"), a real source conflict with main still classifies
as "main", a clean PR still classifies as "accumulated", 0 leaked temp dirs,
0 stray worktrees, and the argument matrix behaves. shellcheck clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
* skill(hassio-addon-workflow): stop pr_review.sh watch reporting a false pass
`gh pr checks` emits TAB-separated columns, but `watch` parsed it with awk's
default field splitting. Every check name containing a space was truncated to
its first word and the state column was never read:
Codacy Static Code Analysis<TAB>fail -> Codacy=Static
Addon linting (wger)<TAB>pass -> Addon=linting
Test addon build (wger)<TAB>pending -> Test=addon
All three blocking gates have multi-word names, so the `case` matched neither
*fail* nor *pending* and fell through to "settled - all passing". That is a
false pass from the one command whose job is to report CI truthfully: #3044
was called green with Codacy red, and #3042 was called green while the HA
add-on linter was failing. A build that had not started would also have read
as a pass.
- parse with `awk -F'\t'`
- judge the state column alone, never the joined name=state text, so a check
named e.g. `flaky-fail-detector` cannot read as a failure
- allowlist the good states (pass/skipping/pending) and treat anything
unrecognised as a failure, so a new state cannot reach the passing branch
- name the checks that failed instead of only saying FAILURES
- keep waiting when only advisory checks have reported
Codacy is red on essentially every add-on PR here (#3019, #3044 and #3050 all
merged with it failing; master has no branch protection), so it is excluded
from the verdict but printed every poll and called out explicitly on settle.
Agreed with the maintainer. It is a denylist of known noise rather than an
allowlist of gates, so a job added to CI later counts as blocking by default.
Verified against real PRs: #3042 (blocking linter failure) now exits 1 and
names the check where it previously exited 0; #3018/#3019/#3044/#3050 report
correctly; pending, advisory-only, unknown-state and empty-output cases
checked against a stubbed gh. shellcheck clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* skill: record PR number in the traps entry
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* skill: address review — skipped-checks wording, document TSV contract
CodeRabbit (correct): the header claimed exit 0 means "every blocking check
passed", but a skipped gate also yields 0. A PR touching no add-on skips all
three gates, so that wording overstated what a 0 means. Reworded; the runtime
warning about skipped jobs was already there.
Copilot recommended switching to `gh pr checks --json`. Not applied: that flag
does not exist before gh 2.36 and 2.23 ships in this add-on, where it fails
with `unknown flag: --json`. Its premises are also wrong for the path the
script takes — piped output carries no header and uses real tabs; the aligned
ANSI table is the TTY renderer, which $(... | awk) never gets. Documented the
non-TTY contract and the gh-version constraint in the comment and traps.md so
this is not "corrected" back into a break later.
The underlying worry — a format change reintroducing a false pass — is already
answered by the allowlist design, now verified explicitly: a header row lands
in the failure branch (exit 1) and a space-aligned table parses to zero rows,
so watch keeps waiting. Neither can return 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The add-on could not be stopped: Home Assistant showed an Error status
after a few seconds and the container kept running and still served the
web UI (#3049).
cont-init.d/99-run.sh started nginx in the foreground -- '&>' is a
redirect, not a background operator -- and ha_entrypoint.sh runs every
cont-init.d script sequentially in the foreground. That script therefore
never returned, so the entrypoint never reached the code that installs
the terminate() handler forwarding SIGTERM to the application. The
reporter's log shows both halves of this: it prints 'Starting custom
scripts' and never reaches 'Everything started!'.
The add-on also shipped no 'init:' key, so Supervisor's default of true
made Docker inject its own init as PID 1 and left ha_entrypoint.sh as
PID 2, where the 'if $PID1' block holding the trap is skipped outright.
Background the launch and set init: false. The script then returns, the
entrypoint installs its trap, and nginx -- orphaned by the exiting
script -- is reparented to the entrypoint as PID 1, where terminate()'s
'pgrep -P $$' finds it and signals it directly.
Backgrounding from cont-init.d is what 24 other add-ons here already do
(autobrr runs a bare 'nginx &'). Moving the launch to services.d was
considered and rejected: ha_entrypoint.sh runs each services.d/*/run
inside a restart subshell, so the application ends up a grandchild of
PID 1 while terminate() enumerates direct children only. Reproduced --
the app survives that path unsignalled -- and it is the larger change.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(wger): set the database env vars the upstream image stopped shipping
wger/server:latest no longer defines DJANGO_DB_ENGINE or DJANGO_DB_DATABASE in
its image environment, and upstream settings/main.py reads both with no
fallback. Every fresh install therefore died at startup with
"ImproperlyConfigured: Set the DJANGO_DB_ENGINE environment variable".
Set both explicitly in the Dockerfile, pointing at the sqlite database in
/data/database.sqlite that the add-on already persists, and add
DJANGO_PERFORM_MIGRATIONS=True so an existing database picks up new migrations
when the image is rebuilt against a newer upstream release.
With the path now set through the environment, the cont-init rewrite of the
database path in the Python settings is dead code — upstream no longer
hardcodes /home/wger/db/database.sqlite anywhere, so it only logged a warning.
Also move the add-on to the addon_configs location, as the issue asks: the
shared 01-config_yaml.sh template migrates an existing
/homeassistant/addons_config/wger/config.yaml on the first start.
Fixes#3043
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(wger): use the ISO date format the rest of this CHANGELOG uses
The 2.6.4 heading was written 04-09-2026 while every other dated heading
in this file, and 22937 of the 23999 dated headings in the repo, use ISO
YYYY-MM-DD. Copilot flagged the inconsistency on #3044.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(linkwarden): stop installing packages Debian 11 no longer serves
The 2.16.2 updater build failed in the first RUN layer:
E: Failed to fetch .../sudo_1.9.5p2-3%2bdeb11u4_amd64.deb 404 Not Found
E: Failed to fetch .../vim-runtime_8.2.2434-3%2bdeb11u3_all.deb 404 Not Found
Debian 11 reached LTS end on 2026-08-31. Its bullseye-security index is frozen
at that date and still lists debs that deb.debian.org no longer serves; sudo is
one of them and still 404s on every deb.debian.org edge checked today, so the
build fails deterministically rather than transiently. Every package in
postgresql-16's own dependency chain that comes from bullseye-security was
checked and does fetch, so removing this first install unblocks the build.
None of the four packages is needed:
- vim was never used by the add-on.
- gnupg2 was only there for "gpg --dearmor"; apt reads the ASCII-armoured key
from /etc/apt/trusted.gpg.d/postgresql.asc directly.
- lsb-release was only there for "lsb_release -cs"; /etc/os-release carries
VERSION_CODENAME.
- sudo is replaced by su in the Postgres bootstrap, which is what the ente and
postgres_15 add-ons already use for the same job.
curl is already present in the upstream linkwarden image, so no install step is
needed before the PGDG repository is configured.
The su rewrite keeps the argv psql receives identical. Because "su -" starts a
login shell, the service call now uses an absolute path (the login PATH has no
/usr/sbin) and the bootstrap SQL is written to and read from /tmp rather than
the script's working directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(linkwarden): fetch bullseye-security from its origin, not the CDN
Dropping vim/gnupg2/lsb-release/sudo got the build past the first RUN, but
"apt-get install -y postgresql-16" then 404'd on its own dependencies, on arm64:
E: Failed to fetch .../glibc/libc-l10n_2.31-13%2bdeb11u14_all.deb 404
E: Failed to fetch .../exim4/exim4-base_4.94.2-7%2bdeb11u6_arm64.deb 404
E: Failed to fetch .../python3.9/libpython3.9-minimal_3.9.2-1%2bdeb11u7_arm64.deb 404
All three are 200 on security.debian.org, the origin that deb.debian.org is a
CDN alias for. The rot is per-file and moves: exim4-base was 404 during the
build and 200 minutes later, so retrying is a coin flip rather than a fix.
Rewrite the security suite in /etc/apt/sources.list to security.debian.org
before "apt-get update". The main suite is left on the CDN; it is intact, and
bullseye main is already on archive.debian.org whereas bullseye-security is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(linkwarden): feed the bootstrap SQL on stdin, and fix two comments
Review follow-up on the temp file, the Dockerfile comment and the CHANGELOG
wording.
The bootstrap SQL no longer goes through a file at all. Both reviewers objected
to the predictable root-written /tmp path; passing the statements to psql on
stdin removes the file rather than defending it, and is less code than either
the version being reviewed or the suggested mktemp. It also restores what the
original did before this branch: sudo ran "cat file | psql", so psql read the
statements from stdin then too.
The Dockerfile comment said "PGDATA repository" where it meant the PGDG apt
repository; PGDATA is the data-directory env var set two lines above, so the
wording was actively misleading.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: record the shipped upstream release in updater.json
Each PR publishes an upstream version the updater bot had already selected
before CI reverted its commit, but updater.json still recorded the previous one.
The updater reads upstream_version as CURRENT and enters its update path
whenever it differs from the latest tag, so its next run would process the same
release again and derive a synthetic trailing-.1 version, producing a redundant
release, a duplicate CHANGELOG entry and a wasted build.
These values are exactly what the bot itself wrote in the reverted commit; this
restores its own record for a release now being shipped rather than choosing a
new one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(linkwarden): keep the Postgres password out of process arguments
CodeRabbit flagged that the password appears in the command string of the
password-setting call, and that the database-creation call reaches Postgres over
a TCP URI carrying the same password with sslmode=prefer. Both predate this
branch, but both lines are touched here.
Sending each statement to psql on stdin removes the password and the URI from
argv, and is shorter than either form it replaces: the escaped-quote nesting on
the ALTER USER call disappears with it.
The connection method is unchanged for the ALTER USER call, which already went
over the local socket as the postgres user. The database-creation call moves
from TCP to that same socket. This is safe by construction rather than by
assumption: the ALTER USER call runs first under "set -e" with no "|| true", so
the container cannot reach the second call unless socket access already worked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(mealie): build the frontend with pnpm and the upstream lockfile
Upstream mealie migrated frontend/ from yarn to pnpm in v3.24.0 and deleted
frontend/yarn.lock. Our builder stage kept running "yarn install
--frozen-lockfile", which silently degraded to a fresh, unpinned resolution of
every dependency. That worked until a newer vuetify 4.x release dropped the
"vuetify/labs/rules" entry point, at which point "nuxt generate" failed with:
Rolldown failed to resolve import "vuetify/labs/rules" from
"virtual:nuxt:.nuxt%2Fvuetify-nuxt-plugin.client.mjs"
and the v3.25.1 updater build was reverted.
Mirror upstream's docker/Dockerfile frontend stage instead: node:24, a global
pnpm@11, and "pnpm install --frozen-lockfile" against the committed
pnpm-lock.yaml, so the dependency set is the one upstream tests. Also copy the
frontend tree with "cp -a frontend/." so dotfiles such as .nuxtignore come
across, and shallow-clone the tag.
Bumps the add-on to v3.25.1, the version the updater bot could not build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: record the shipped upstream release in updater.json
Each PR publishes an upstream version the updater bot had already selected
before CI reverted its commit, but updater.json still recorded the previous one.
The updater reads upstream_version as CURRENT and enters its update path
whenever it differs from the latest tag, so its next run would process the same
release again and derive a synthetic trailing-.1 version, producing a redundant
release, a duplicate CHANGELOG entry and a wasted build.
These values are exactly what the bot itself wrote in the reverted commit; this
restores its own record for a release now being shipped rather than choosing a
new one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(calibre-web): make kepubify executable so Kobo sync can be enabled
The LinuxServer base image installs kepubify with `curl -o /usr/bin/kepubify`
and never marks it executable, so the file ships as mode 0644. Calibre-web's
resolve_binary_path() only accepts a binary that passes os.access(X_OK), so
enabling Kobo sync failed with "Kepubify binary not found" even when the path
was set to /usr/bin by hand. Verified against the published layer of
lscr.io/linuxserver/calibre-web:arm64v8-latest, whose tar header for
usr/bin/kepubify reads `-rw-r--r-- 0/0 3670016`.
Set mode 0755 on it at build time, unguarded: if a future base image stops
shipping the binary, the build should fail rather than ship a broken add-on.
Calibre-web separately only autodetects kepubify under /opt/kepubify, never
/usr/bin, so the setting was stored empty on the first start and never
retried. Fill it in with /usr/bin from the cont-init script that already
applies conditional settings to app.db, and only while it is still empty, so
a path the user set by hand is never overwritten.
Fixes https://github.com/alexbelgium/hassio-addons/issues/3040
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(calibre-web): publish kepubify under a name calibre-web accepts
The first attempt was incomplete. It read binary_helper.py at calibre-web
master, which accepts a bare "kepubify"; the shipped 0.6.27 tag does not:
SUPPORTED_KEPUBIFY_BINARIES = ("kepubify-linux-64bit", "kepubify-linux-32bit")
So there are two independent defects, and the chmod only fixed one. The base
image installs the converter with `curl -o /usr/bin/kepubify`, which leaves it
mode 0644 *and* names it something calibre-web will not look for.
A symlink alone does not fix it either: os.access() follows the symlink and
tests the target, and the target has no execute bit for anyone, root included.
Verified against the exact 0.6.27 resolution logic, on a scratch tree:
symlink only -> '' (still broken)
chmod only -> '' (still broken)
chmod 0755 + symlink -> '/opt/kepubify'
Put the symlink in /opt/kepubify, which is where calibre-web's own
autodetect_kepubify_binary() already looks, rather than in /usr/bin where only
our own database write would find it. init_config() re-runs that detection on
every start while the column is NULL, so calibre-web now configures the path
itself and a fresh install needs no second restart.
That in turn shrinks the cont-init statement: instead of hardcoding a path it
resets an empty value to NULL, which un-sticks calibre-web's own detection for
installs that already persisted "". A path set by hand is not empty and is
left alone.
Mode measured on the published add-on image, all 28 layers scanned:
ghcr.io/alexbelgium/calibre_web-aarch64:0.6.27.3 carries usr/bin/kepubify at
mode 0o644 in the base layer and nowhere else.
Reported by @andMaximus in https://github.com/alexbelgium/hassio-addons/issues/3040
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(calibre-web): keep /usr/bin resolving for the manual workaround
CodeRabbit spotted that an install already storing "/usr/bin" stays broken:
that value is not empty, so the cont-init statement leaves it alone, and with
the symlink only in /opt/kepubify it no longer resolves under 0.6.27.
The case is real and narrow. A failed save never persists the value --
_configuration_result() calls config.load() on the error path, discarding it --
so the only way to hold "/usr/bin" is a save that succeeded, which requires
having first applied the workaround published in the issue thread:
ln -sf /usr/bin/kepubify /usr/bin/kepubify-linux-64bit
Anyone who did that, and anyone copying that comment, would have been broken
again by this PR.
Fixed with one more symlink rather than CodeRabbit's suggested migration of
"/usr/bin" back to NULL, because that would overwrite a path the user set by
hand. Making their setting keep working is better than resetting it. Verified
against the 0.6.27 resolution logic:
symlink in /opt only stored '/usr/bin' -> ''
plus /usr/bin symlink stored '/usr/bin' -> '/usr/bin/kepubify-linux-64bit'
autodetect -> '/opt/kepubify'
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(baikal): update to Baikal 0.12.1 and track sabre-io releases
ckulka/baikal-docker, and the archived fork the addon was based on, stopped
publishing images at Baikal 0.10.1, so the addon could not follow upstream and
its updater tracked a repository that no longer moves.
The base image is now used for its runtime only (nginx, php-fpm, msmtp) and the
application comes from the release published by sabre-io, which the updater can
follow. The Home Assistant timezone fix the fork carried as a whole patched
Plugin.php is applied as the single hunk it actually is, and the build fails if
sabre/dav ever moves that code.
The addon data folder holds the application as well as the user data, and it was
seeded with no-clobber, so a rebuilt image never replaced the code being served.
Application folders are now refreshed on every start ; Specific and config are
still only seeded when missing.
Closes#3038
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(baikal): satisfy markdownlint on the new changelog and readme lines
Bare URLs (MD034) and a heading with no blank line before its list (MD022 /
MD032). The blank line matches the older hand-written entries in the same file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The previous build shipped the SoX-default render (its own axes, legend
and palette). PR #61 now matches the detection spectrogram style, so
rebuild to re-merge it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
merge-prs.sh re-clones the fork and merges every open non-draft PR on
each build, so bumping the version is what pulls in alexbelgium/birdnet-go#61
along with the Dockerfile changes made since the last build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream commit 9d166ef2 raised go.mod to 'go 1.27.0' and moved its own
Dockerfile to golang:1.27-trixie. The addon keeps a separate copy of that
Dockerfile, which stayed on golang:1.26-trixie, so the amd64 build failed
with 'go.mod requires go >= 1.27.0 (running go 1.26.7; GOTOOLCHAIN=local)'.
Re-bumps to 20260901 to retrigger the builder after the failed run was
auto-reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(filebrowser_quantum): keep the no-preview Download and Open file links inside the ingress panel
1.5.3.2 fixed the download anchor api/resources.js builds and clicks itself,
but the 'no preview available' screen -- what a .zip or .bin gets -- offers its
own Download and 'Open file' buttons as target="_blank" links, and so does the
share list in settings. The companion app has no navigationAction policy
delegate, so every new-window request reaches createWebViewWith and is handed
to an external browser, which carries no ingress session cookie: 401.
Replaces the wrapper around HTMLAnchorElement.prototype.click with a single
capturing click listener. It reaches the hidden anchor exactly as before -- a
programmatic .click() dispatches through the document like a real one -- and
also the two the user clicks, which the wrapper never saw.
Downloads gain the attribute everywhere; dropping target="_blank" is limited
to the companion app, identified by the Mobile/HomeAssistant marker it appends
to the user agent, because in a real browser a new tab is the better
behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(filebrowser_quantum): leave modified clicks alone, and correct the comments
Review of #3033 found that the listener ran for every click, so a
cmd/ctrl/shift-click on the visible Download link -- an explicit request for a
separate context -- was turned into a download instead. It now only touches
unmodified primary clicks, which is also what a programmatic .click() reports
(button 0, no modifiers), so the hidden anchor is unaffected.
Also corrects three overclaims: the listener reaches connected anchors only
(both shipped download paths append theirs first); absolute sidebar links go
through window.open rather than an anchor and are not covered; and links inside
the pdf, srcdoc-preview and OnlyOffice iframes are a separate document this
listener never sees. Records that an error response is now saved as a file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(filebrowser_quantum): say why the download branch also drops target
Review of #3033 read the unconditional target removal as contradicting the
comment above it. The removal is deliberate: with the download attribute set, a
same-origin link downloads and never opens a tab, so it changes nothing in a
browser (measured), but it stops the companion app from taking its new-window
path before it considers the download -- an ordering not testable from outside
iOS. The comment now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The AI pipeline has been down since ~2026-08-25. Every Claude-backed step fails
with:
"result": "Failed to authenticate. API Error: 401 OAuth access token has been
revoked."
"error": "authentication_failed", "api_error_status": 401
The CLAUDE_CODE_OAUTH_TOKEN secret (last updated 2026-07-24) has been revoked.
That is not fixable in code — it needs regenerating — but the six days it went
unnoticed are, because nothing on the way out said so.
What a maintainer actually saw was the action reporting:
"--json-schema was provided but Claude did not return structured_output.
Result subtype: success"
which points at the schema, and then Apply verdict's generic "usually a
workflow-level fault ... left untouched for a retry". Neither mentions
credentials, and the failure presents per-issue while the real scope is every
tier at once: tier 1 cannot label, so tier 2's batch is empty and the sweep
reports success daily having done nothing.
GATE 1 now checks the execution file for authentication_failed / HTTP 401
before the max-turns branch and says what is wrong and what to do — regenerate
with `claude setup-token`, update the secret in the CR_PAT environment, and set
AI_DISABLED=true to silence the runs meanwhile. Same array guard and
fail-closed posture as hit_max_turns: an unrecognised shape is simply not an
auth failure and falls through to the generic branch.
Behaviour is otherwise unchanged — this branch already exited 1 without
touching labels, which was correct for a systemic fault.
Verified against the exact execution-file shape captured from the live 08-30
failure (both the issues and catch-up paths report the new error), and
regression-checked that max_turns still escalates on the automated retry, that
generic failures keep the generic message with and without an execution file,
and that the verdict paths are untouched.
Co-authored-by: claude-ai-fix[bot] <claude-ai-fix[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(calibre-web): document the optional features and drop the dead calibre install
Issue #1143 asks how to add Calibre-Web's optional extras (metadata, kobo,
...) to the add-on. They are already there: the LinuxServer base image pip
installs optional-requirements.txt alongside requirements.txt into its
/lsiopy virtualenv, so every extra ships enabled. Listing the .dist-info
directories of the published image confirms scholarly, rarfile, py7zr,
mutagen, jsonschema, python-ldap, flask-dance, PyDrive2, comicapi and the
rest are present. Running pip install calibreweb[...] in the container just
fetches an unused second copy from PyPI and is thrown away when the
Supervisor recreates the container.
What actually hides the reporter's cover fields is Calibre-Web's own
gating: book_edit.html only renders 'Fetch Cover from URL' and 'Upload
Cover from Local Disk' when current_user.role_upload() and g.allow_upload
are both true, i.e. Enable Uploads plus the user's Upload permission. The
README now says so.
While checking where the Calibre binaries come from, the Dockerfile step
that claimed to install them turned out to be dead. The image has no wget,
only curl, so 'wget ... | sh /dev/stdin install_dir=/opt/calibre' loses
wget to exit 127, hands sh an empty script, and the pipeline still exits 0.
In the published amd64 image that RUN's layer decompresses to an empty tar
and no layer contains anything under /opt/calibre. Repairing it with curl
would not help: calibre's installer then hard-exits on the missing libEGL,
libOpenGL and libxcb-cursor that the universal-calibre mod apt-installs
itself, so the build would start failing and the image would grow by about
a gigabyte for binaries the mod already provides at start. The step is
removed and a comment records where the binaries really come from.
Refs https://github.com/alexbelgium/hassio-addons/issues/1143
* docs(calibre-web): tighten the optional-features wording after review
Say pip install calibreweb[...] is unsupported and can disturb the pinned
dependencies rather than calling the result unused, scope the extra-package
advice to compatible packages, list the calibre binaries as examples rather
than as a set all three operations need, and mark the docker mod as the
default rather than a certainty since DOCKER_MODS can be overridden.
The `Lint workflows` autofix job has failed on every scheduled run since at
least 2026-08-16, with `shfmt` reporting parse errors ("LitWord cannot be
followed by a word", "${ stmts;} is a mksh feature", ...) in ~70 shell scripts
that parse cleanly on a pristine checkout.
Root cause is the preceding "Fix non-printable Unicode spaces" step. Its regex
was written as `$'[\\u00A0\\u2002...]'`: the doubled backslash makes bash's
ANSI-C quoting emit the literal text ` `, and Perl has no `\u` codepoint
escape — `\u` is the titlecase-next-character operator, so the character class
degrades to the plain characters `0 2 3 5 7 8 9 A B F`. The step therefore
replaced those digits and letters with spaces in every text file in the repo,
which is what left the shell scripts unparseable. `shfmt` then exited 1 and the
job stopped before opening its autofix PR — the only reason the corruption was
never committed.
Switch to Perl's own `\x{...}` escape in a plain single-quoted string, so the
class holds the ten intended code points and nothing else.
Verified locally against the exact step body extracted from the workflow: on a
sample of the repo it now rewrites only the real U+202F occurrences (e.g.
`postgres_15/.../99-run.sh`, `birdnet-pi/DOCS.md`) and leaves all other text
untouched, and `shfmt v3.12.0 -w -i 4 -ci -bn -sr` over the whole repo exits 0
with no parse errors.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(filebrowser_quantum): make Download save the file in the iOS companion app
FileBrowser downloads by clicking an <a> that carries no download attribute
and letting the attachment response do the rest. The Home Assistant iOS
companion app is a WKWebView, where a download only happens when WebKit turns
a navigation action into a WKDownload -- which is what the download attribute
does, and the app hands the result to its own download manager
(WebViewController+WebKitDelegates.swift, navigationAction:didBecome
download:). Its response policy delegate returns .allow for every sub-frame
and never returns .download, so inside the ingress panel a plain attachment
navigation is simply rendered: a text file opens and shows its content with no
way to save it.
The ingress filter now adds the attribute, matched on the two exact download
endpoints so nothing else in the app is touched. Desktop browsers already
downloaded these and are unaffected, and an empty value keeps the filename the
server sends in Content-Disposition.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(filebrowser_quantum): keep Open file a navigation, and narrow the claims
Review of #3030 found that the 'no preview available' fallback renders an
'Open file' link on the same download endpoint with inline=true
(views/files/Preview.vue), so a pathname-only match would have turned opening
a file into downloading it. Exclude inline=true.
Also narrows two overstated claims: the app's download manager is gated on
iOS 17, and the public-share sidebar downloads with window.open() rather than
an anchor, so it is not covered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The tool views (Tools -> File Size Analyzer, Duplicate Finder, the file list
panel) always pass showLimitedOptions to the context menu, and the context
menu's openParentFolder() hands that same flag to goToItem() as its newTab
argument. The parent folder is therefore opened with
window.open(<absolute url>, '_blank'). Behind Home Assistant ingress that
popup lands on the raw /api/hassio_ingress/<token>/ URL with no Home
Assistant frontend around it to keep the ingress session alive, so the new
tab answers 401 instead of showing the folder.
The ingress vhost now injects the same window.open shim the komga add-on
uses, scoped to the two SPA route prefixes goToItem() builds ('files/' and
'public/share/'), so download and preview popups keep their own tab.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(zoraxy): restore apk removed by the upstream image build
Upstream's image build has ended with "rm -rf /sbin/apk" since v3.3.4. The
binary is deleted but /etc/apk (repositories, keys, world) and /lib/apk/db
survive, so package management is recoverable. The image also ships neither
bash nor curl, so ha_automodules.sh failed with "apt-get: not found / apk:
not found" (exit 127) and both architectures failed to build.
Restore the statically-linked apk binary from an Alpine build stage before the
shared module and package scripts run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(zoraxy): keep BUILD_FROM a global build arg
Declaring the tools stage above the ARG lines scoped BUILD_FROM to that stage,
so the final FROM resolved to an empty base name. Move both global ARGs above
the first FROM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(skill): record the global-ARG and vanishing-package-manager traps
Both cost a CI cycle on PR #3024: a tools stage inserted above ARG BUILD_FROM
demoted it to stage scope, and the upstream image had started deleting
/sbin/apk between releases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Reported from a remote session: ingress answered
`403 External internet access denied - https://sabnzbd.org/access-denied`.
Root cause is `check_access()` in `sabnzbd/interface.py`:
# Never check the XFF header unless access would have been granted
# based on the remote IP alone!
if is_allowed and cfg.verify_xff_header() and (xff_ips := ...):
is_allowed = all(is_local_addr(ip) or is_loopback_addr(ip)
for ip in xff_ips)
nginx's own address is loopback, so the first test passes, and then every
address in X-Forwarded-For has to be local too. Supervisor puts the browser's
address in that header, so anyone reaching Home Assistant from outside the LAN
is refused. `verify_xff_header` defaults to on (`cfg.py:531`), so this is not a
configuration a user opted into.
Reproduced against the running add-on, `GET /config/general/`:
no X-Forwarded-For 200
X-Forwarded-For: 81.164.12.7 403 External internet access denied
X-Forwarded-For: 81.164.12.7, 172.30.32.2 403
X-Forwarded-For: 192.168.1.44 200
which is why it worked on the LAN and not from outside. Verified the fix the
same way, running the shipped nginx.conf and ingress.conf in front of the live
add-on with both variants side by side: the current config 403s on a public
address, the fixed one answers 200 for all three chains, and redirects, static
roots and the API are unaffected. That instance has an empty `url_base`, so the
pass-through routing is now confirmed for both `url_base` values.
The header was forwarded because SABnzbd reads it — the wrong test, since what
it does with it is reject. Clearing it leaves SABnzbd looking at nginx's
loopback address, which is what it saw before the header was added; ingress is
gated by Home Assistant authentication before reaching this proxy either way.
The evidence.md entry records the methodology error, per the skill's own
feed-the-skill rule.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
SKILL.md's step 7 said to match `## X.Y (DD-MM-YYYY)`. The repo does not use
that: 7705 dated CHANGELOG headings are ISO `YYYY-MM-DD` against 363 in
`DD-MM-YYYY`, and the newest entry is ISO in 125 of 135 add-ons. Following the
instruction cost a Copilot review round on #3019.
`DD-MM-YYYY` is not invented, which is presumably how it got written down. It
is what `onpush_builder.yaml` inserts with `date '+%d-%m-%Y'` when a push
arrives with no heading for the config.yaml version, and it is the addons_updater
bot's default in `99-run.sh` — but that bot runs here with `date_iso8601: true`
(confirmed against the running add-on's options), which is why almost everything
on master is ISO. Neither is a reason to write `DD-MM-YYYY` by hand.
The traps.md entry also records that the builder's duplicate check is
`grep -q "^## ${version} ("` — keyed on the exact config.yaml version and blind
to the date — so an ISO heading you wrote yourself still suppresses the bot's
insertion.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(sabnzbd): enable Home Assistant ingress
The add-on already carried a complete but disabled nginx ingress scaffold:
`etc/nginx/` with its includes and a `servers/ingress.conf`, a
`cont-init.d/32-nginx_ingress.sh` short-circuited by `exit 0`, and
`ENV PACKAGES="nginx"` in a Dockerfile byte-identical to nzbget's. Only
`ingress: true` and the s6 service that starts nginx were missing.
What SABnzbd 5.1.1 actually needs from the proxy, measured against the
running add-on rather than assumed:
- Its interface emits only relative links (`href="../../config/general/"`,
`href="../../staticcfg/css/Auto.css"`, `action="./one"`), and grepping the
5.1.1 source for `(href|src|action)="/` across `interfaces/{Glitter,Config,
wizard}` and for absolute `url:` literals in the Glitter JavaScript returns
nothing. A plain pass-through proxy preserves path depth, so no `sub_filter`
is warranted. The previous config's `sub_filter /sabnzbd ...` would also have
mangled the `https://sabnzbd.org/wiki/...` help links present on every
config page.
- Redirects are the one exception: `Raiser()` prefixes `cfg.url_base()`, so
`GET /` answers `303 Location: /sabnzbd/wizard/`. One `proxy_redirect`
handles every observed case; all of them were path-absolute, never a full
URL. Login redirects and logout go through the same `Raiser()`, and the
session cookie's path is hardcoded to `/` (`interface.py:316`), so it is
still sent under the ingress path.
- SABnzbd rejects a Host header that is not an IP literal:
`Host: homeassistant` answers 403 "Hostname verification failed", while
`Host: 192.168.1.5:8123` answers 200. nginx therefore sends `$proxy_host`
instead of including the shared `proxy_params.conf`, which forwards
`$http_host`.
`ingress_entry: sabnzbd` is dropped rather than kept: Supervisor appends it to
the ingress URL, which only resolves while the user's `url_base` is literally
`/sabnzbd`, and that is a setting they can change. SABnzbd serves the same
interface at `/` as under its `url_base` (verified for `/config/general/`,
`/static/`, `/staticcfg/` and `/wizard/`), so entering at the ingress root
works for any value, including the empty code default.
Ingress traffic reaches SABnzbd as `127.0.0.1:8080` and so is not filtered by
a user's host whitelist; direct ip:port access is unchanged and still is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(sabnzbd): keep the ingress Location relative and scope the login cookie
Exercising the shipped config against the running add-on caught a bug that
reading it did not. With nginx's default `absolute_redirect on`, rewriting
`Location: /sabnzbd/wizard/` produced
`http://homeassistant.local:18099/api/hassio_ingress/<token>/sabnzbd/wizard/`
— nginx expands a scheme-less replacement using the browser's Host and its own
listen port, which is the add-on's internal ingress port and is not reachable
from the browser. `absolute_redirect off` keeps it a path, which the browser
resolves against the Home Assistant origin.
`proxy_cookie_path` comes from Codex's review of the diff. SABnzbd hardcodes
the login cookie to `Path=/` (`interface.py:316`), so on the shared ingress
origin the browser would send it to every other add-on's ingress path as well.
Verified end to end by running the shipped nginx.conf and ingress.conf against
the live add-on, with the browser Host set to a non-IP hostname throughout:
all five redirect cases return a path under the ingress entry, the config
pages, wizard, API and both static roots return 200, and a stub upstream
emitting `Path=/` comes back rewritten to the ingress path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(sabnzbd): drop webui, which the add-on linter forbids alongside ingress
frenck/action-addon-linter fails the PR with "'webui' should be removed,
Ingress is enabled." No other ingress add-on in this repo keeps the key. The
"Open Web UI" button now opens ingress; the ports mapping is untouched, so
direct ip:port still works, it just has to be typed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(sabnzbd): make the nginx finish script actually work on s6-overlay v3
CodeRabbit is right that the execline finish script copied from nzbget is
inert on this image. s6-portable-utils dropped `s6-test` in favour of
execline's `eltest` — s6-overlay 3.2.1.0 ships no `s6-test` at all — so
execlineb cannot run the first `if` block and never reaches s6-svscanctl.
`/var/run/s6/services` is also the v2 scandir path; v3's legacy services.d
compatibility layer uses /run/service.
Rather than port it to eltest, use the shell form the scrutiny add-on already
ships: `kill -15 1` signals s6-overlay's init directly, so it depends on
neither the s6 tool set nor the scandir path, and it is three lines shorter.
The 0 and 256 exclusions are kept, so a normal shutdown does not trigger it.
Also fix the CHANGELOG date to YYYY-MM-DD per Copilot: that is what this file
and 7705 of the repo's 8068 dated headings use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(skill): stop the workflow scripts reporting verdicts they have not established
Every defect here is the same species: a check printed as passed that never ran, or
never proved what it claims. All six were reproduced before and after.
validate.sh
- `run`/`finish` were selected by filename and fed to bash -n and shellcheck. 25 of the
97 such files here are `#!/usr/bin/execlineb`, so 21 add-ons -- calibre_web and seerr
among them -- reported `local validation FAILED` and a wall of parse errors no matter
what the diff contained. One list, filtered by shebang, now feeds both checks; seerr
went from 15 shellcheck findings and two bash -n failures to the single finding the
test diff actually introduced.
- `grep -q "$ADDON/CHANGELOG.md"` was unanchored with `.` as a wildcard, so a diff
bumping zzz_archived_overseerr/CHANGELOG.md reported seerr's as updated -- a false
green on the one hard CI gate. Five such name collisions exist in this repo
(also birdnet-pi/battybirdnet-pi, mealie/social_to_mealie, plex/spotify_to_plex).
`grep -Fxq`.
- The --vs-master section skipped any file absent from origin/master, so a newly added
script's findings -- all of which are by definition added by the diff -- were never
reported, under a line reading "your diff introduced no new lint findings". An added
file now compares against an empty base. A deleted one is skipped: it was being linted
at a path that no longer exists, which turned every deletion into a fabricated
`openBinaryFile: does not exist` finding.
- That loop ran as the right-hand side of a pipe, so it could not have reached `fail`
even had it tried. It now runs in this shell and new findings fail the script; the
all-clear line is printed only when nothing was listed. Findings that merely moved
lines still cancel -- the comparison strips file:line:col before comm.
- `bash -n ok` stood for an add-on with no shell files at all, and hadolint could print
`clean` directly after printing findings (`A && {...} || C` with pipefail). Counted
and branched properly.
preflight.sh
- `MATCH -- this checkout corresponds to the running image` was concluded from
config.yaml's version equalling $BUILD_VERSION. Version is bumped once per PR, so any
later commit or a dirty tree matches while differing from what runs -- the one
conclusion the script exists to establish was the one it overstated.
pr_review.sh
- `watch` exhausting its minutes with checks still pending fell out of the loop and
exited 0, reporting success for checks that never settled. Unsettled is now exit 2.
Checks reported as `skipping` still count as passing, which is correct -- for this PR
itself, three jobs skipped because no */config.* changed, and that is the right
outcome, not a failure. But a skipped job tested nothing, so `watch` now says so.
Reviewed by Codex (gpt-5.6-sol), which corrected two claims in the audit behind this:
the .templates CHANGELOG assertion (CI skips the gate entirely for a template-only PR,
so that fix is not in this diff) and a tradeoff that did not exist. The deleted-file and
watch-timeout defects are its finds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(skill): address review — cwd independence, no pass verdict for an empty check
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* docs(skill): simplify again after the code review, and demand a trigger for defensive branches
The full loop ran simplify (step 5) before Codex's code review (step 6) and never
again, so nothing walked back what the review added. Adversarial review is asked to
find what could go wrong, so its output is a list of arguments for more code and it
is never asked whether the branch it wants is reachable — accepting objections only
ratchets the diff upward. Step 6 now ends by re-running step 5's checks over the
hunks the review touched.
The other half was earlier than the review. The standing rule already said complexity
is bought only by a measurement, but it said it about performance, so a branch added
for robustness did not visibly fall under it. It now covers hypothetical hosts as
well as hypothetical performance: name the input that reaches a defensive branch and
the image it happens on, or delete it and let the case fail visibly. Step 3's
attack-your-own-plan list asks the same question before any code exists, which is
where it is cheapest to answer.
The case study in references/simplify.md is #3013: 25 lines of code at review, 10
merged. A pure-bash fallback written at implement time for images shipping
with-contenv but not s6-dumpenv — reasoned from the two binaries living in different
s6 packages, never demonstrated on a real image, and defending a case that would have
degraded to the pre-fix behaviour anyway — plus the helper function and second reset
that existed only to serve it. Deleting the fallback deleted all of it. The review's
own objections were correct and cost two tokens on an existing line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(skill): fold recent-issue lessons into hassio-addon-workflow
Distilled from the calibre-web trusted-ips saga (#3004/#3009/#3010) and the
seerr builder revert (#2993/#2997):
- traps.md: 'merged is not on master' (builder revert-on-failure); new
section on writing into an app's own config (user-editable fields,
prefer boot-constant values, dual-stack mapped ranges)
- simplify.md: case study — stateful merge machinery (+34 lines, closed)
vs trusting the static supervisor range (net -6 lines, shipped)
- SKILL.md: post-merge survival check in step 9; a one-line feedback
loop in step 10 so follow-up PRs feed lessons back into references/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mbqutqkh7yTj4EnhWKFBQx
* docs(skill): address Codex review — auth blast radius, fetch before post-merge check
- traps.md: trusting a whole range for an auth header is an impersonation
trade-off needing the maintainer's explicit call, not a neutral
simplification (Codex P1)
- SKILL.md: fetch origin master before the post-merge survival check, the
tracking ref is stale otherwise (Codex P2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mbqutqkh7yTj4EnhWKFBQx
* docs(skill): fetch before the traps.md post-merge check too (Copilot review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mbqutqkh7yTj4EnhWKFBQx
* docs(skill): name the post-merge check explicitly — tree, not ancestry
CodeRabbit's merge-risk note on #3015: the post-merge step said to confirm
the commit 'survived', which reads as an ancestry check. A revert leaves the
commit in history and undoes its tree, so --contains reports success on
exactly the case the step exists to catch. Names the diff check instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mbqutqkh7yTj4EnhWKFBQx
* docs(skill): scope the post-merge check to your paths, not the whole tree
CodeRabbit was right that traps.md's 'your commit's tree is still what
origin/master holds' is invalid on a moving master — unrelated commits break
whole-tree equality. Scoped to the touched paths, matching SKILL.md.
Its other half, an ancestry check with merge-base --is-ancestor, does not
hold here: the repo squash-merges, so a merged PR head is never an ancestor.
Verified on #3010 — --is-ancestor reports NOT an ancestor while its fix is
live on master, i.e. a false failure on exactly the case the step must pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mbqutqkh7yTj4EnhWKFBQx
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(free_games_claimer): update upstream remaster to 1.6
Bumps the pinned Free-Games-Claimer-Remaster commit from 1.1 to the 1.6
release, adding the Ubisoft, Fab, AliExpress and Epic mobile stores, fixed
daily scheduler times and the --accept-lang detection fix.
Mirrors upstream's Chromium hardening (no-op xdg-open plus an
AutoLaunchProtocolsFromOrigins managed policy) so app-scheme links cannot
block the VNC session, and defaults upstream's release-update notification
off because it advises "docker compose pull" instead of the add-on store.
Closes#2990
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(free_games_claimer): track upstream releases instead of a pinned commit
The Dockerfile pinned upstream by commit SHA, which the repository updater
cannot bump, so updater.json was paused and every upstream release needed a
manual edit.
Replaces the SHA with ARG BUILD_UPSTREAM="1.6" -- the repo-wide idiom the
updater rewrites -- and downloads the matching v<version> source tarball.
Unpauses updater.json and excludes upstream's development tags (v1.7d and
similar), which carry no GitHub release.
The add-on keeps its own 2.x version series: ha_version.py derives a strictly
newer add-on version (2.1.0 -> 2.1.1) from a lower-sorting upstream tag, so
Home Assistant still offers the update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(free_games_claimer): survive an upstream tag naming change
lastversion reports upstream's v1.7d development tag as release "1.7", for
which GitHub serves no source archive; "github_exclude": "d" keeps it out of
the updater's reach. As a second line of defence the build now also tries the
tag name without the "v" prefix, so an unattended version bump cannot break
the image build on a tag naming change alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(free_games_claimer): make the description's store list explicitly partial
The shortened description named a subset of the supported stores, which both
review bots read as an inaccurate list. "and more" says the list is partial
while keeping the line inside the 80 column limit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(free_games_claimer): state the mutable-tag trade-off honestly
The comment and README carried over a claim from the commit-pin era: that the
image contents cannot change without a version bump. A release tag is mutable,
so that is no longer true. Say what actually holds and why the trade-off is
accepted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(free_games_claimer): stop the upstream label guessing the tag form
io.hass.upstream named the v-prefixed tag, which is wrong on the path where
the build falls back to the unprefixed archive. Point it at the releases list,
which is correct either way; the installed release is already recorded in
updater.json, CHANGELOG.md and the startup banner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Add-ons that override the base image's ENTRYPOINT ["/init"] with
ENTRYPOINT ["/usr/bin/env"] plus CMD ["/ha_entrypoint.sh"] never run s6-overlay's
stage 1, so nothing creates /run/s6/container_environment. `with-contenv` empties
the environment and repopulates it from that directory, which means every script
carrying a #!/usr/bin/with-contenv shebang outside the three globs whose shebang
ha_entrypoint.sh rewrites either exits non-zero before its first line of logic
(directory missing: s6-envdir errors) or runs against whatever a cont-init script
happened to leave there.
Measured in a running add-on of this repo whose PID 1 is /ha_entrypoint.sh: a
with-contenv script saw 16 environment variables where the entrypoint has 110, with
SUPERVISOR_TOKEN among the missing. Neither failure prints anything, so all that
surfaces is whatever the caller makes of a non-zero exit — a Docker HEALTHCHECK
reading "unhealthy" for eight months, in the case that prompted this. Cron jobs,
user-facing CLI wrappers and the user's own script.sh share the blind spot.
Dump the environment here instead, with s6-dumpenv, which is what stage 1 would
have done. Guarded on being PID 1 and on with-contenv existing, so it neither runs
under /init — where stage 1 already wrote the directory — nor warns in images that
have no with-contenv to fix.
Filled in a sibling directory and renamed into place rather than written live. A
half-populated envdir is worse than an absent one: s6-envdir accepts it, so a
with-contenv script starts and runs against an environment quietly missing
SUPERVISOR_TOKEN, where an absent one stops it at its shebang. A HEALTHCHECK can run
alongside PID 1, and rename(2) means such a reader sees the directory either absent
or complete. Measured with a racing poller over 664 samples: only 0 or 110 entries,
never a partial count.
The directory is cleared first rather than written over. /run is not a tmpfs in
these containers, so an image layer could persist entries there, and merging into
them would leave variables PID 1 does not have, including a stale SUPERVISOR_TOKEN.
A failed rm aborts the attempt, since mkdir -p accepts a surviving
symlink-to-directory and would let the dump follow it.
A failed seed leaves the directory absent, which is how this already fails today, so
the failure mode is unchanged rather than newly degraded — but it now says so.
Placed after the shebang probe on purpose. The probe's first candidate is
"/command/with-contenv bashio" and it fails today in exactly these add-ons, so the
probe falls through to "/usr/bin/env bashio". Seeding earlier would make that first
candidate start succeeding and flip the shebang of every cont-init and service
script that lands here, which is a much larger change than this fixes.
Refs #3006
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(calibre-web): trust the supervisor range for the ingress auth header
Reimplements #3004 from the code as it stood before it, in one statement.
#3004 derived the addon's own address and wrote it unconditionally on every
start. That address changes across restarts, so the value had to be rewritten
each boot, which erased anything the user had added to the same field from the
calibre-web admin page -- and a follow-up that preserved their entries needed a
merge pass and a record of what had been injected, because a preserved stale
address stays trusted after supervisor hands it to another addon.
Trusting 172.30.32.0/23 removes the reason for all of it: the range covers
whichever address the addon gets, so the value is constant and can be written
once. Both forms are listed because calibre-web listens dual-stack and an ipv4
entry never matches an ipv4-mapped address; /119 is the mapped equivalent of
/23.
The WHERE clause is what keeps it out of the user's way. The list is written
only when the range is absent, which is true on a fresh 0.6.27 install and on
an install still carrying #3004's per-address list, and false afterwards -- so
an entry added in the admin page for a reverse proxy outside the supervisor
network survives every later start.
The trade-off is that any addon on the supervisor network can now present
X-WebAuth-User to port 8083 and be logged in. Maintainer's call, taken
knowingly in preference to the machinery the narrow list required.
The tolerated failure from #3004 is kept: the column only exists once
calibre-web 0.6.27+ has migrated app.db and cont-init runs first, so the
statement is allowed to fail and the next start applies it. The sqlite error
is now included in the warning rather than dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(calibre-web): guard on the mapped range and keep existing entries
Addresses the review on #3010, both findings inside the one statement.
Codex, Copilot and CodeRabbit all noted the WHERE clause tested only
172.30.32.0/23, so a value carrying the ipv4 range without the mapped form
would skip the update forever while ingress stayed rejected -- a plausible
state, since that is exactly what someone adds by hand after reading that the
supervisor network is the source. Rather than test both, the guard now tests
::ffff:172.30.32.0/119 alone. That is the form ingress actually needs, given
calibre-web listens dual-stack, and the form nobody types by hand, so it
serves as the marker that this already ran. One substring either way.
Copilot and CodeRabbit also noted the assignment replaced the whole column,
losing an administrator entry on the first start. The required list is now
prepended to the existing value instead of replacing it. No case expression
is needed for the empty and NULL cases : the trailing comma that leaves
behind is an empty entry, which calibre-web's parser skips.
Both together cost one `||coalesce(...)` and a different substring. The
statement still runs at most once, and the duplicates it can leave behind are
entries calibre-web skips, or addresses inside the range now trusted anyway.
Checked against a transcription of cps/reverse_proxy_auth.py from 0.6.27 :
every produced value parses with nothing ignored, ::ffff:172.30.33.10 is
trusted and ::ffff:192.168.1.99 is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(calibre-web): trust the addon ip so ingress login works on 0.6.27
Calibre-web 0.6.27 added a trusted-source check for the reverse proxy auth
header (cps/reverse_proxy_auth.py:is_trusted_proxy_source) and defaults
config_reverse_proxy_trusted_ips to "127.0.0.1,::1". The ingress nginx binds
its upstream socket to the addon ip (proxy_bind $server_addr,
rootfs/etc/nginx/servers/ingress.conf:13), so calibre-web sees
::ffff:<addon ip> and discards X-WebAuth-User, leaving ingress at the login
page.
80-configuration.sh now writes that address - plain and ipv4-mapped, plus the
loopback forms - into config_reverse_proxy_trusted_ips next to the two
settings it already applies. The update is tolerated failing because the
column only exists after calibre-web 0.6.27+ has migrated app.db.
Closes#3003
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: address CodeRabbit review
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(portainer_agent): make the healthcheck runnable again
rootfs/usr/sbin/healthcheck line 1 was `#!/usr/bin/with-contenv bash`.
with-contenv runs `s6-envdir -Lf -- /run/s6/container_environment`, which
fails when that directory does not exist. This addon overrides the base
image's `ENTRYPOINT ["/init"]` with `/usr/bin/env /ha_entrypoint.sh`
(Dockerfile:88-89), and ha_entrypoint.sh runs the cont-init and services.d
scripts itself instead of handing over to s6-overlay, so s6 stage 1 never
runs and that directory is never created.
ha_entrypoint.sh rewrites the shebang of everything under /etc/cont-init.d
and /etc/services.d, which is why the service `run` script works. Nothing
rewrites /usr/sbin/healthcheck, so Docker's HEALTHCHECK died on the shebang
before reaching the curl - exit 1, no output, forever unhealthy.
The script only needs curl and hardcoded values, so plain bash is enough.
Closes#3002
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(portainer_agent): bump version, remove unused wait-for-signal script
Delete rootfs/usr/sbin/wait-for-signal (unused, broken expr, same bad
shebang) and its chmod in the Dockerfile. Bump version to 2025.12.7 to
match the CHANGELOG entry so Supervisor offers the healthcheck fix.
Co-authored-by: Alexandre <44178713+alexbelgium@users.noreply.github.com>
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(comicarr): new add-on with Home Assistant ingress
Comicarr is a fork of Mylar3 with a React frontend and a FastAPI backend.
The upstream image is a plain python:3.12-slim with no s6-overlay, so
ha_entrypoint.sh runs as pid 1 and supervises both the app and nginx —
the same shape the komga add-on uses.
Ingress needs a reverse proxy because the app has no url-base support of
any kind: vite emits absolute /assets urls, the api client and the cover
img tags build absolute /api and /cache urls, and SecurityHeadersMiddleware
sends X-Frame-Options: DENY together with a CSP carrying
frame-ancestors 'none', which alone would leave the panel blank. The
bundled nginx rewrites those paths onto the ingress entry, replaces the
two framing headers with the same policy narrowed to the Home Assistant
origin, scopes the session cookie to the ingress path and drops upstream's
one-year immutable caching for the rewritten assets.
The app is started directly as root by default rather than through the
upstream /entrypoint.sh, which runs useradd -u "$PUID" under set -e and
would exit on this repo's PUID=0 default; that entrypoint is still used
when the user asks for an unprivileged uid. --port 8090 is forced because
the port is writable from the Settings page and changing it there would
silently break both the proxy and the health check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(comicarr): note that switching PUID leaves existing files root-owned
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(comicarr): drop ingress_port, the add-on linter rejects the default
8099 is the Supervisor default, and frenck/action-addon-linter fails with
"'ingress_port' should be removed, it uses a default value". komga omits it
for the same reason; nginx still binds whatever bashio::addon.ingress_port
reports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(comicarr): 0755 on the entrypoint instead of 777
The rest of the repo uses 777 here, but this add-on is the one that offers a
non-root mode: with PUID set, the app runs as an unprivileged user that could
otherwise rewrite a file docker executes as root on the next start. Nothing
writes to /ha_entrypoint.sh at runtime, so 0755 costs nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): install the complete Codex package, not just the executable
Since codex-cli 0.147.0 the CLI delegates every shell and file-read tool call to a
companion codex-code-mode-host binary that it looks up next to its own executable.
81-codex_cli.sh downloaded the codex-<target>.tar.gz release asset, which contains
only the codex executable, so that binary was never installed and every tool call
failed with "failed to spawn code-mode host ...: No such file or directory" while
the run still exited 0.
Download the codex-package-<target>.tar.gz asset instead — the complete package
tree upstream's own installer uses — and install all of it into the existing
/data/codex prefix, which already satisfies Codex's layout contract. Make the
"already installed" test require the code-mode host and the package manifest so
existing incomplete installs repair themselves, and report layout completeness in
claude-tools-doctor.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): advertise Codex only when its package tree is complete
82-claude_tools.sh registers the Codex MCP server whenever the launcher at
/data/codex/bin/codex is executable and re-checks nothing else, while the launcher
and the package tree persist in /data independently of each other. Three paths
therefore reached that launcher next to an install that cannot run a tool call: a
boot that cannot reach the release metadata and keeps a pre-existing install missing
the code-mode host or the manifest, the same boot finding a stamp-less tree left by
an interrupted replacement, and a launcher surviving from an earlier boot after the
install was dropped. All three reproduced against the real script with stubbed
bashio/s6 and an unreachable metadata endpoint.
Define completeness once (executable, code-mode host, package manifest, version
stamp) and gate the launcher on it, removing the launcher and the /usr/local/bin
symlink when it does not hold. Nothing else is deleted, so a later boot completes the
install without another download or another login. The doctor's layout check now
includes the stamp for the same reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): install the Codex package tree by position, not by name
The whole reason for downloading the 118 MB codex-package asset instead of the
lone executable is that a helper Codex needs must not be left out — that is the
bug this branch fixes. install_codex_package still enumerated the five paths
release 0.148.0 happens to ship, so a helper added by a future release would be
downloaded, extracted and then discarded, failing exactly the way the missing
code-mode host does today. Verified against the extracted function: with a
staged tree carrying an unknown bin/ helper and an unknown top-level directory,
the previous code installed neither.
Move whatever the archive contains instead: every staged entry beside bin/ into
/data/codex, every staged bin/ entry except the entrypoint into /data/codex/bin,
then the entrypoint to codex-real last, so the ordering guarantee the stamp
relies on is unchanged. Only paths the archive actually contains are touched,
because /data/codex also holds this install's staging directory, and the
existing launcher is skipped by name while the version stamp is a dot file that
no glob matches. Removing each destination before moving onto it also drops
files an older release left behind.
Exercised with a scaffold around the extracted function: fresh install with
unknown helpers present, upgrade over an existing install with a stale helper
and a launcher to preserve, a minimal package with no optional directories, and
an unwritable prefix to confirm failure is reported rather than swallowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(claude_desktop): scope the two deletion claims to what the code does
Both overstated. install_codex_package() replaces every path the new release
ships, but does not prune a path upstream stops shipping, so "files an older
release left behind are removed with it" was wrong for exactly that case; and
"nothing is deleted beyond the launcher" read as if the /usr/local/bin/codex
symlink named in the previous sentence survived, when it is removed with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(kapowarr): new add-on with Home Assistant ingress support
Kapowarr is a comic book library manager in the *arr family. The add-on is
built on the upstream image (mrcas/kapowarr), with the repository's standard
nginx ingress scaffolding on top.
Ingress uses the pattern komga and bazarr already use here: Kapowarr renders
absolute urls from its url base, Home Assistant strips its own ingress prefix
before forwarding, so Kapowarr is started with --UrlBase /kapowarr and nginx
rewrites that fixed prefix back onto the ingress entry.
Database and logs go to the add-on configuration directory. Temporary
downloads are symlinked there rather than passed with --TempDownloadFolder,
which upstream re-applies on its own restarts and would keep overwriting a
folder chosen in Settings > Download.
* fix(kapowarr): review fixes from the codex pass
- repair a /app/temp_downloads symlink pointing at the wrong target instead of
accepting any symlink
- exclude logs and temporary downloads from Home Assistant backups: the temp
folder now lives in the add-on config directory and can hold gigabytes
- fix the /dev/nvme2n3p3 typo inherited from the copied device list (the
partition is nvme2n1p3); the same typo is present in the other add-ons
- document that the url base must not be changed, and that a non-zero PUID
only reaches folders that user can already access
- drop three dead Dockerfile lines (BASHIO_VERSION is overridden inside
ha_automatic_packages.sh, USER root is a no-op on this image)
* fix(kapowarr): pin host and port too, not just the url base
Found by a Codex review that could read the upstream source.
Kapowarr stores host, port and url base in its database and reads the stored
value whenever the matching flag is absent. Only --UrlBase was passed, so a
host or port changed in Settings > General survived every restart and upgrade
while nginx and the healthcheck stayed pointed at 127.0.0.1:5656 -- a permanent
502 with no way back except editing the database by hand.
All three flags are startup-only upstream, so passing them re-applies the
add-on's values once per container start without fighting the self-restarts
Kapowarr performs after a settings change.
The addon builds every psql connection as a postgres:// URI with the raw
username and password interpolated in. libpq percent-decodes the userinfo
part of a URI, so a password containing '%' (or '@', '/', '?', '#') is
decoded into different bytes before it reaches the server, and every
connection fails with "password authentication failed for user".
Encode the credentials with jq's @uri once and use the encoded copies in
the URIs only; the raw password is still what gets handed to Immich via
export_db_env and what is written by CREATE/ALTER USER. Those SQL
statements now double single quotes so a password containing a single
quote no longer breaks the statement either.
This is the same approach already used by the postgres_15 and postgres_17
addons in this repo.
Closes#1614
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(filebrowser_quantum): repair direct access on port 8071
1.5.1.1 published the port but direct access still did not work, in two ways
measured against a running instance:
1. The root redirect was absolute, so nginx built it from $server_port and
sent the browser to :8072 — the container-internal port, not the published
one. `absolute_redirect off` keeps the redirect relative.
2. The page served under /filebrowser_quantum/ referenced its assets under
the app's own baseURL (the ingress entry path), which that vhost did not
route: GET /api/hassio_ingress/<hash>/public/static/favicon.svg returned
404 while the same file under /filebrowser_quantum/ returned 200. The page
loaded and every asset on it failed.
Rather than translating paths, the vhost now passes requests through
unchanged and redirects only the bare root to the app's baseURL, which is
what its own links already point at. Asset, API and websocket URLs then work
without any response rewriting. Ingress is untouched.
* docs(filebrowser_quantum): describe the legacy redirects accurately
The comments, CHANGELOG and README still said only the bare root was
redirected, which stopped being true when the two /filebrowser_quantum
compatibility redirects were added. Raised by CodeRabbit and Codex.
* fix(komga): keep the reader inside the ingress panel
Komga's ui opens the reader with window.open(url, '_blank'). The Home
Assistant companion apps hand such a popup to an external browser, which
carries no ingress session cookie, so Home Assistant answers 401 before
Komga is reached.
Nginx now injects a small script into the ui shell that turns same origin
popups into a navigation in the current tab. The OAuth2 login popup, which
passes a window name and a feature string, and cross origin links are left
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): only intercept popups when resourceBaseUrl is known
Review feedback : the '/' fallback meant that if Komga ever stopped
setting window.resourceBaseUrl, every same origin _blank popup would be
captured -- and ingress shares the Home Assistant origin. Require the
base, and give it a trailing slash so a sibling path such as
<entry>/komgaX is not treated as being below <entry>/komga.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Restores #2993 verbatim. It was merged, then reverted by the builder's
revert-on-failure job a minute later - not because of anything in it, but
because EndBug/add-and-commit's floating v11 tag had moved to a release whose
action.yml no longer loads, so prebuild-sanitize failed before running a step.
The tag is pinned back to v11.0.0 in #2996, which has to land first for the
builder to get past that job.
The change itself is unchanged and still verified against the real njs module:
the rewritten /_next paths carry the add-on version, njs strips the marker
before proxying, so a browser holding the year-cached rewritten bundle fetches
fresh URLs on the first load after the update.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Every push to master has failed to build since 2026-08-19 05:15. The
prebuild-sanitize job dies before running a single step:
EndBug/add-and-commit/v11/action.yml (Line: 25, Col: 18):
Unrecognized named-value: 'github'. Located at position 1 within
expression: github.workspace
Failed to load EndBug/add-and-commit/v11/action.yml
Upstream's v11.1.0, published 2026-08-18 22:44 UTC, put a literal
"${{ github.workspace }}" inside the description of the `cwd` input. Action
metadata descriptions are still parsed as expressions and the `github` context
does not exist there, so the action no longer loads at all. The floating v11
tag was moved to it, which is why nothing changed in this repo and every
workflow using the action broke at once - the builder, the README and stats
refreshers, the CRLF sweep, the image compressor and the issue labeller.
v11.0.0 does not contain that line and loads normally, so pinning to it keeps
the version Dependabot moved us to in #2985 while stepping off the tag. It also
took out an unrelated add-on fix: the builder's revert-on-failure job reverted
the seerr merge (#2993) as collateral, and that is being reapplied separately.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(seerr): version the rewritten asset paths so cached bundles expire (#2975)
Seerr serves everything under /_next/static/ with "Cache-Control: public,
max-age=31536000, immutable", and nginx's sub_filter strips ETag,
Last-Modified and Content-Length from every response it rewrites. The HTML
is served "no-store" but keeps naming the same chunk URLs, and all three
3.4.1.x add-on versions ship the same upstream build, so a browser that had
loaded Seerr through ingress once kept replaying the JavaScript it cached
then - for up to a year, with no request to revalidate it.
That is why #2975 outlived two fixes: the reporter's https origin was still
executing the 3.4.1/3.4.1.1 bundle, whose rewritten root link makes Next
hard-navigate to /api/hassio_ingress/<token> without a trailing slash, which
Home Assistant does not route and answers with its own "404: Not Found". An
origin that had never cached it - the same instance over http://<ip>:8123 -
already showed the fixed behaviour.
The asset paths now carry the add-on version ("/ha-3-4-1-3/_next/..."), so
every release has its own URLs, a poisoned cache is bypassed on the first
load after an update, and any future change to a rewrite rule is actually
delivered. njs strips the marker again before proxying, so Seerr still
receives the paths it serves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(seerr): tighten the cache-bust comments after review
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(seerr): take the cache-bust marker straight from BUILD_VERSION
bashio::addon.version is an indirection here: bashio-standalone.sh defines it
as printf '%s' "${BUILD_VERSION:-1.0}", and the builder always passes
BUILD_VERSION from config.yaml, which the Dockerfile bakes in as an ENV. Reading
it directly drops a Supervisor round-trip and the fallback chain around it, for
the same value. The sanitiser stays: it protects the sed replacement and the
regex literal the marker lands in inside Seerr's bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(scrutiny_original): keep real /init as PID 1 so s6 supervision starts
collector-once's s6-svwait -u /run/service/scrutiny fails because
ha_entrypoint.sh (not the image's own /init) was PID 1 and never runs
real s6 supervision, so /run/service/* never gets created. Same root
cause and fix as scrutiny/scrutiny_fa (#2878): keep /init as PID 1 and
let ha_entrypoint.sh act only as the S6_STAGE2_HOOK.
Verified against the actual analogj/scrutiny:latest-omnibus image
(pulled via the GHCR registry API): its collector-once run script is
byte-identical to the one that caused #2877, and it bundles the same
s6-overlay-3.1.6.2.
Closes#2989
* fix(scrutiny_original): symlink /command into /usr/bin at build time
With /init as PID 1, ha_entrypoint.sh's own PID1 branch (which creates
this same symlink at runtime, and rewrites service run-file shebangs)
never runs. nginx/run and finish use #!/usr/bin/with-contenv bashio,
which only resolves if /usr/bin/with-contenv exists — so without this,
nginx (ingress) fails to start. scrutiny/Dockerfile and
scrutiny_fa/Dockerfile already do this in their "Install apps" stage;
this was missed when porting their PID-1 fix over.
Caught by chatgpt-codex-connector's PR review.
Same bug and root cause as scrutiny_original (#2991) and scrutiny/scrutiny_fa
(#2878): collector-once's `s6-svwait -u /run/service/scrutiny` fails because
ha_entrypoint.sh (not the image's own /init) was PID 1 and never runs real s6
supervision, so /run/service/* never gets created.
scrutiny_fa_original shares the exact same rootfs (byte-identical cont-init.d
and services.d/nginx scripts) as scrutiny_original and builds from the same
ghcr.io/analogj/scrutiny:latest-omnibus image, so the same fix applies:
keep /init as PID 1, patch ha_entrypoint.sh to hand off to real s6-rc
supervision, and symlink /command into /usr/bin at build time (nginx/run and
finish use #!/usr/bin/with-contenv bashio, which only resolves once that
symlink exists — a P1 finding from scrutiny_original's PR review that
applies here identically).
Keeps the existing bashio::require.unprotected guard in
/etc/cont-init.d/90-run.sh unchanged.
* fix(seerr): stop rewriting the root link inside the JS bundle (#2975)
3.4.1.1 appended a trailing slash to both the server-rendered "Discover"
link and its counterpart inside Seerr's JavaScript bundle. The slash is
correct in the HTML - Home Assistant routes ingress on
"/api/hassio_ingress/{token}/{path:.*}" and rejects a slash-less entry -
but it cannot survive in the bundle: next/link resolves a pushed href
through normalizePathTrailingSlash(), which drops a trailing slash while
`trailingSlash` is false, and Next.js then hard navigates to the
slash-less URL, recreating the same 404. On the root page it instead
throws "Invariant: attempted to hard navigate to the same URL" and the
click does nothing.
Dropping the bundle rewrite leaves the link as "/", which the client
router matches against its own "/" route and transitions to in-app -
the same path every other sidebar entry already takes, none of which are
rewritten here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(seerr): drop text/html from sub_filter_types
nginx pre-seeds text/html into sub_filter_types, so listing it emits
`[warn] duplicate MIME type "text/html"` on every config load. Verified
against nginx 1.22.1 locally: with the type dropped, `nginx -t` is
warning free and an HTML response is still filtered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Seerr's Discover link is href="/". nginx rewrote it to the bare ingress
entry, but Home Assistant only routes ingress on
"/api/hassio_ingress/{token}/{path:.*}", so a URL without the trailing
slash matches no route and Home Assistant answers its own plain-text
"404: Not Found" before the request reaches the add-on.
Closes#2975
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Fix FileBrowser Quantum direct web access
* fix(filebrowser_quantum): make direct ip:port access actually work
ports: {8080/tcp: 8071} alone (as originally proposed) publishes the app's
own port, but FileBrowser Quantum's server.baseURL is set at boot to the
Supervisor ingress-entry path (an opaque, per-install hash), so the app only
serves correctly under that exact path -- a bare port publish gives an
unreachable page, per alexbelgium's own analysis on #2978.
Add a second, dedicated nginx vhost (direct.conf) that proxies a fixed public
path (/filebrowser_quantum/) onto the same ingress-entry baseURL the existing
ingress vhost already targets, instead of changing the app's baseURL itself.
This leaves the ingress vhost, and therefore Ingress access, completely
unchanged -- only the new vhost is new surface area. config.yaml now
publishes the new vhost's internal port (8072) to host 8071, not the app's
own 8080 directly.
Co-authored-by: polmonta <polmonta05@gmail.com>
---------
Co-authored-by: polmonta <polmonta05@gmail.com>
Co-authored-by: alexbelgium <alexandre.pary@gmail.com>
* fix(claude_desktop): patch safeStorage on bundles without a use-strict directive
The v1.37 safeStorage patcher only knew how to inject its plaintext-encryption
opt-in after a leading "use strict" directive in Claude Desktop's main bundle,
and refused to patch anything else. Confirmed live: Claude Desktop 1.30096.1's
main bundle no longer opens with that directive (bare IIFE instead), so the
patch has been silently refusing to run on every boot and sessions stopped
persisting across restarts again, with the same "Encryption not available"
warning documented in SIGN_IN.md before v1.37.
applyPatch() now falls back to prepending the opt-in as the bundle's first
statement when no directive is found, after skipping any leading BOM,
hashbang, or banner comment so a directive hidden behind a comment is still
protected rather than pushed out of position zero.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(claude_desktop): scan the full directive prologue, not just line 0
Addresses two Codex review findings on PR #2983:
1. skipPrologue()'s //-comment scan only recognized "\n" as a line
terminator. A comment ending in CR-only or U+2028/U+2029 (all valid
ECMAScript LineTerminators) made it swallow the rest of the file as
"still the comment", landing the patch after the bundle's last
statement instead of before it. Reproduced with
`// banner\r"use strict";(function(){})();`.
2. applyPatch() only checked whether the very first statement was
literally "use strict". A directive prologue can hold more than one
string-literal statement, and "use strict" only has to appear
somewhere in it, not first; prepending ahead of an earlier directive
pushed the whole prologue out of first-statement position and
silently dropped strict mode. Reproduced with
`"use custom";"use strict";(...)`.
Replaced the single-directive check with scanDirectivePrologue(), which
walks every leading string-literal-only statement and inserts right
after the full prologue (or at the same position when there is none).
skipPrologue/applyPatch split into skipBomAndHashbang +
skipWhitespaceAndComments + scanDirectivePrologue accordingly.
Verified: both findings reproduced against the pre-fix code and no
longer occur; 13-case regression suite covering the original edge cases
plus both findings all pass; re-run against the live production
app.asar still patches successfully and idempotently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
config.yaml declares ingress_port: 8071 but no ports: key, so nothing is
published to the host network. ingress_port is the internal port the
Supervisor ingress proxy connects to on the add-on's private IP, and Home
Assistant only renders the Network card for add-ons that declare ports:.
Direct access at <your-ip>:8071 has therefore never worked; the README was
carried over from the sibling filebrowser add-on, which does declare
ports: 8080/tcp: 8071.
Correct the three README claims rather than publishing a port, since the
app is configured with server.baseURL set to the ingress entry and would
not serve correctly on a plain published port without further work.
Closes#2978
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(birdnet-pi): turn ALSA_CARD into a valid ALSA PCM name for REC_CARD
99-run.sh copied ALSA_CARD verbatim into REC_CARD, but BirdNET-Pi passes
REC_CARD to "arecord -D" (scripts/birdnet_recording.sh) and "ffmpeg -f alsa
-i" (scripts/livestream.sh), which expect an ALSA PCM name. A card index such
as ALSA_CARD=1 therefore produced "Unknown PCM 1" and no recording at all.
Build "plughw:CARD=<value>,DEV=0" from a card index or card id, and pass
through a value that already is a PCM name. Also use sed --follow-symlinks so
the rewrite no longer replaces the ~/BirdNET-Pi/birdnet.conf symlink with a
detached copy of /config/birdnet.conf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: address CodeRabbit review
* fix(birdnet-pi): bump version so the ALSA_CARD fix actually ships
The PR changed 99-run.sh and added a CHANGELOG entry but left config.yaml
untouched, so `version` still read 2026.08.02. Supervisor only offers a rebuild
when `version` changes: without this the fix would have merged, the add-on would
have kept running the old image, and the issue would have looked closed while
ALSA_CARD stayed broken.
2026.08.15 matches the CHANGELOG heading this PR already adds, which is this
add-on's convention — every past version lines up with a dated heading
(2026.08.02, 2026.07.22, ...). Not a `.N` counter bump: birdnet-pi's `version`
has drifted from updater.json's `upstream_version` (0.11), so the counter rule
does not apply and the add-on's own date scheme governs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(birdnet-pi-zach): turn ALSA_CARD into a valid ALSA PCM name for REC_CARD
birdnet-pi-zach/rootfs/etc/cont-init.d/99-run.sh carried a byte-identical
copy of the same bug fixed in birdnet-pi by this PR: REC_CARD was copied
verbatim from ALSA_CARD, but BirdNET-Pi passes REC_CARD to "arecord -D"
and "ffmpeg -f alsa -i", which expect an ALSA PCM name, not a card index.
sed -i also replaced the $HOME/BirdNET-Pi/birdnet.conf symlink with a
detached copy on first use.
Apply the same fix: build "plughw:CARD=<value>,DEV=0" from a card index
or card id, pass through a value that already is a PCM name, and use
sed --follow-symlinks against /config/birdnet.conf only. Documented in
README_standalone.md, same as birdnet-pi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: resolve ALSA_CARD against real PCM names, not a fixed allowlist
CodeRabbit and Codex both flagged that the passthrough check only recognized
default/null/pulse/pipewire: any other colon-free ALSA PCM name (sysdefault,
front, surround51, a custom .asoundrc alias, ...) was still misread as a card
index/id and rewritten as plughw:CARD=<name>,DEV=0, which then fails to open.
alsa-utils is already installed in both images, so check the value against
"arecord -L" (an exact, whole-line match against its unindented PCM-name
lines) instead of hardcoding the set of names ALSA ships with. Anything that
isn't a real PCM name still falls through to the plughw:CARD= build, so a
numeric index or a card id is handled exactly as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ai): let the fix step own config.yaml, and require the patch-counter bump
The premise that the fix step cannot touch config.yaml turned out to be wrong,
and the real problem was the opposite of what it looked like.
config.yaml was already in scope — issue-fix.md lists it among the files the
sweep reads and owns, and all three merged ai-fix PRs edited it. What they
edited, though, was the one thing hard limit 2 forbade outright:
PR #2970 qbittorrent version: "5.2.3.2" -> "5.2.3.3"
PR #2912 bazarr version: "1.6.0.1" -> "1.6.0.2"
Both bumped only the LOCAL PATCH COUNTER, leaving the upstream X.Y.Z alone —
i.e. exactly the right thing, in direct violation of the written rule. Nothing
enforces that rule (ai_guard_paths.sh only covers .github/ and .templates/), so
it has been quietly contradicted by practice, and it also contradicts CLAUDE.md's
own PR requirement to bump version.
It matters because Supervisor will not offer a rebuild without the bump: a fix
merged without one ships inert while the issue looks closed. That is the worst
outcome available — worse than not fixing it.
So the carve-out is narrowed to what addons_updater actually owns (the
`upstream` field and the upstream X.Y.Z), and bumping the trailing .N is now
required rather than forbidden, with the dot-not-hyphen trap called out
(X.Y.Z-N reads as a semver pre-release and Supervisor treats it as older).
Exotic version shapes — LSIO tags, dates, nightlies — are explicitly left alone
rather than guessed at.
Applied to all four places the rule is stated so they cannot drift:
issue-fix.md, issue-execute-plan.md, CLAUDE.md, and pr-coderabbit.md — the last
keeps the restriction, since it amends a PR whose single bump already covers it,
but now says why instead of reading as a contradiction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ai): derive the patch counter from updater.json, not from version's shape
Six review findings, all reproduced against the repo before accepting.
Codex (P1) — the rule "increment the trailing .N" is wrong for most of this
repo, because you cannot tell a local counter from an upstream component by
looking at `version`. Checked all 134 add-ons:
version == upstream_version (no counter, must APPEND .1): 82
version == upstream_version + .N (counter, INCREMENT): 8
version drifted from upstream (LEAVE ALONE): 36
no usable updater.json (LEAVE ALONE): 8
So the previous wording would have mutated updater-owned data on 82 add-ons:
sonarr's 4.0.19.3001 IS the upstream version, and incrementing it to
4.0.19.3002 burns the identifier of a future real release; linkwarden's 2.16.0
would have become 2.16.1, indistinguishable from an upstream minor bump.
updater.json's upstream_version is now the authority: append .1 when version
equals it, increment only the digits that follow it, otherwise leave version
alone. Validated by running the rule as written over every add-on — 0
violations of the invariant that a bumped version must still start with
upstream_version.
Copilot — there is no `upstream:` key in any config.yaml (0 of 134); upstream
tracking lives in updater.json as upstream_repo / upstream_version. That was
inherited text naming a field that does not exist, in all four places. Replaced
with the real constraint: never edit updater.json.
Copilot — the "a workflow step enforces them" headers over-claimed. Only limit
1 is machine-enforced (ai_guard_paths.sh); the rest ship silently if broken,
which is worth saying plainly given limit 2 has been quietly contradicted by
practice for months.
Copilot — Outcome B produces a plan and no PR, so "say so in the pull request
body" had no place to land. Now covers both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A single failing matrix leg in the builder reverts the entire push, so any
transient error inside a build silently undoes a good version bump.
Observed on zoneminder 1.38.4 (run 31678876409, attempt 1): the aarch64 leg
failed after 44 s inside the ha_autoapps.sh layer with
curl: (92) HTTP/2 stream 1 was not closed cleanly: REFUSED_STREAM (err 7)
gzip: stdin: unexpected end of file
tar: Error is not recoverable: exiting now
while the amd64 leg built and pushed 1.38.4 to GHCR. revert-on-failure then
pushed 0ee26fc72 reverting the bump; a manual re-run of the same source went
fully green. Because that re-run flips the run conclusion to success, these
incidents do not even show up in run-conclusion statistics.
The build-image step is now run tolerantly (continue-on-error) and repeated
once when the first attempt fails. Only a second failure reaches
revert-on-failure, so genuinely broken add-ons are still reverted, one build
later than before.
The retry is unconditional rather than gated on the log text looking
transient: BuildKit reformats error strings and registry/runner failures
spell themselves many different ways, so a text classifier would eventually
stop reverting real breakage. It is also cheap - the add-ons that fail
deterministically on every push (ente, comixed, binance-trading-bot) each
fail in 16-34 s.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(qbittorrent): allow ::/0 so WireGuard stops dropping IPv6
_wireguard_up() sets allowed_ip_types[0.0.0.0/0] when the config declares an IPv4 Address, but the IPv6 branch never sets ::/0. The peer therefore only accepts the tunnel's own /128, while _routing_add() installs a default IPv6 route into the interface, so every outbound IPv6 packet is routed into WireGuard and dropped.
Regression from 7af8610a25; the pre-refactor code appended ::/0 in the same place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Update config.yaml
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds a short answer-style rule to the hassio-addon-workflow skill: no
pleasantries, no tool-call narration, no dumped logs, no re-printing what
is already in context. Uncertainty markers, negations, numbers and
verbatim technical text are explicitly exempt, so the Verified / Checked /
Assumed discipline in step 9 is not compressed away. Persisted text
(commits, CHANGELOG, PR bodies, review replies, the report) stays prose.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: wait for the Supervisor API before running startup scripts
48 add-ons build their nginx ingress config out of bashio::addon.ip_address
and bashio::addon.ingress_port. Both come from one GET /addons/self/info, and
when that is answered before the Supervisor is ready bashio prints nothing.
Nine add-ons paste the result straight into a sed and end up writing
"listen : default_server;", which nginx rejects with `invalid port in ":"`;
the other 39 assign first and abort under set -e, leaving %%port%%
placeholders. Either way ingress is dead for that boot.
ha_entrypoint.sh now polls /addons/self/info once before the cont-init loop
and waits until it reports this add-on's ip_address (and, for ingress
add-ons, a non-zero ingress_port). Bounded at 30s via HA_SUPERVISOR_WAIT,
never fatal, and skipped entirely without SUPERVISOR_TOKEN or curl. When the
Supervisor is already up -- the normal case -- it costs one request.
qBittorrent is bumped so the change is actually built and reaches the add-on
with the open report; the other add-ons pick it up on their next rebuild.
Refs #2949, #2962
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: harden the Supervisor wait after bot review
- HA_SUPERVISOR_WAIT=08 was accepted by test -gt but read as octal by
arithmetic expansion, leaving deadline empty; the comparison then errored
every iteration and the loop never exited, hanging start-up. Digits-only
validation plus base-10 forcing.
- A request started near the deadline could run --max-time past it. The
per-request timeout is now capped to the time remaining, and the retry
sleep is skipped once the budget is gone, so the ceiling is exact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: probe the Supervisor through bashio instead of curl + sed
The wait reimplemented what the 48 consumers already do: it called
/addons/self/info with curl and picked the fields out with sed. That parallel
implementation was where one of the review findings landed, and it left a
residual race -- proving the API answered a moment ago says nothing about the
bashio call that runs next.
Probing through bashio removes both. bashio caches a successful
/addons/self/info under ${CACHE_DIR:-/tmp/.bashio}, so once the probe returns,
every bashio::addon.* call in every cont-init script reads that file rather
than asking the Supervisor again. Verified: one bashio::addon.ip_address call
writes a 26 KB addons.self.info.cache.
One call also settles all the fields, so the separate ingress/ingress_port
branch was redundant and is gone: a populated ip_address means the whole object
is cached. 36 -> 31 code lines.
Two consequences handled: bashio's own curl carries no --max-time (api.sh:41),
so each attempt is bounded with timeout; and bashio-standalone.sh answers these
calls from environment variables without ever contacting the Supervisor, so
BASHIO_LIB_FULL gates the probe to images carrying the real library.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Komga scopes its cookies to its servlet context path, so the browser never
sent them back from the ingress url and every request after a successful
login was anonymous (401).
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): ship an apparmor profile so local disks can be mounted
Without apparmor.txt Supervisor adds no apparmor security_opt, so Docker's
default profile applies and denies mount() and raw block device access:
mount reported 'cannot mount /dev/sda1 read-only' and the kernel logged
'/dev/disk/by-label/NAS: Can't open blockdev'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(komga): bound the nginx readiness probes and log an exhausted wait
Follow-up to #2960, which merged one commit before this landed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): use a wall clock deadline for the readiness wait
An attempt count plus a per probe timeout stretched the wait to roughly twice
the advertised 15 minute ceiling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): restore add-on reverted by a transient ghcr login failure
The amd64 builder job failed at docker login (denied: denied) before any build
step ran, which tripped revert-on-failure. Re-running the same commit unchanged
succeeded and both arch images are published.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): poll komga directly instead of bashio::net.wait_for, clarify config path
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(komga): add Komga comics/manga server add-on with ingress
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): correct chmod path, PUID default and server-generated absolute urls
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): drop webui, the addon linter rejects it when ingress is enabled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(komga): review fixes - init order, POSIX healthcheck, drop inert s6 vars
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(radarr): bump version so HA offers the rebuilt ls313 image
The `6.3.0.10514` image tag was rebuilt and re-pushed on 2026-08-03 and
now ships LinuxServer.io ls313, while installations made before that date
still run the ls311 build they originally pulled.
Because build.json tracks the floating `lscr.io/linuxserver/radarr:*-latest`
tags, a rebuild silently changes the image contents without changing the
add-on version. The Supervisor decides whether an update exists purely by
comparing the `version` string in config.yaml against the installed one --
it does not compare image digests -- so an unchanged string means the
update is never offered and the new image is never pulled.
Add the local patch counter documented in CLAUDE.md to make the rebuild
visible to the Supervisor. Radarr itself is unchanged at 6.3.0.10514, so
updater.json keeps upstream_version as-is; the updater bot only rewrites
config.yaml when the upstream version moves, matching how lidarr
(3.1.0.4875 -> 3.1.0.4875.1) and bazarr (1.6.0 -> 1.6.0.2) already work.
The dotted `.1` form is required rather than `-1`: AwesomeVersion parses
`6.3.0.10514-1` as an unknown strategy and raises on comparison, whereas
`6.3.0.10514.1` compares as SimpleVer and sorts above `6.3.0.10514`.
* chore(sonarr,prowlarr): update to latest upstream releases
Sonarr 4.0.19.2997 -> 4.0.19.3001 (develop-4.0.19.3001-ls184, 2026-08-11)
Prowlarr 2.6.2.5517.9 -> 2.6.2.5534.9 (nightly-2.6.2.5534-ls9, 2026-08-08)
Both add-ons track a prerelease channel (github_beta), and their build.json
files pin the floating `-develop` / `-nightly` LinuxServer.io tags, so the
rebuild picks up the matching base image on merge.
Unlike radarr, neither add-on was affected by the stale-image problem: the
published images match the versions they claim (sonarr ships ls183 for
4.0.19.2997, prowlarr ships ls9 for 2.6.2.5517), so these are ordinary
version bumps that the weekly updater bot would otherwise pick up.
Sonarr also records the upstream version in ARG BUILD_UPSTREAM, updated
here to match. Prowlarr has no BUILD_UPSTREAM line.
* perf(stargazer-map): negative-cache blank locations for 90 days
The lookup predicate treated a blank country as "not cached", so all 1689
blank rows of the 2652-row cache were re-queried on every weekly run --
~1689 GitHub API calls plus ~28 minutes of the polite time.sleep(1), to
re-derive the same blank answer. In a 89-user sample of those blanks,
87 (97.8%) simply have no public "location" on their profile, so the
lookups fail permanently rather than transiently.
Add a "last_checked" column to the CSV cache. A blank country is now only
re-queried once its check date is more than 90 days old; a known country is
still never re-queried; a user absent from the cache is queried immediately.
Rows from the old two-column file are treated as checked on 2026-08-10, so
the migration happens in the loader and the next run rewrites the CSV.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(stargazer-map): treat a missing last_checked as never checked
Rows carried over from the two-column CSV are no longer backfilled to the
migration date; an absent, empty or non-ISO-date last_checked now reads as
"never checked" and is looked up on the next run, which stamps it. The first
run after merge therefore does the ~1689-user sweep once, and only after that
does the 90-day cadence take over.
Also addresses the review point that a corrupted last_checked in an already
three-column CSV would compare as "recent" under the lexicographic check and
suppress re-checks indefinitely: load_cache() now validates the cell with
datetime.date.fromisoformat and drops anything that is not a real date.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* style(stargazer-map): add the missing save_cache docstring
Codacy flags C0116 on the touched function.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* style(stargazer-map): capitalize load_cache docstring (pydocstyle D403)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(stargazer-map): cap expired rechecks at 200 per run to stagger them
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(stargazer-map): cap re-checks only, never the first sweep
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(stargazer-map): resolve countries via ISO code, not English name
username_to_country() matched Nominatim's English display_name against
pycountry, but the two vocabularies disagree: pycountry.countries.lookup()
raises LookupError for "Russia", "Turkey" and "Ivory Coast" (its ISO names
are "Russian Federation", "Türkiye", "Côte d'Ivoire"). Those users were
silently recorded as unknown -- the committed cache has 963 users with a
country and zero Russia, so Russia rendered grey on the map.
Request addressdetails from Nominatim and read address.country_code
instead. No new dependency, same one request per user, and it drops the
reversed-component loop that could false-positive on a city or region
named like a country.
The return value is unchanged: still a pycountry .name string, so the CSV
cache and the ISO-3 rendering lookup are unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(stargazer-map): drop non-answer locations before geocoding
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The country lookup is done with Nominatim, i.e. OpenStreetMap data
(ODbL), which requires attribution wherever the derived data is shown.
The footnote credited only the GitHub profile. Add two lines crediting
Nominatim/OSM for the geocoding specifically -- the country shapes are
plotly's Natural Earth basemap, not OSM.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(stargazer-map): readable log-scale map with baked-in stats
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(stargazer-map): count only current stargazers, honest caption wording
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(stargazer-map): show shares only, drop absolute per-country counts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore(skill): prefer reusing existing code for repo homogeneity
The standing rule already demanded the simplest solution; it said nothing
about where that solution should come from. A bespoke-but-simple mechanism
in one add-on is still a second way to solve a problem 120+ add-ons share.
- Standing rule: build out of what exists (.templates/ module, existing
cont-init script, a sibling add-on's pattern), and match repo naming
conventions when something new is genuinely needed.
- Step 3 (Plan): search for prior art before ranking mechanism levels; not
reusing an existing mechanism now requires stating why.
- Step 5 (Simplify): reuse check alongside the existing ones — fold
near-duplicates in, or justify the divergence in the PR body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(skill): address Codex and CodeRabbit review feedback
- Prior-art search: the --include='*.sh' --include='config.yaml' allowlist
missed the repo's main mechanisms. `ARG MODULES=` lives in Dockerfiles and
s6 v3 services are extensionless `run` files; searching for MODULES= found
6 files under the allowlist vs 129 (125 Dockerfiles) without it. Widened to
--exclude-dir=.git and named the two file types explicitly.
- Reuse vs isolation: "fold a near-duplicate into the existing mechanism"
contradicted traps.md:125, which requires a new numbered script rather than
editing scripts shared by symlink with the webtop add-ons. Added the carve-out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): stop tier 1 wasting its turn budget; escalate max-turns after one retry
Now that classification actually runs, the 12-turn budget got its first real
exercise — and #2949 died on it. The budget was never the problem; how it was
spent was. Turn-by-turn from that run: 3 turns retrying Bash (not in
allowedTools, and failing against the bubblewrap sandbox that
allowed_non_write_users switches on), 6 hunting .templates/ha_entrypoint.sh and
ha_automodules.sh which are not in the sparse checkout, leaving 3 for the issue.
Fixed at the cause rather than by raising the cap, which stays at 12:
* .templates is now checked out. Most add-ons are thin wrappers around those
shared scripts, so a large share of reports can only be explained by reading
them — this makes triage more accurate, not merely faster. 184K, 25 files.
It has to be added in TWO places: ai_triage_context.sh calls
`git sparse-checkout set`, which REPLACES the list, so omitting it there
would silently undo the workflow's checkout at exactly the wrong moment.
* The prompt now states the environment up front: three tools, no Bash, and
precisely which paths exist on disk. The model cannot discover these cheaply
— every probe costs a turn it then does not have for the analysis.
Separately, a max-turns death is NOT a workflow fault, but GATE 1 treated every
action failure as systemic and never escalated. So #2949 failed red, stayed
unlabelled, and the catch-up re-dispatched it daily forever — taking the first
of only five slots each time, since it sorts newest-first. It is now handled
like GATE 2: one retry, then ai:needs-human. Detected from the action's
execution_file, which is written even on failure. Warning rather than error,
because a red run per day for a per-issue condition is alarm fatigue, and the
outcome is recorded durably on the issue itself.
The two escalation sites are now one shared function, so they cannot drift.
Re-tested all 15 paths: max-turns across the three events, genuine action
failure with and without an execution file, and the full existing sweep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): make the max-turns probe fail closed on an unexpected file shape
Copilot: hit_max_turns scanned with `.[]?` and no root-type check. jq's `.[]?`
iterates the VALUES of an object, so if the action ever changed the execution
file's shape, {"result":{"subtype":"error_max_turns"}} would have matched —
downgrading a genuine workflow failure from a red run to a warning. That is the
silent-failure class this workflow exists to remove, arriving through the door
I had just built.
Reproduced: with the old filter that object matched; with `(type == "array")`
prepended it does not. Anything that is not the array we expect now falls
through to the loud path.
Verified: the real array shape is still detected and still escalates on the
second look; object-root, nested-object and non-JSON execution files all exit 1
red instead of being swallowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): fail loudly when escalation doesn't land; don't escalate a manual first look
Two CodeRabbit findings, both reproduced before accepting.
1. escalate_to_human suppressed `gh issue edit` with `|| true`, so it returned
success even when ai:needs-human never landed. Both callers then exited 0
reporting a hand-off that had not happened — and, having no label, the issue
went straight back into the retry rotation the escalation existed to remove.
The edit now propagates its status and callers exit 1 with an explicit error.
`gh label create` stays best effort; the edit fails on its own if the label
is genuinely missing. Verified that removing a label an issue does not carry
is a no-op, so this cannot fail spuriously.
2. EVENT_NAME was doing duty as an attempt counter, but workflow_dispatch is
BOTH the daily catch-up retry and the maintainer's manual re-triage — so a
hand-dispatched FIRST attempt was escalated immediately.
Rather than the suggested explicit retry state, the two are already
distinguishable: the catch-up dispatches with GITHUB_TOKEN and arrives as
github-actions[bot], a manual run as the maintainer. Confirmed against run
metadata (catch-up 2026-08-10 = github-actions[bot]; manual 2026-07-27 =
alexbelgium). is_automated_retry() keys on both, which makes "one retry then
a human" literally true without new persistent state: a manual attempt that
fails leaves the issue unlabelled, so the catch-up still gets its go.
Re-tested 15 paths including a stubbed `gh` failure at the escalation site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): raise max-turns to 25; no max-turns path may end in a silent green run
Three changes, one requested and two from an independent Codex review.
* --max-turns 12 -> 25, per the maintainer's updated call. The prompt preamble
and comments were carrying the old number and are updated with it. The
upfront optimisation stays: the earlier waste was 3 turns retrying an
unavailable Bash and 6 hunting files outside the sparse checkout, and a
bigger budget should buy analysis rather than more of that.
* Codex objected that the max-turns branch reintroduced the very failure class
this workflow exists to prevent. It was right. On the SECOND look the outcome
is durable (ai:needs-human), but on a FIRST attempt nothing was recorded
anywhere except an annotation, so exiting 0 was a green run over triage that
silently did not happen. Now the only exit 0 is the one where the escalation
label actually landed; every other max-turns path is red. My "alarm fatigue"
argument was overstated: escalation ends the rotation, so this costs at most
one red run per problem issue, not one per day.
* Codex also flagged inferring the retry from github.actor as brittle — a
re-run, a PAT- or App-issued dispatch, or a different maintainer all change
it, and the false NEGATIVE (an automated retry never recognised as one, so it
retries forever) is the dangerous direction. Replaced with an explicit
`source` dispatch input that only the catch-up sets. Unknown provenance is
now safe by construction because that path ends red rather than green.
Re-tested: max-turns across first look / manual dispatch / catch-up retry /
catch-up-with-failing-label / issue_comment, plus the full existing sweep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ci): correct two triage comments the recent logic changes left stale
Comments only — no behaviour change, confirmed by diffing out comment lines
(nothing else moved) and re-running the behavioural suite to identical results.
* The prompt preamble still said "the turn budget is 12" and computed
"leaving 3 for the actual issue" off it. The budget is 25 now. Reworded to
keep the #2949 evidence, which is still true as history (3 turns retrying
Bash, 6 hunting files outside the sparse checkout), while stating the
current budget and why it is not licence to probe more.
* GATE 2 still said "A workflow_dispatch is the catch-up or a manual
re-triage, i.e. the second look". That stopped being true when escalation
moved to is_automated_retry(): only source=catchup counts as the second
attempt, and a manual dispatch is a first look that deliberately does not
escalate, leaving the issue unlabelled so the catch-up still gets its go.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): allow the github-actions bot actor, stop quarantining on systemic failure
Follow-up to #2947, from watching it run in production. The catch-up dispatched
for the first time (the 403 is gone), but all five dispatched runs then failed:
Actor type: Bot
##[error]Workflow initiated by non-human actor: github-actions (type: Bot).
checkHumanActor (src/github/validation/actor.ts) is a SEPARATE gate from the
write-permission one, and rejects any actor whose account type is not User.
allowed_non_write_users does not cover it — that is only consulted for User
accounts. Switching the catch-up to GITHUB_TOKEN in #2947 made those runs
arrive as github-actions[bot], so it traded the 403 for this.
Fixed with `allowed_bots: "github-actions"` — named rather than "*", since only
this repo's own workflows dispatch as that actor. Scheduled runs are unaffected
either way: they arrive as actor=alexbelgium, a User, which is also why the
tier-2 sweep never hit this.
The same run exposed a design error in #2947's bounded retry. It quarantined an
issue with ai:needs-human when the ACTION failed — but an action failure is
systemic, hitting every issue identically, so a workflow-level fault silently
buried a batch a day. It is the opposite case that is issue-specific: the action
ran fine and the model still produced no usable verdict. Inverted:
* action failed -> fail red, touch no labels, let the catch-up retry
* ran but no verdict -> one retry, then ai:needs-human on the second look
Five issues (#2847#2850#2852#2896#2918) were quarantined by the old rule and
need their ai:needs-human removed once this lands, so they re-enter the queue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): allow the coderabbitai bot actor on the PR follow-up tier
Same gate, latent instance. on_pr_coderabbit.yml fires on a review submitted by
coderabbitai[bot], so github.actor is a Bot-type account and checkHumanActor
rejects it. Every run so far skipped on the `ai-fix/*` branch guard before ever
reaching the action, so this has never surfaced — it would have failed on the
first genuine invocation, taking the whole CodeRabbit follow-up tier with it.
Note this is NOT covered by the write-permission check returning early for
[bot] actors: checkHumanActor is a separate gate consulted independently.
Audited all five claude-code-action call sites. The other three need nothing:
on_claude_mention and on_issue_approved are gated to alexbelgium, and the
daily_ai_fix schedule runs as actor=alexbelgium — confirmed from run metadata,
not assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): clear retry triggers when escalating to a human; correct a comment
CodeRabbit: the no-verdict escalation added ai:needs-human but left ai-triage
and ai:needs-info in place. The catch-up search excludes both, so the automated
path never reaches it — but a MANUAL re-triage of an already-queued issue does,
and there it matters: ai-triage would keep an issue we just handed to a human
sitting in tier 2's unattended fix queue, and ai:needs-info would let a reporter
reply silently re-trigger classification behind the human's back. The normal
verdict path already clears stale control labels; this makes the escalation
path consistent with it.
Copilot: the action-failure comment claimed "touch no labels", but the
ai:needs-info restore above may already have run on the issue_comment path.
Reworded to say it adds no labels of its own, and why the restore still stands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): check the action outcome before trusting its structured output
CodeRabbit caught the silent-failure mode sneaking back in. The shape check ran
first, so if the action failed AFTER writing a valid structured output, the
object sailed through, labels and a comment were applied, and the step exited 0
— a green run on a failed action, which is the precise thing this workflow was
rebuilt to eliminate. Reproduced: valid verdict + CLASSIFY_OUTCOME=failure
applied bug/ai-triage/ai:classified and posted the comment at exit 0.
A failed action means its output is not trustworthy, full stop, so the outcome
check now runs before the payload is read at all. That also reads better as two
sequential gates rather than nested branches:
gate 1 action failed -> restore ai:needs-info, fail red, add nothing
gate 2 payload unusable -> restore ai:needs-info, warn, escalate on 2nd look
otherwise -> normal verdict handling
The ai:needs-info restore is now a function rather than being repeated at each
exit, since both gates need it.
Re-tested all 12 paths: the two newly-corrected cases plus a full regression
sweep over empty/array/valid payloads across issues, issue_comment and
workflow_dispatch, and the owned / low-confidence / label-grab branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): revive AI issue triage — permission gate and catch-up dispatch
Tier 1 has been failing on every issue since it went live, while every run
reported success. Two independent causes, both masked:
1. claude-code-action treats `issues` / `issue_comment` as entity contexts
and runs checkWritePermissions() against github.actor — the outside
reporter, who never has write. Every Classify step died with "Actor does
not have write permissions"; continue-on-error painted the job green, and
Apply verdict found no verdict.json and exited 0. No issue ever got the
`ai-triage` label, so the tier-2 sweep collected an empty batch nightly
and there were no automatic fixes either.
Fixed with `allowed_non_write_users: "*"`, which is the input this case
exists for. It only takes effect alongside the `github_token` already
passed. `schedule` / `workflow_dispatch` are automation contexts and skip
the gate, which is why tiers 2 and 3 were unaffected.
2. The catch-up job dispatched with AI_PR_TOKEN, a fine-grained PAT with no
actions scope: every dispatch returned 403 and `|| echo :⚠️:`
swallowed it. Switched to GITHUB_TOKEN with a job-level actions:write —
workflow_dispatch is exempt from the no-recursion rule, so no PAT is
needed at all.
Both failures now fail the run instead of reporting success, which is the
part that stops this recurring.
Harden the model's output path, as the action's docs require when the
permission gate is bypassed: drop Bash and GH_TOKEN from the Classify step
(the context script already ran the duplicate search), validate the verdict
enum, cap the comment at 4000 chars, defuse @mentions in it, and accept only
`bug`/`enhancement` as model-supplied labels — the repo also carries
automerge, Priority, codex and wontfix, which a crafted issue body must not
be able to reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): require the verdict document to be a JSON object
`jq -e .` accepts any truthy JSON, so a verdict of `[1,2]` or `"hi"` passed
the guard and then died on `.verdict` with "Cannot index array with string".
Under set -e that killed the step before the ai:needs-info restore, stranding
the issue so no later reporter reply could re-trigger classification.
Reproduced at exit 5 on an issue_comment event before the fix; the same case
now takes the restore path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): drop the Write tool from triage, deliver the verdict via json-schema
Codex review raised a real escalation path. `allowed_non_write_users: "*"`
deliberately admits untrusted reporters, and the model reads their issue body.
It also had a Write tool, so an injected instruction could write a script to
disk and append BASH_ENV=<that script> to the runner's $GITHUB_ENV file command
— discoverable under $RUNNER_TEMP with Glob. The runner applies $GITHUB_ENV
between steps, so the very next bash step (Apply verdict, holding an
issues:write GH_TOKEN) would source it before any validation ran.
Removing Write closes the chain at its source rather than patching a link:
the verdict now comes back through the action's --json-schema structured
output, so the model needs no filesystem write at all and is left with
Read/Glob/Grep. The schema also enforces the verdict and confidence enums and
the two-label cap at the action layer; the shell-side validation stays as
defence in depth.
Apply verdict materialises the structured output through env, never inline
interpolation. issue-classify.md updated to match. All existing behaviour
re-tested through the new path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(binance-trading-bot): pip install with --break-system-packages (PEP 668)
Alpine's py3-pip now marks the system Python as externally managed, so the
TradingView requirements install failed with 'externally-managed-environment'
and broke every rebuild (run 31227083872).
Also bumps to upstream v1.0.0, which the updater bot tried and had reverted by
the same build failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(binance-trading-bot): pin base image to the frozen v0 line, drop the v1.0.0 bump
Upstream retagged chrisleekr/binance-trading-bot:latest to v1.0.0 on 2026-07-31
(latest and v1.0.0 share digest sha256:60a1a88e...). build.json pinned :latest,
so this add-on was silently building on the v1 rewrite while its rootfs still
starts mongod/redis and runs the v0 'npm start' entrypoint.
v1.0.0 is a complete rewrite with no in-place upgrade: the datastore moved to
Postgres + TimescaleDB. Adopting it needs an add-on rewrite, not a version bump,
so pin build_from to :0.0.101 (the frozen v0 line, linux/amd64 + linux/arm64)
and pause the updater so the bot stops re-proposing v1 weekly.
Keeps --break-system-packages so the build survives PEP 668 if the pin ever
moves forward.
Reported by Copilot on PR #2945.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(binance-trading-bot): drop --break-system-packages, the pinned image predates it
CI: 'no such option: --break-system-packages'. The 0.0.101 image ships a pip
older than 23.0.1, which is where that flag was introduced.
The flag was only ever needed because :latest had moved to the v1 rewrite and
its newer Alpine carries a PEP 668 marker. With build_from pinned to the frozen
0.0.101 image, that marker cannot appear, so the flag is both unnecessary and
fatal. binance-trading-bot/Dockerfile is now identical to master again: the
whole fix is the build.json pin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ente): base image moved to ghcr.io/ente/server
Upstream renamed the GitHub org ente-io -> ente. github.com redirects, so the
web-builder clone still worked, but GHCR does not redirect: the base image
ghcr.io/ente-io/server:latest now resolves to 'not found' and every rebuild
failed (run 31227202542).
Points the base image and the web source clone at the new org, and lands the
4.4.25 bump the updater bot had reverted by the same failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ente): sync build.json and README to the renamed org, tidy changelog
build.json still pinned ghcr.io/ente-io/server:927c6a31... — a dead reference
after the org rename (the pinned digest does resolve under ente/server, verified
200 from the registry). It is inert today since this Dockerfile hardcodes its
FROM rather than consuming ARG BUILD_FROM, but leaving a dead ref there is a
trap for the next person.
Also repoints the two README links and matches the changelog date format to the
surrounding entries.
Reported by Copilot on PR #2946.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(portainer): request identity encoding on the ingress listener
Portainer compresses its own responses, so every response reached Home
Assistant's ingress relay gzipped and chunked, with no Content-Length.
Both relay hops (Supervisor api/ingress.py and Core hassio/ingress.py)
only take their buffered path for responses carrying a Content-Length
under 4 MB; everything else goes through the streaming path, where an
aiohttp error surfaces to the browser as 502 Bad Gateway even though the
add-on's own nginx logged a 200.
proxy_params.conf already stripped Accept-Encoding, but the location
block declares its own proxy_set_header directives, and nginx discards
every server-level proxy_set_header once a location sets any of its own
(the comment above those lines warns about exactly this). The strip was
therefore dead config.
The same server block serves both the ingress listener and the direct
web UI port, so the strip is scoped through a map on $server_port:
ingress gets identity, direct access keeps compression. The map keys on
the direct-access port rather than the templated ingress port, so the
default stays correct if the ingress port ever changes.
Verified with a local nginx against the live Portainer backend:
- ingress listener, client sending "Accept-Encoding: gzip, deflate"
-> identity, Content-Length: 14203
- direct listener, same request -> Content-Encoding: gzip, chunked
- direct listener, no Accept-Encoding -> identity, Content-Length
- websocket upgrade through ingress still reaches Portainer (401 auth)
- nginx -t passes for both the ssl and non-ssl rendered variants
Partial mitigation only: vendor.js (5.7 MB) and main.js (7.0 MB) exceed
the 4 MB buffering threshold uncompressed and still stream. This
supersedes 2.43.0.1, which disabled nginx's own gzip module rather than
the compressor that was actually running.
Refs #2766
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portainer): request identity explicitly on the ingress listener
Address review feedback: use "identity" rather than an empty value as
the map default. Both were verified to produce identity responses with a
Content-Length from Portainer, and both leave direct access on 9099
compressed, but "identity" states the intent explicitly instead of
relying on the server's choice when no Accept-Encoding is present.
Also reword the changelog entry for readability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* refactor(skill): shorten hassio-addon-workflow via progressive disclosure
SKILL.md was 365 lines, loaded in full on every add-on task. Split it per
Anthropic's Agent Skills best practices: keep steps, completion criteria,
and the mechanism ladder inline; push rationale, war-story examples, and
Codex CLI invocation details into reference files loaded only when that
branch is taken.
- SKILL.md: 365 -> 171 lines. The "ship the simplest solution" rule was
stated three times; now once. Steps 3/6/9 keep their load-bearing
checklist but point to detail files instead of inlining it.
- references/evidence.md (new): measurement methodology, the
host-generalization failure examples, the merged-and-inert case studies
- references/codex-review.md (new): CLI invocation, prompt guidance,
plan-attack checklist
- references/simplify.md (new): mechanism-ladder case studies
Also replaces the old "Token efficiency" section (which named this
maintainer's personal MCP tools - rtk/headroom/tokensave - not guaranteed
present for CI agents or other collaborators using the checked-in copy)
with a subagent-delegation instruction: Codex's plan/code review and
PR-comment triage on >5 threads should run in a subagent that returns a
condensed summary, not raw output, into the calling session.
* fix(skill): address PR review feedback
- SKILL.md: define $SKILL once and state that all scripts/ and references/
shorthand paths are relative to it — they read as repo-root-relative
otherwise and don't resolve from an add-on directory
- codex-review.md: drop the reference to a "global CLAUDE.md note" that
isn't in this repo's CLAUDE.md; describe --sandbox read-only accurately
(reads allowed, writes/exec blocked, approvals disabled) instead of
"cannot run anything"; add the missing & so the example is actually
backgrounded as the prose claims
- evidence.md: use smaps_rollup for process RSS — plain smaps prints one
Rss: line per mapping (47 for a trivial process here), not a total
- simplify.md: drop the absolute "removals cannot regress" claim, which
contradicted the /dev/shm case study in evidence.md
* refactor(skill): apply independent quality-review findings
From an independent model review of the skill against the Agent Skills
best-practice guides:
- traps.md: CHANGELOG was described as "the only hard gate", contradicting
SKILL.md — the HA add-on linter and the image build also block PRs
(verified against onpr_check-pr.yaml). Gates list corrected.
- codex-review.md: the "attack your own plan" checklist is run by the main
agent, but lived in the file whose stated consumer is the delegated
subagent — an agent that delegates correctly would never see it. Moved
inline into SKILL.md step 3.
- SKILL.md: delegation instruction now says exactly what to do (spawn a
subagent whose prompt includes references/codex-review.md and follows its
invocation) instead of "delegated per the rule above".
- SKILL.md: traps.md is no longer mandatory reading on the light path —
the light-path facts it holds (versioning, CHANGELOG format) are inline
in step 7; it stays required when touching scripts/Dockerfiles/env.
- SKILL.md: dropped the bundled-files table (every row already cited at
point of use) and the "read the script when you use it" anti-instruction
— scripts are run, not read. 177 -> 167 lines.
- description: 982 -> 550 chars; removed workflow narrative that does
nothing for skill selection and the stale-prone model name, kept all
trigger terms.
* fix(skill): note that CI hard gates skip on non-addon PRs
The three hard gates in onpr_check-pr.yaml (changelog check, addon-linter,
check-build) are each matrixed over check-addon-changes.outputs.changedAddons
and if:-skipped when it is '[]'. A PR touching only docs, .github/ or .claude/
therefore shows them as "skipping" rather than passing — which should not be
read as a green build. Verified against onpr_check-pr.yaml lines 64, 83, 101
and against this PR's own check output.
/docker-entrypoint.py renumbers www-data to PUID:PGID after cont-init has
run, then re-chowns only its DATA_DIR. Chowning /config by name beforehand
left /config/modules_v4 owned by the image's original www-data uid (33),
so the running Apache/PHP process (uid 1000) could not create directories
in it -- breaking custom module installation from the webtrees UI.
Fixes#2940
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore: add hassio-addon-workflow skill for Claude Code
Checks in the repo-specific skill so future Claude Code sessions get the
tiered scope->measure->plan->implement->verify workflow, repo traps, and
helper scripts (preflight/measure/env_trace/validate/pr_review) without
depending on a local machine's ~/.claude config.
* fix(skill): address PR review feedback from Copilot and Codex
- SKILL.md: repo-relative script invocation (skill is now checked in);
correct the CI-gates list — the PR add-on linter is blocking, only the
weekly Super-Linter is non-blocking
- preflight.sh: git-aware repo detection (worktrees have a .git file)
- pr_review.sh: header now documents resolve's actual --all behavior
- measure.sh: CPU% uses getconf CLK_TCK; sample all processes, not the
top-24 by RSS
- env_trace.sh: validate VAR as a strict env-var name before regex use
- validate.sh: shellcheck also covers extensionless run/finish; --vs-master
skips visibly when a linter is missing instead of reporting a false clean
* fix(skill): address CodeRabbit review feedback
- pr_review.sh: resolve exits nonzero unless every thread actually
resolved; watch exits nonzero and says so when checks settle with
failures instead of reporting bare "settled"
- validate.sh: pass the config.yaml path to Python as argv instead of
interpolating $ADDON into the source (CWE-94)
The GPU acceleration shipped in 2026.08.03 was not inert — it was what turned
the GPU off. `--use-gl=angle --use-angle=gl-egl` forces Mesa's EGL X11 platform,
which offers no window-capable EGLConfig under this Xvfb. The GPU process logged
`gl_surface_egl.cc:262 No suitable EGL configs found`, abandoned GL, and was
relaunched with `--use-gl=disabled` while every renderer got
`--disable-gpu-compositing`.
Chromium already renders on the GPU here with no flags at all: LSIO's Xvfb runs
`-vfbdevice /dev/dri/renderD128`, so its GLX is backed by the real render node.
The premise that Xvfb offers only an indirect/software path was wrong for this
base image. Measured on a separate display, including at the production
15360x8640 screen — with the flags the GPU process loads libEGL_mesa and holds
1 fd on the render node; without them it loads the Mesa gallium megadriver over
DRI3, holds 8, and no renderer carries --disable-gpu-compositing.
claude-gpu-probe was not wrong about the hardware, it answered the wrong
question: it exercised ANGLE's default GLX path, which works, so it passed while
the flags it gated disabled the GPU. Removed with the gpu_acceleration option.
Also fixes two other changes from the same release that never did anything:
- max_resolution wrote MAX_RES into the s6 container_environment, but svc-xorg
starts `#!/usr/bin/env bashio`, not with-contenv, and never reads it. Renaming
the option to MAX_RES makes the add-on env layer inject it into every service
run script, which is how DRINODE already reaches Xvfb. It ships with no
default, so nothing changes until it is set: carrying over the old 1920x1080
default would have silently shrunk every existing desktop, since that value
had never taken effect. Schema bounds each axis to 100-9999.
- The amd64 driver install has never run in any release: guarded by `if [[ ]]`
with no SHELL directive, so it runs under dash, which has no `[[` — condition
false, RUN still exit 0. It is deleted rather than repaired, because it had
nothing to add. Hardware GL already works without it, Mesa already comes from
the LSIO base at a newer backports version, and intel-media-va-driver-non-free
was installed live on the running add-on and changed nothing: VA-API failed
identically to the free driver on both DRM nodes. That failure is below the
add-on, in the host i915 stack.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* perf(claude_desktop): render on the GPU and stop duplicating MCP servers
Measured live inside a running add-on (amd64, 4 cores): 3388 MB RSS across
73 processes, with the Electron renderer burning ~44% of a core even with no
browser client connected.
GPU: Chromium was rendering everything on the CPU. Under Xvfb it probes GLX,
finds only Xvfb's indirect/software path, and falls back to `--use-gl=disabled`
plus `--disable-gpu-compositing` — while a perfectly good iGPU sits idle behind
/dev/dri. Claude Desktop is now launched through ANGLE's OpenGL backend over
EGL, but only when the new claude-gpu-probe confirms that Desktop's own bundled
ANGLE can create a hardware GL context on this host; the probe rejects
llvmpipe/SwiftShader, is bounded by a timeout, and any failure leaves the
command line exactly as it was. New `gpu_acceleration` option (auto|on|off).
MCP: every stdio MCP server is a separate process per client, and Desktop
starts another full set for each Claude Code session it hosts. Claude Code now
reaches the Home Assistant MCP server over its native HTTP transport instead of
the mcp-proxy stdio bridge, removing the most expensive duplicate (~45 MB of
private RSS per copy). Desktop keeps the bridge: its remote-entry config schema
could not be confirmed, and guessing would silently break it. New
`mcp_servers_desktop` / `mcp_servers_code` options let each client register only
what it actually uses; defaults are unchanged.
Display: new `max_resolution` option (default 1920x1080) caps the virtual screen
via the base image's MAX_RES. Xvfb ran at 15360x8640, so it and the Selkies
capture loop tracked damage over a 133-megapixel area continuously. This is a
CPU saving, not a memory one — the framebuffer is a lazily populated shared
segment whose unused portion was never resident.
Dockerfile: the Intel graphics block was dead code. It was gated on TARGETARCH,
which this repo's builder does not pass, so it never ran — the shipped amd64
image has no vainfo and no intel-media-va-driver-non-free, and its apt history
contains no matching install. It now uses BUILD_ARCH, and its Vulkan ICD check
no longer names intel_icd.x86_64.json, a file Debian does not ship.
Also removes `--disable-dev-shm-usage` (a workaround for a 64 MB /dev/shm; this
image has 7.7 GB) and fixes stale Home Assistant MCP registrations, including
the bearer token inside them, being left behind when enable_ha_mcp is disabled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): satisfy static analysis in claude-gpu-probe
Codacy flagged two new issues, both in the probe: a broad exception catch and
too many locals in main(). Split the EGL bring-up into load_angle(),
open_angle_display(), make_current_context() and describe_renderer(), each
raising a dedicated ProbeFailure, so the failure paths read as intent rather
than as a chain of early returns. The catch-all remains — a probe must never
stop the desktop from starting — but is now explicit and narrowly scoped.
No behaviour change: exits 0 with a hardware renderer under DISPLAY, 1 without.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): address PR review on shm and MCP entry ownership
Two review findings, both correct.
/dev/shm: dropping --disable-dev-shm-usage outright was generalised from one
host. The flag was added to fix a real Electron renderer crash loop on Docker's
64 MB default, and Home Assistant ignores the add-on's shm_size, so the size
genuinely varies per install and cannot be asserted from this repo. The size is
now read at startup: the workaround is kept below 256 MB, dropped above it, and
kept when the size cannot be determined.
MCP ownership: claiming an HTTP 'homeassistant' entry by URL and shape would
have deleted a user's own manually configured server on the first boot after
upgrade, since ha_mcp_url defaults to the same public endpoint that a hand-
written entry would use, and enable_ha_mcp defaults to false. An HTTP entry is
now only ever modified or removed when the add-on recorded writing it, in
~/.config/claude_desktop_addon/managed-mcp.json. Anything not written by the
add-on is untouchable regardless of how it looks.
Also drops the invalid '?' optional marker from the list *item* type in the
mcp_servers_* schema; both keys always carry defaults, so the marker was
meaningless as well as wrong.
Tests cover the regression directly: a user-owned HTTP entry on the default URL
now survives both a disabled and an enabled boot, while the add-on's own entry
is still removed with its token when enable_ha_mcp is turned off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): tighten max_resolution validation and reporting
grep anchors ^...$ per line, so a multi-line max_resolution such as
"1920x1080\n640x480" passed validation on its first line and was then written
to MAX_RES verbatim, leaving svc-xorg with a corrupt screen size. Bash's =~
anchors the whole string and rejects the embedded newline.
Also stop reporting success when no s6 environment directory existed and
nothing was written — the cap silently did not apply, and the log said it did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(birdnet-pi): restore the ingress Caddy site, ingress returned 502
nginx forwards ingress traffic to 127.0.0.1:8082 (rootfs/etc/nginx/servers/
ingress.conf:11), a site appended to the Caddyfile by helpers/caddy_ingress.sh.
The upstream script $HOME/BirdNET-Pi/scripts/update_caddyfile.sh regenerates
/etc/caddy/Caddyfile from scratch and 02-caddy.sh runs it immediately before
`exec caddy run`, so 91-nginx_ingress.sh injected a call to caddy_ingress.sh
into that script to re-add the site. The injection was anchored on
`sudo caddy fmt --overwrite`.
Since 2026.07.10-1 the Dockerfile strips `sudo ` from every BirdNET-Pi script at
build time (Dockerfile:110), so the shipped line is `caddy fmt --overwrite` and
the anchor stopped matching. sed reports success when a pattern matches nothing,
so this failed silently: Caddy came up listening only on :8081 and every ingress
request got connection-refused on 8082. Confirmed by extracting the script from
the published ghcr.io/alexbelgium/birdnet-pi-amd64 image - it contains no `sudo`.
- 91-nginx_ingress.sh: make the anchor accept the line with or without `sudo`,
skip the injection when it is already there (cont-init re-runs on restart),
and verify afterwards, falling back to appending the call if the anchor is
ever gone again.
- caddy_ingress.sh: return early when a `:8082` site already exists. The script
now runs from more than one place, and a duplicate site address makes Caddy
refuse to start.
- 02-caddy.sh: re-add the ingress site just before starting Caddy if it is
missing, so a future upstream change to update_caddyfile.sh cannot silently
bring back the 502.
- 91-nginx_ingress.sh: drop /ingress_url when ingress is off, so that marker is
a truthful signal for the check above even across an in-container restart.
Verified with a harness that replays the boot sequence (build-time sudo strip,
81-modifications.sh, 91-nginx_ingress.sh, 02-caddy.sh) against the real upstream
update_caddyfile.sh: master ends with no :8082 site, this branch ends with
exactly one, on fresh boot, on restart, and when the `caddy fmt` anchor is
removed entirely; standalone mode still gets no ingress site.
Closes#2928
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(birdnet-pi): harden the ingress-site recovery check in 02-caddy.sh
Review feedback on the pre-start check:
- Require /etc/caddy/Caddyfile to exist and silence grep's stderr. `! grep` is
also true for grep's exit code 2, so an unreadable or missing Caddyfile read
as "ingress site missing" and would have produced an ingress-only Caddyfile
out of an error state. Letting caddy fail on the missing config is easier to
diagnose.
- Report a failure of caddy_ingress.sh instead of swallowing it. Do not exit:
/custom-services.d scripts are LSIO longruns that s6 restarts when they
return, so exiting would flap the service and take the WebUI down on 8081 as
well, which is strictly worse than ingress alone being broken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Seventeen addons carried a config.yaml version that awesomeversion, the
library Home Assistant orders versions with, reports as UNKNOWN: text
tags such as version-bf9e0b4f or sts, LinuxServer tags such as
v26.2-ls256, and local counters written as a semver pre-release such as
15.7-47. Home Assistant cannot tell which of two such versions is newer,
so update detection depends on a compare exception rather than on
ordering, and any version that becomes partially comparable silently
stops being offered.
Each version keeps every number it carried, as a section of its own:
v26.2-ls256 becomes v26.2.256, 4.16-r0-ls95-7 becomes 4.16.0.95.7 and
5.0.0b5-3 becomes 5.0.0.5.3, so nothing that ordered the addon is lost
and no previously published version is reused. The two versions holding
no number at all use the date instead. Only config.yaml and CHANGELOG.md
change, so every addon still builds from the upstream tag recorded in
its Dockerfile and updater.json.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(addons_updater): write Home Assistant compliant addon versions
The updater copied the raw upstream tag into config.yaml. Home Assistant
orders addon versions with awesomeversion and hides the update when it can
compare both versions and the new one is not strictly newer, so tags such
as 1.2.3-2, 1.2.3+4 or 1.2.3-2026-08-01 silently stopped the update from
being offered, and tags such as version-bf9e0b4f or ubuntu-2026-06-01
cannot be ordered at all.
The addon version is now derived from the upstream tag by ha_version.py,
using the same library Home Assistant uses: a sortable and newer tag is
kept as it is, 1.2.3-4 and 1.2.3+4 become 1.2.3.4, otherwise the release
number inside the tag, an incremented addon number or the date is used.
updater.json keeps the raw upstream tag, so the same upstream release is
never published twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(addons_updater): address review comments
- calendar versions carrying a counter now advance to the current date
instead of only incrementing the counter
- --selftest runs against a fixed date, so it keeps passing after today
- config.json is written from a validated jq result, as updater.json is
- README states the raw tag is added to the changelog only when it
differs from the addon version
- docstring, comment and changelog formatting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(addons_updater): turn pre-release markers into a version section
"5.0.0b5" is published as "5.0.0.5" so the beta number keeps ordering
the addon instead of relying on how awesomeversion reads the marker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(addons_updater): keep every number of an unorderable tag
"v26.2-ls256" is published as "v26.2.256", "nightly-2.6.1.5509-ls8" as
"2.6.1.5509.8" and "4.16-r0-ls94" as "4.16.0.94", so the build number
keeps ordering the addon instead of being dropped. Words holding no
number, architectures and commit hashes are left out, and a section
ending on a year is counted up rather than incremented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* style(addons_updater): keep the helper docstrings on one line
Codacy runs pydocstyle with D213, which the multi-line summary added
with the numbers rule trips.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(addons_updater): leave out more arch words and unreal dates
"i686" and friends were read as the number 686, and "2026.02.31" was
taken for a calendar version. Both now fall back to the plain number
rules.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The "mount" auto-detection in ha_automatic_packages.sh appended the
glob "ntfs*" to the apt install list. On Debian trixie-based images
(e.g. ghcr.io/starosdev/scrutiny:latest-omnibus, used by scrutiny and
scrutiny_fa) this glob fails to resolve even though the real package
ntfs-3g exists, breaking the Docker build. Use the exact package name
instead, matching the apk branch just above it.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Exclude the compile-breaking Species Manage PR from dynamic source merging by keeping it in draft, and retrigger the birdnet-go-dev build as source-20260729.1.
* fix(claude_desktop): persist sign-in by opting into Electron safeStorage
Claude Desktop asked the user to sign in again on every start. The v1.35 fix
was inert: --password-store=basic did reach the process (confirmed on a live
install's /proc/<pid>/cmdline), but the app still logged "safeStorage not
available, tokens will not persist" on every launch.
Electron refuses its built-in basic_text backend unless the application calls
safeStorage.setUsePlainTextEncryption(true) before the ready event, and Claude
Desktop never calls it - the symbol is present in the shipped Electron binary
but absent from resources/app.asar. So isEncryptionAvailable() stayed false and
the auth token was never persisted. Verified against a standalone Electron of
the same generation: without the opt-in it is false; with it, true, and a
separate later process decrypts a blob written by an earlier one.
There is no equivalent command-line switch, and NODE_OPTIONS=--require is
ignored by packaged Electron apps (verified against the real binary), so the
opt-in is injected into the app's main bundle inside app.asar. gnome-keyring
stays out of the image: its first-boot password prompt blocks the app from
launching at all.
The patcher fails closed, rebuilds the archive preserving unpacked/symlink
entries, recomputes the changed entry's SHA-256 integrity record, and fully
re-validates the result from disk before renaming it into place. It re-runs on
every boot after 81-claude_update.sh, since an apt upgrade ships a fresh
unpatched app.asar, and is marker-guarded so an unchanged app is a no-op.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): harden the safeStorage hook against review findings
- Sweep stale .app.asar.addon-tmp.* from the shell hook. `timeout` kills the
patcher outright, so a run that hits the 120s cap never executes its own
cleanup; the live archive stays unpatched, so every later boot would retry
under a new pid and strand another archive-sized file.
- End the hook with an explicit `exit 0`. The logging `while` loop's status
became the script's status, so an empty last line could exit non-zero and
fail cont-init - the opposite of the documented "never block startup".
Both raised in review on #2922.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(webtop,webtop_kde,claude_desktop): give Selkies XDG_RUNTIME_DIR and the right WS port
Selkies panics with `RuntimeDirNotSet` right after its data websocket server
comes up, and binds that server on 8081 while nginx proxies 8082.
Upstream relies on s6-rc ordering: init-selkies-config publishes
XDG_RUNTIME_DIR and CUSTOM_WS_PORT into the s6 envdir, and svc-selkies is
started afterwards. ha_entrypoint.sh replaces s6-overlay and launches every
s6-rc.d run script in parallel with no dependency graph, so a longrun can
snapshot the envdir (with-contenv reads it once, at exec) before the oneshot
has written to it. Port 8081 in the report is the proof: that is selkies' own
default, not the 8082 init-selkies-config writes near the end of its run.
Only the webtop images carry PIXELFLUX_WAYLAND=true, which is why the missing
runtime dir reaches a Wayland socket bind there and not on claude_desktop.
20-folders.sh now exports both variables inside each run script, where no
start ordering can lose them, and 90-ingress.sh derives the nginx CWS
substitution from the same value.
Also correct the base image's $HOME/.XDG override where that write happens
rather than appending a correction to init-selkies-config: the oneshot
tolerance block appends `exit 0`, so on every boot after the first the
appended correction sat past it and never ran.
81-microsoft_edge.sh (webtop only), addressing the open review comments on
PR #2920:
- apt-get/dpkg failures no longer abort cont-init; each is guarded, warns and
exits 0, and apt acquisition is bounded so a stalled mirror cannot hang
start-up
- quote ${EDGE_VERSION+x}
- gate the wrapper swap on /helpers/microsoft-edge-stable still existing, so a
second run cannot move the installed wrapper aside with nothing to replace it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(claude_desktop): inject the run-script exports after the data_location rewrite
Two findings from an independent review of the previous commit.
The `s|$DEFAULT_LOCATION|$LOCATION|g` pass over the s6-rc.d run scripts is a
blind textual substitution, and it ran after the export injection. A
data_location *under* the image default -- /config/data_kde/foo on an image
whose default is /config/data_kde -- therefore rewrote the freshly injected
`export HOME=/config/data_kde/foo` into `.../foo/foo`. Injecting after the
rewrite instead of before removes the double substitution.
Quote the injected values so a location containing whitespace cannot produce a
broken run script. XDG_CACHE_HOME stays unquoted: the loop greps for it as its
idempotence marker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(webtop,webtop_kde,claude_desktop): address review feedback on the Selkies env fix
Refresh the injected run-script exports on every boot (codex, coderabbit).
They were guarded by the XDG_CACHE_HOME sentinel, so they were written once and
then survived in the writable layer. Raising PUID left every service exporting a
/run/user/<old-uid> the remapped abc user cannot use, and clearing a custom
CUSTOM_WS_PORT left Selkies on the old port while 90-ingress.sh moved nginx back
to 8082 -- with the envdir written at the same boot disagreeing with both. The
exports now sit in a marked managed block that is stripped and rewritten each
boot, mirroring how the ~/.bashrc block in the same script already works. The
sweep also removes the bare exports earlier versions wrote, so an upgraded
container cannot end up with two sets. No upstream run script in these images
sets any of the five, so it only ever removes our own.
Validate CUSTOM_WS_PORT once, where it enters (coderabbit). It is interpolated
into generated shell and into a sed replacement, so a non-numeric or
out-of-range value could corrupt a run script or the nginx config. 90-ingress.sh
repeats the check rather than trusting the envdir, so a malformed value cannot
reach the nginx config if 20-folders.sh did not get that far.
Download Edge to an mktemp path instead of a fixed /tmp/edge.deb (coderabbit).
This runs as root against a world-writable tmpfs, where a predictable name can
be pre-created as a symlink to redirect the download or swap what is installed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts
All three add-ons are built on the LinuxServer Selkies base image and had
independently drifted copies of the same startup scripts. claude_desktop's
copies carry a set of fixes the two webtops never received, so make
claude_desktop the single source and symlink the shared scripts from
webtop_kde/rootfs (which webtop/rootfs already symlinks in full).
Shared by symlink: 20-folders.sh, 21-gpu_permissions.sh, 80-configuration.sh,
90-ingress.sh and the six etc/nginx/includes files.
Kept add-on specific: everything Claude-only stays in claude_desktop
(81/82/83/84 tool installs, 85-openbox_autostart.sh, defaults/, usr/local/bin,
svc-headroom), and everything webtop-only stays in webtop_kde (90-ssl.sh,
helpers/microsoft-edge-stable, and the new 81-microsoft_edge.sh).
To make the shared scripts add-on agnostic:
- 20-folders.sh derives its default data location from the home directory the
Dockerfile baked into the abc user instead of hardcoding /data/data. That
yields /data/data on claude_desktop and /config/data_kde on both webtops,
matching each add-on's previous behaviour exactly.
- The permission_mode: bypass root guard is skipped on add-ons that do not
declare that option.
- 80-configuration.sh falls back to pip when the image does not ship uv.
- The Microsoft Edge install moves out of 80-configuration.sh into a
webtop-only 81-microsoft_edge.sh, which also absorbs the ownership fixup
that used to run in 20-folders.sh before Edge was installed and so never
matched anything.
CI: the builder's symlink-resolution step made a single pass over a
pre-computed file list, so resolving webtop/rootfs (a directory symlink)
could copy the symlinks inside it verbatim, leaving links that escape the
webtop build context. Verified on this tree: the old loop leaves 10 dangling
symlinks under webtop/. Extract it to .github/scripts/resolve_symlinks.sh,
repeat until a pass finds nothing, and run it in the PR check too, which had
no resolution step at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: guard two startup aborts found in review
Both are crash paths in code added by this PR, not hardening:
- 20-folders.sh: getent exits 2 when the user does not exist, and under
bashio's `set -o pipefail` plus the script's `set -e` that aborts at the
assignment, so the "could not read abc's home" fallback below it was
unreachable. Same trap already documented in 21-gpu_permissions.sh.
Verified: without the guard the shell exits 2; with it the fallback runs.
- 81-microsoft_edge.sh: the ownership fixup lost the `-f` guard the original
had in 20-folders.sh. Without nullglob an unmatched /usr/bin/microsoft-edge*
reaches chown as a literal and `set -e` kills container startup. Now a
nullglob array with a warning when empty.
Also make resolve_symlinks.sh fail on a broken symlink instead of deleting it.
Dropping it silently yields an image that builds clean and misbehaves at
runtime; a red build is easier to diagnose. Verified both paths: the repo as-is
resolves to 0 symlinks and exit 0, and an injected broken link exits 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: address review comments on data-location default and Edge downloads
Two findings from CodeRabbit, both correctness rather than hardening:
- 20-folders.sh rewrites abc's home in /etc/passwd further down, so re-reading
it on the next boot returned the *previously selected* location as the image
default. On a restart that reuses the container's writable layer, clearing
data_location would strand the user on their old custom path instead of
restoring the built-in one. Cache the value in /etc/.addon_image_home, which
shares the writable layer's lifetime with the edit it compensates for: a
rebuilt or recreated container starts from a pristine /etc/passwd and
regenerates it. Simulated all three cases (first boot, reused container with
a rewritten passwd, recreated container) under `set -e` + `set -o pipefail`.
- 81-microsoft_edge.sh: both curl calls were unbounded, so a stalled
packages.microsoft.com would hang cont-init.d and with it the whole add-on.
Add --fail/--connect-timeout/--max-time and skip the install with a logged
error when version discovery or the download fails. The desktop is useful
without Edge; an add-on wedged before Selkies starts is not.
Not addressed, deliberately: escaping $LOCATION/$DEFAULT_LOCATION for sed, and
validating symlink targets in resolve_symlinks.sh. Both are hardening against
inputs that are not reachable in normal use, both predate this PR, and the
maintainer has asked to prioritise usability over that class of change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(seerr): re-encode ingress query strings for the OpenAPI validator
Searches through ingress fail with a 400 from the Seerr API, which the UI
reports as "500 Internal Server Error". The same searches succeed on the
directly published port 5055.
Supervisor proxies ingress traffic with `params=request.query`
(supervisor/api/ingress.py) - an already-decoded MultiDict - so aiohttp/yarl
re-encodes the query string on the way to the add-on. yarl's safe set is far
wider than the one express-openapi-validator accepts: it emits a space as "+"
and forwards ":", "/", "@", "!", "$", "'", "(", ")", "*" and "," bare, while
the validator tests the raw, still-encoded value against
RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/
and rejects the request. That breaks most real titles - "Monsters, Inc.",
"Ocean's Eleven", "Mission: Impossible", "Mamma Mia!".
An njs handler now re-encodes exactly those characters before proxying. This
is lossless: yarl only emits them bare when they were literal characters of
the value, since anything ambiguous arrives already encoded ("+" as %2B, "&"
as %26, "=" as %3D). "&" and "=" are left alone as the query string's own
separators, and the path is forwarded byte-for-byte.
Verified against a real express-openapi-validator over all 19 characters in
RESERVED_CHARS, and diffed byte-for-byte against the previous config across
representative traffic: only query-string encoding changes.
Fixes#2906Fixes#2646
* fix(seerr): ship the njs load_module snippet and encode "?" and ";"
Addresses two review findings.
1. The njs module never loaded. .templates/ha_automatic_packages.sh moves the
rootfs /etc/nginx aside to /etc/nginx2 before installing nginx, then does
`rm -r /etc/nginx` and restores the saved tree. That deletes the
load_module snippet nginx-mod-http-js installs, so nginx aborted with
`unknown directive "js_import"` and the service's finish hook would have
shut the add-on down - all ingress dead, not just search. The image build
still passed CI because it never starts nginx. The snippet now ships in the
rootfs so it survives the swap, under the package's own filename so the two
can never both be present and double-load the module.
2. "?" arrives bare from yarl and was not encoded. It slipped through testing
because the validator strips one occurrence with `qs.replace('?', '')`
before checking, so a single "?" passes by accident and only a second one
("Who? What?") returned 400.
NEEDS_ENCODING is now derived from the validator's RESERVED_CHARS minus the
"&" and "=" separators, rather than from the characters yarl happens to emit
bare today, so it stays correct if either side changes its safe set. The added
characters are a no-op for current traffic: yarl already percent-encodes
"# [ ] ;", so Seerr receives them encoded regardless.
Verified by replaying the build-time /etc/nginx2 swap and starting nginx, which
fails without the snippet and serves correctly with it; by sending every
RESERVED_CHAR bare; and by diffing forwarded bytes against master, unchanged at
2/20 with all query-parser edge cases identical.
* style(seerr): satisfy Codacy - double quotes in njs, changelog blank lines
Clears the 11 new Info-level Codacy findings: 9x ESLint 'quotes' in
njs/ingress.js and 2x markdownlint MD022/MD032 on the changelog entry.
No behaviour change; re-verified through the build-time /etc/nginx2 swap.
install_codex_cli was non-functional: every boot logged "Verified Codex
<version> installation failed; Codex is unavailable this boot" and no binary
was ever installed.
The download, its SHA-256 verification against the GitHub-published digest, and
the extraction all succeeded. The chain broke at the final step, which validates
the candidate binary by running --version as the abc runtime user: mktemp -d
creates its directory 0700 root:root, and abc cannot traverse a root-only
directory, so exec failed with "unable to exec: Permission denied" (exit 126)
before the binary could be moved into place. Because the whole chain is a single
&&-list, that surfaced only as the generic failure warning.
Fixed by making the staging directory traversable immediately after mktemp.
Nothing secret is staged there -- the public release archive and the extracted
binary, both world-readable upstream artifacts -- and the existing cleanup()
trap still removes the directory on exit. The validation deliberately keeps
running as abc rather than root, so the binary is exercised as the identity that
will actually run it.
Reproduced and verified on a live add-on container: the same probe goes from
exit 126 to success once the mode is widened, and the fixed script now completes
the install (codex-real 0.145.0 in place, wrapper on PATH, managed config
written, no staging leftovers).
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(portainer_be): add Portainer Business Edition add-on
Adds a new `portainer_be` add-on based on the existing Portainer (CE)
add-on, requested in #873.
Business Edition has no public GitHub release tarball like CE, so the
binary and web assets are pulled from the official multi-arch
`portainer/portainer-ee` image via a multi-stage build and placed under
/opt/portainer, mirroring CE's layout exactly. All runtime scripts,
nginx/ingress config, options schema, SSL and password handling are
unchanged from CE, so behaviour is identical apart from the edition.
Users obtain a free (up to 3 nodes) Business Edition license key by
registering with Portainer and enter it in the web UI on first launch.
- config.yaml: slug portainer_be, BE image name, BE description/name
- Dockerfile: multi-stage COPY from portainer/portainer-ee (no CE tarball)
- updater.json: dockerhub source tracking portainer/portainer-ee
- apparmor.txt: unique profile name (portainer_be_addon)
- CHANGELOG/README/DOCS: BE-specific, documents the license-key step
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(portainer_be): address PR review feedback
- nginx finish: move shebang to byte 0 (leading blank line prevented S6 from
recognising the interpreter, so the finish hook could fail to tear down the
supervision tree) [Codex P2]
- ingress: tighten CSP to `frame-ancestors 'self'` to match the adjacent
X-Frame-Options SAMEORIGIN; HA ingress embeds same-origin so the panel keeps
working [CodeRabbit]
- README: correct login note (password is the configured option value, never
printed to logs); drop MD012 consecutive blank lines [CodeRabbit]
- DOCS: fix "environement" -> "environment" typo [CodeRabbit]
Skipped: nginx SSL "idempotency" finding — /etc/nginx lives in the read-only
image layer and cont-init re-renders from the pristine template on every
container start, so in-place sed edits never accumulate or need restoring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* revert(portainer_be): keep CSP frame-ancestors * for ingress compatibility
Reverts the frame-ancestors 'self' change from the previous commit. The
wildcard is required for the Home Assistant ingress iframe to embed the
Portainer UI; tightening it breaks the ingress panel. Matches the CE add-on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* claude_desktop: add optional Codex CLI with device-code login and MCP bridge
Adds OpenAI's Codex CLI to the add-on as an opt-in fourth tool, so a Claude
session can delegate work to ChatGPT Codex as an independent second agent.
Install (install_codex_cli, default off): Codex is deliberately not baked into
the image -- its Linux binary is ~310 MB extracted, which is not worth carrying
in every installation for an off-by-default option, and updating it would then
need an add-on rebuild. A new 81-codex_cli.sh downloads the pinned static-musl
release (ENV CODEX_VERSION) into /data/codex/bin instead. That prefix is outside
$HOME on purpose: the managed-MCP merge treats any command under $HOME as
user-installed and refuses to manage it. Staging happens under /data rather than
the default /tmp, which here is a RAM-backed tmpfs mounted noexec -- holding
420 MB there during boot is a risk on a small host, and the binary could not be
verified there at all. The download fails open like the Claude Desktop update
check and validates the new binary by running it before replacing the old one.
Login (codex-login): Codex's default sign-in serves an OAuth callback on
localhost:1455 and expects a local browser, which cannot work in this add-on.
The helper runs `codex login --device-auth` instead -- the flow OpenAI documents
for headless machines -- printing a URL and one-time code to approve elsewhere.
It drops to the abc runtime user first so auth.json is not created root-owned.
MCP (codex mcp-server): registered through the existing managed-MCP merge rather
than a second copy of it, so it inherits that code's idempotence, no-clobber and
removal-when-disabled behaviour. A managed CLAUDE.md block explains when a second
agent is worth the round-trip.
New codex_sandbox_mode (default danger-full-access) is applied both as -c
overrides on the MCP command and as a managed block at the top of
~/.codex/config.toml; Codex's own Landlock/bubblewrap sandbox is unreliable
inside the container, which is already the security boundary.
Verified against the real 0.145.0 binary: tools/list returns `codex` and
`codex-reply` (hyphen, not the underscore upstream docs report), an invalid
-c sandbox_mode is rejected by name, the installer lifecycle behaves correctly
on re-run and on a bad pin, and the device code is flushed within seconds while
still polling, which is the non-TTY case that matters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* claude_desktop: harden Codex subscription MCP setup
* claude_desktop: use runtime home for Codex login
* claude_desktop: reconcile runtime user home ownership
* claude_desktop: report verified Codex subscription setup
* claude_desktop: track latest Codex at runtime
* claude_desktop: document subscription-only Codex MCP
* claude_desktop: enforce Codex runtime identity
* claude_desktop: persist Codex in runtime home
* claude_desktop: prevent Codex auth override bypass
* claude_desktop: default Codex to workspace write
* claude_desktop: redact Codex authentication diagnostics
* claude_desktop: document safer Codex MCP defaults
* claude_desktop: validate Codex candidate as runtime user
* claude_desktop: align Codex sandbox fallback
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bazarr's config.yaml carries a base_url key under general: (Bazarr's own
ingress path) AND a separate base_url under each configured *arr integration
-- radarr.base_url, sonarr.base_url, etc. -- which is how Bazarr reaches
those services at their own ingress-prefixed URL.
Every base_url sed in this addon was unscoped:
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
sed applies s/// to every matching line in the file, not just the first, and
" base_url:.*" matches any 2-space-indented base_url line regardless of
which top-level section it's under. Since general.base_url, radarr.base_url,
sonarr.base_url etc. all sit at that same indent, this rewrote all of them to
Bazarr's own value on every container start (32-nginx_ingress.sh) and again
in the run script's fallback -- silently breaking Bazarr's configured
connections to Radarr and Sonarr.
Scope each sed to the general: block only, reusing the range idiom this file
already uses to scope the auth: block's type: substitution:
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" ...
Verified against a representative config.yaml (general/radarr/sonarr/subsarr
sections, including general:'s list-style provider entries) for all three
connection_mode branches plus the run script's fallback: general.base_url is
the only line touched in every case; radarr.base_url and sonarr.base_url
survive with their original values.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix Bazarr ingress: keep redirects relative so they aren't blocked as mixed content
Opening the Bazarr panel over HTTPS failed with:
Mixed Content: ... requested an insecure frame
'http://<ha_host>:8099/bazarr/'. This request has been blocked
Bazarr is Flask-based and answers /bazarr (the ingress entry, which has no
trailing slash) with a redirect to /bazarr/, made absolute against the Host
nginx sends upstream -- http://127.0.0.1:6767/bazarr/. proxy_redirect's
implicit "default" rule strips that prefix, which makes nginx treat the
Location as its own; the header filter then re-absolutises it as
$scheme://$host:$server_port/... Since $host is the browser's host forwarded
by the Supervisor and $server_port is the ingress port (8099, the Supervisor
default as no ingress_port is declared), the result is a plain-http URL on a
port the browser refuses to frame from an https page.
absolute_redirect off keeps the Location relative, and the proxy_redirect
rules re-prefix it with the ingress entry so it resolves under
/api/hassio_ingress/<token>/. The second rule also covers backends that emit
an already-relative Location; external absolute redirects match neither rule
and pass through untouched.
Verified against a local nginx with a stand-in backend: the pre-fix config
reproduces http://<host>:<ingress_port>/bazarr/ exactly, and the fixed config
returns /api/hassio_ingress/<token>/bazarr/ for both absolute and relative
upstream Locations while leaving an external redirect alone.
Also fixes the fallback base_url in services.d/nginx/run, which wrote it
without the leading / and so reintroduced the startup crash fixed in 1.5.6-4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Tighten base_url guard in nginx run script to require the leading slash
CodeRabbit review on #2910: the guard `grep -q "base_url.*$slug"` matches
both "base_url: bazarr" and "base_url: /bazarr" -- the .* swallows the slash
-- so it treated the malformed no-slash form as already correct and never
triggered the repair. Require the literal "base_url: /$slug" instead, so a
config missing the slash is actually detected and fixed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The AI fix sweep has never completed a non-empty batch. claude-code-action
rejects track_progress unless the triggering event is pull_request, issues,
issue_comment, pull_request_review_comment or pull_request_review, and
daily_ai_fix.yaml only ever runs on schedule or workflow_dispatch. The step
died in input validation after 0.3s:
Action failed with error: track_progress is only supported for events:
pull_request, issues, issue_comment, pull_request_review_comment,
pull_request_review. Current event: workflow_dispatch
This went unnoticed because the step is gated on `count != '0'`, so every
run with an empty batch skipped it and reported green. Every run that
actually had issues to work through failed identically (runs 30265277395,
30011599875).
daily_ai_fix.yaml: remove it — no trigger of this workflow can ever satisfy
the constraint. Claude still comments per issue via gh, as issue-fix.md
instructs; only the run-level sticky comment is lost.
on_issue_approved.yaml: same latent failure on its workflow_dispatch path,
but the `issues` path is valid, so gate it on the event instead of dropping
it. The action defaults this input to the string "false", so the expression
result is a shape it already handles.
Co-authored-by: Claude <noreply@anthropic.com>
Every claude-code-action step except on_claude_mention.yml left the github_token
input unset, so the action fell back to the OIDC -> Claude App token exchange.
That exchange requires github.actor to have write access on the repo; on an
issues.opened event the actor is the outside reporter, so it always 401'd.
Classify is continue-on-error, so the job went green while doing nothing.
Setting the input short-circuits the exchange (action.yml maps it to
OVERRIDE_GITHUB_TOKEN; token.ts returns it before requesting OIDC).
GITHUB_TOKEN for the read-only classifier; AI_PR_TOKEN for the three that push
branches or open PRs, so the resulting PR triggers CI.
Also dropped the workflow-level id-token: write grant, which is unreachable once
github_token is set (CodeRabbit).
Keep stderr from the dotenv env-file validation so a failure shows which line
broke instead of just "not valid shell".
Co-Authored-By: Claude <noreply@anthropic.com>
build.json named the multi-arch collabora/code:latest for both architectures.
The builder never passes --platform -- it runs each architecture on its own
native runner -- so BUILD_FROM is the only thing selecting which binaries end
up in the add-on. That resolves correctly today only because the runner
architecture happens to match the target. Name collabora/code:latest-amd64 and
collabora/code:latest-arm64, which are published in lockstep with latest.
The official image sets file capabilities on two binaries, and COPY --from does
not carry extended attributes, so they arrived stripped:
coolforkit-caps cap_chown,cap_fowner,cap_sys_chroot=ep
coolmount cap_sys_admin=ep
Without them coolwsd starts and serves the admin console, but cannot chroot a
kit process, so no document ever opens. Reapply and verify them.
Replace the smoke test, which is why the build is currently red: coolwsd
refuses to run as root (exit 78), and --version does not exit anyway, since the
official entrypoint passes it to the long-running server. Check instead that
every binary resolves its libraries against the Debian base.
For ssl: true, hand Collabora the certificate copies in /etc/coolwsd rather
than /ssl. coolwsd runs as uid 1001 and a private key in /ssl is commonly
root-only, so it could not be read; the copies were already being made and
chowned, but nothing pointed at them.
Co-Authored-By: Claude <noreply@anthropic.com>
The base image already sets it, but hadolint cannot see an inherited SHELL
(DL4006), and the ldd linkage check relies on pipefail to notice a failing ldd.
Co-Authored-By: Claude <noreply@anthropic.com>
"coolwsd --version" does not short-circuit: it runs a full initialisation and
tries to create a jail, which fails in a build layer because the --o: paths the
launcher passes are absent, so it looked for /usr/bin/jails. It did prove the
binaries link against the Debian base, but booting Collabora is the wrong check
for a build step.
ldd asserts the same thing directly: every NEEDED library of coolwsd,
coolforkit-ns and coolmount resolves on this base. The loop uses an if rather
than "grep && exit 1" so that a clean result does not leave the loop with
grep's non-zero status and fail the good case.
Co-Authored-By: Claude <noreply@anthropic.com>
coolwsd refuses to start as root, so "coolwsd --version" failed the build even
though it proved what it was there to prove: the payload copied out of the
distroless image links and executes on the Debian base. Run it through su as
uid 1001, which is also exactly how 99-run.sh launches it.
Also drop --system from the useradd/groupadd, which only produced a
"uid 1001 is greater than SYS_UID_MAX 999" warning.
Co-Authored-By: Claude <noreply@anthropic.com>
build.json is where every add-on in this repo records the upstream image it
tracks, and it is what the updater bot rewrites. Hardcoding the Collabora tag in
the Dockerfile and putting the Debian base in build.json inverted that.
BUILD_FROM is now collabora/code:latest again and feeds the build stage the
payload is copied from; the Debian runtime base is named in the Dockerfile,
where it is an implementation detail of the add-on rather than the upstream
being tracked.
Co-Authored-By: Claude <noreply@anthropic.com>
dotenv_quote emits a double-quoted value, and both files are read back by
sourcing them from a shell: browserless_chrome does "set -a; . /.env" from its
Dockerfile, wger copies /.env into /data/env.sh as export lines, and
fireflyiii_data_importer relies on /etc/environment for cron. Inside double
quotes $ and ` are still special, and neither was escaped, so the value was
expanded rather than read literally:
pa$$w0rd came back as pa904869w0rd (the shell PID)
${HOME} came back as /root
back`tick` ran tick as a command and kept only "back"
Escape both, after the existing backslash doubling so the added backslashes are
not doubled in turn.
Extend --self-test to cover this path as well: it now writes an env file, checks
it parses, sources it and compares. An unescaped backtick makes the file
unparseable, which would take the sourcing shell down with it, so that case is
reported rather than left to abort the run.
Values containing a real newline remain out of scope: dotenv_quote writes them
as a literal \n, which a dotenv parser unescapes but a shell does not.
Co-Authored-By: Claude <noreply@anthropic.com>
Upstream rebuilt collabora/code as a Nix distroless image between 26.04.2.1.1
(2026-07-01) and 26.04.2.2.1 (2026-07-18): /bin and /sbin are empty and the
entrypoint is coolwsd itself. It can no longer serve as BUILD_FROM, since every
RUN, s6-overlay and bashio need a shell. The addon build has been failing since,
which is independent of the option fixes in this branch.
Collabora is now taken from the official image as a build stage and copied onto
ghcr.io/hassio-addons/debian-base:
- Only the payload is copied: /usr/bin/cool*, /usr/share/coolwsd, /etc/coolwsd,
/opt/collaboraoffice and /opt/cool. /etc and /nix are deliberately left out:
in the distroless image /etc/resolv.conf, /etc/hosts, /etc/passwd, /etc/group
and /etc/nsswitch.conf are symlinks into /nix/store, so importing them would
break DNS and wipe the base image users.
- coolwsd links only against glibc, libstdc++, libgcc and libm, and needs at
most GLIBCXX_3.4.22, so the Debian base satisfies it; the office engine
bundles its own cairo, fontconfig, curl, icu and fonts. Only openssl,
fontconfig, libcap2-bin, cpio, findutils and ca-certificates are installed.
- The uid/gid 1001 cool user is recreated, matching the official image.
- /start-collabora-online.sh is gone, so the addon ships an equivalent launcher
which also regenerates the self-signed certificate when ssl is off.
- The build now runs "coolwsd --version" so a payload that cannot link fails the
build instead of shipping an image that will not start.
Co-Authored-By: Claude <noreply@anthropic.com>
00-global_var.sh turns every addon option into an "export KEY='value'" block
that is injected at the top of cont-init scripts, service run scripts and the
shells, so the quoting has to survive an eval byte for byte.
Two defects sat on that path and hid each other:
- shell_quote replaced ' with '"'"' followed by a stray space, so a value like
"O'Brien pass" reached the application as "O' Brien pass". Passwords and any
option holding an apostrophe were silently wrong.
- shell_quote also doubled every backslash, and append_export then passed the
result through "awk -v", which runs its own escape processing and halved it
again. Backslash values (regexes, Windows and UNC paths) therefore survived
by accident, and fixing either half alone breaks them: dropping the doubling
leaves awk eating \t, \b and \\, while keeping it doubles the value for real
once awk is gone.
shell_quote now applies the POSIX rule (only ' needs escaping, as '\'') and
append_export appends the line directly instead of going through awk, which
also drops a full rewrite of the block per option.
Add a --self-test that builds a real export block and sources it, so the check
covers the whole path rather than either helper in isolation -- testing them
separately is exactly what let this pair stay wrong:
bash .templates/00-global_var.sh --self-test
Reported in #2768. dotenv_quote is left alone: its output is double-quoted, so
the doubling it does is correct there.
Co-Authored-By: Claude <noreply@anthropic.com>
Reported in #2768: several users could not get Collabora to talk to
Nextcloud, and the two options meant to configure it had no effect.
- 99-run.sh read a `domain` option that does not exist in the schema (the
option is `domain1`), and recent Collabora releases dropped the `domain`
environment variable entirely, so `domain1` was inert. It now maps to
`server_name` with a deprecation warning.
- `server_name` and `cert_domain` were in the schema but never passed to
Collabora. `server_name` is what fixes "Your browser has been unable to
connect to the Collabora server" behind a reverse proxy.
- `aliasgroup*` entries are matched by Collabora as regular expressions, so
a dot needs a single backslash. The README asked for two, which can never
match a real hostname. Values are now normalised (unescaped, escaped and
double-escaped all give the same correct pattern) and logged at startup.
Values containing other regex metacharacters are left untouched.
- Added `ssl_termination`, needed when `ssl` is false but Collabora is
reached over https through a reverse proxy, and `aliasgroup2`/`aliasgroup3`.
- `cert_domain` is a certificate common name, so it is a string, not a bool.
- Releases on CollaboraOnline/online are now Helm charts only, which had
renumbered the addon from 25.4.9.2 down to 1.3.0 and hid updates from the
Supervisor. Version tracking moves back to the collabora/code Docker Hub
tags.
Co-Authored-By: Claude <noreply@anthropic.com>
The HEALTHCHECK branched on "$ssl", but that variable is never present
in the container environment: Supervisor only injects the environment:
block from config.yaml (FB_BASEURL, PGID, PUID) plus TZ/SUPERVISOR_TOKEN.
The ssl option lives in /data/options.json and is read via bashio inside
cont-init, and HEALTHCHECK CMD is spawned by dockerd, so no export from
that shell can ever reach it.
The test was therefore always false and the healthcheck kept probing
http:// against the TLS listener, producing the
http: TLS handshake error ... client sent an HTTP request to an HTTPS server
spam reported in #2881.
Write the resolved protocol to /run/health_protocol from 99-run.sh and
read it back in the healthcheck, matching the pattern already used by
the gitea addon. Also corrects the 127.0.01 typo (missing octet).
Co-Authored-By: Claude <noreply@anthropic.com>
Opus 5 released 2026-07-24: same price as 4.8, both effort levels already
used here (xhigh in the tier-2 sweep, high in the tier-3 executor) remain
supported. Swap --model claude-opus-4-8 -> claude-opus-5 in the two Opus
steps; Sonnet-low tiers (classify, @claude, CodeRabbit follow-up) untouched.
Auto-merge for AI PRs was considered and declined -- keeping the existing
ready-PR-requires-manual-merge behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ingress responses were re-gzipped by nginx (default gzip_types includes
text/html), dropping Content-Length and forcing a chunked/streamed
response. That pushes both Supervisor and Core's ingress proxy out of
their buffered relay path into the streaming path, where an
aiohttp-side error surfaces to the browser as a 502 Bad Gateway even
though the addon's own nginx logs a 200. Disabling gzip on the ingress
server block keeps responses identity-encoded with an intact
Content-Length so the relay uses the simpler, more robust buffered
path.
Fixes#2766
- A (security): authenticate the ai-plan comment selector — only accept a
plan from a trusted author (OWNER/MEMBER/COLLABORATOR), so a reporter can't
inject a plan that executes on approval. Fail-safe to no-plan otherwise.
- B: clear ai:approved in the no-plan branch so a later real plan can be
re-approved (re-adding a present label fires no labeled event).
- C: claim ai:needs-info via a live re-check inside the serialized job so
queued reporter replies can't each run a classification; restore the flag if
no verdict was produced so the issue doesn't drop out of the retry path.
- D: gate workflow_dispatch of the tier-3 executor to github.actor == alexbelgium.
- E: exempt ai:approved from the stale bot.
- F: filter catch-up candidates server-side (search) instead of capping at the
100 newest issues, so older untriaged issues aren't silently missed.
- G: run ai_guard_paths.sh from the trusted default-branch copy, not the
in-tree copy a job could have modified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Redesign the AI issue-triage pipeline so it can fix confidently on its own,
ask for approval only when unsure, and always yield to manual actions — while
staying cheap and fast.
- Tier 2 (daily_ai_fix) becomes graded: high-confidence small fixes open a
READY-for-review PR; anything medium/large gets a full Opus-written plan
comment (ai:plan-pending) instead of a PR.
- New Tier 3 (on_issue_approved): maintainer adds ai:approved and the posted
plan is executed on Opus into a ready PR — immediate, zero cost until asked.
- New @claude interactive workflow (on_claude_mention): maintainer-only,
Sonnet-low, full precedence over the automated tiers.
- New CodeRabbit follow-up (on_pr_coderabbit): one-shot Sonnet pass that fixes
or replies to CodeRabbit's review of an ai-fix/* PR.
- Tier 1 self-healing: a reporter's reply to a needs-info request re-runs
classification exactly once (ai:needs-info); a daily catch-up re-dispatches
any issue that never got triaged.
- Extras: global kill switch (repo var AI_DISABLED), stale-bot exemption for
AI labels, shared ai_guard_paths.sh, track_progress on the Opus tiers,
CLAUDE.md documentation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified: gh issue edit --add-label=... is purely additive, and the
"owned" branch exited without touching labels at all — so a manual
workflow_dispatch re-triage that changes the verdict (e.g. a prior
addon-bug run now comes back needs-info, upstream-bug, or owned) left
the old ai-triage label in place, and daily_ai_fix.yaml would still pick
the issue up for the unattended fix pass despite the fresh verdict.
Both label-applying paths now also remove whichever of
ai-triage/ai:classified/ai:needs-human this run did NOT re-apply, as a
separate best-effort call that can't block the add. Simulated every
verdict/confidence transition, including the reported case (addon-bug ->
needs-info): ai-triage is now correctly removed instead of left stale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two findings from Codex review of #2899, both live now that DRY_RUN is gone:
- A low-confidence `addon-bug` had ai:needs-human set by the low branch and
then ai-triage appended right back unconditionally, so it would enter the
unattended tier-2 fix pass despite Rule 2 saying an uncertain call should
only flag a human. Guard the ai-triage add on CONF != low.
- The label-create loop used `--force`, which updates existing labels; with
a model-supplied cosmetic label like `bug` that already exists, triage
recolored it to ededed as a side effect. Drop `--force` so existing labels
are left untouched (create fails harmlessly via || true) while missing
ones are still created.
tier 2's own label step keeps --force intentionally: its list is a fixed
set of workflow-owned ai:* labels meant to be gray, not model input.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a workflow_dispatch trigger with a required `issue` input to
on_issues_ai_triage.yaml, so a specific (existing) issue can be triaged
on demand instead of only on issues.opened.
- Every issue-number reference now reads
`github.event.issue.number || inputs.issue`, so it resolves from the
event on the auto path and from the input on manual dispatch.
- The job's auto-trigger guards (skip the maintainer's own issues, honour
no-ai) are bypassed on workflow_dispatch — a manual run is a deliberate
override.
- The 60s ping_submitter wait is skipped on manual dispatch; there's no
race to lose against an issue whose ping already landed.
The input flows only through env vars and expression contexts, never
inline into a run: block, so there's no shell-injection surface; a bad
number just fails `gh issue view` cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A manual AI fix sweep failed with "Could not fetch an OIDC token. Did you
remember to add id-token: write to your workflow permissions?". The action
mints a GitHub OIDC token to authenticate the claude_code_oauth_token flow,
which needs id-token: write — absent from both jobs' permissions. Tier 2
failed on it now; tier 1 would have failed identically the first time it
ran live. Added to both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- on_issues_ai_triage.yaml: remove DRY_RUN entirely. AI_PR_TOKEN is now
configured, tier 1 has been watched in dry-run, and the toggle was
meant to be temporary scaffolding, not a permanent code path — verdicts
now apply labels/comments unconditionally.
- daily_ai_fix.yaml: fold ai:blocked into the existing "ensure labels
exist up front" step (renamed to reflect that). It was the one control
label neither workflow ever created: the forbidden-paths guard applies
it directly, and under set -euo pipefail a missing label there aborts
that step's loop entirely, silently skipping every remaining PR behind
the one that failed. No repo had hit this yet only because no label in
the ai:*/ai-* namespace existed at all before now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified each against current code before fixing; verification details are
in the PR description update.
Fixed:
- issue-classify.md: Rule 0 now requires the addon-submitter-ping marker to
appear in a comment headed "### @github-actions[bot]", not just anywhere
in a comment or issue body, so it can't be spoofed to suppress triage.
- ai_triage_context.sh: separator-insensitive addon-slug matching (fixes
"Calibre-web" -> calibre_web, and the earlier ImmichFrame -> immich_frame
miss) before falling back to substring matching; sparse-checkout failure
now surfaces "UNRESOLVED" into the bundle instead of silently proceeding
addon-less; duplicate-issue search excludes the issue being triaged from
its own candidate list.
- on_issues_ai_triage.yaml: persist-credentials: false on the read-only
tooling checkout (nothing in that job pushes); both actions pinned to
commit SHAs (Dependabot already covers github-actions repo-wide, and
on_issues_ai.yml already sets this precedent for another AI action);
model-supplied labels are now filtered to drop anything in the ai-*/ai:*
control namespace before merging with the deterministic ai-triage/
ai:classified additions, closing a path where a verdict could
self-trigger tier 2 regardless of its actual classification.
- daily_ai_fix.yaml: both actions pinned to the same commit SHAs;
workflow_dispatch inputs.issue/inputs.limit moved out of direct
${{ }} interpolation in the run: script and into env vars with numeric
validation (template-injection); Guard forbidden paths' PR listing
limit raised 50 -> 300 so it can't silently drop ai-fix/ PRs behind
unrelated open PRs before the branch-name filter applies.
Skipped (reasons in PR description):
- persist-credentials on daily_ai_fix.yaml's checkout: disabling it
breaks the only auth path git push currently uses, and the same
AI_PR_TOKEN is already directly readable via GH_TOKEN env by that job's
unrestricted Bash(git:*)/Bash(gh:*) tools regardless.
- Splitting untrusted AI analysis into a separate job from PR-creation/
write access: legitimate defense in depth, but a full architecture
redesign, not a minimal fix.
- Full hard-limit enforcement (config.yaml immutability, diff caps,
draft-only status) replicated at the workflow level: heavy lift: the
prompt already covers these as Claude-followed instructions; only the
protected-paths check is duplicated as deterministic enforcement,
which is the single highest-severity one to enforce outside the model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that tier 2 runs daily instead of weekly, an issue left carrying
ai-triage after a sweep would be re-selected and fully re-analysed the very
next morning, before there's been a working day to review the first PR.
Nothing previously dropped issues out of the ai-triage backlog once handled.
- issue-fix.md: new hard limit 6 — relabel every issue as the last action
before moving to the next one. ai:fixed / ai:upstream / ai:needs-human
replace ai-triage depending on outcome.
- daily_ai_fix.yaml: pre-create the three replacement labels once, up front
(Claude never has to improvise a color or retry a missing-label error —
wasted turns multiplied by batch size). Add a "Guard against repeat
processing" step, same belt-and-braces pattern as the existing forbidden-
paths guard: force-relabel to ai:needs-human anything the batch still
finds carrying ai-triage after the run, independent of whether Claude's
own relabeling succeeded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Swap anthropic_api_key -> claude_code_oauth_token in both Claude steps,
reading CLAUDE_CODE_OAUTH_TOKEN from the CR_PAT GitHub Environment. Both
jobs now declare `environment: CR_PAT` so the environment-scoped secret
is reachable.
- Rename weekly_ai_fix.yaml -> daily_ai_fix.yaml (matches this repo's
daily_/weekly_ filename convention) and change its cron from
"0 3 * * SUN" to "0 3 * * *".
- on_issues_ai_triage.yaml already triggered on issues.opened; no schedule
change was needed there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Require repo-owner authorship or the `ai: fix-approved` label before Codex
runs on existing-add-on improvements, mirroring the bug path. Closes the
cost/abuse vector where any external user could auto-trigger expensive Codex
runs and draft PRs.
- Pin openai/codex-action to a commit SHA (was the mutable @v1 tag) since it
receives OPENAI_API_KEY.
- Add a catch-all failure reporter to publish_fix so apply/push/PR-create
failures notify the issue and swap labels instead of failing silently.
- Reject creation of new top-level files in the patch validator (previously
only new directories were blocked).
- Update triage comment wording and README to match the new gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Install tier-1 classifier (on_issues_ai_triage.yaml) and tier-2 fix sweep
(weekly_ai_fix.yaml) plus their helper script and prompts.
Pre-merge fixes from verification:
- Wait-for-ping sleep 150s -> 60s. on_issues_ping_submitter completes in
6-11s of job time across the last 10 runs; 60s covers runner-queue skew
with margin.
- Rule 0 rewritten to match the real ownership signal: ping_submitter posts
a github-actions[bot] comment with a stable marker
`<!-- addon-submitter-ping:<addon> -->`. Rule 0 now keys off that literal
marker instead of fuzzy prose, and guards against @<user> == alexbelgium.
- Silence one intentional shellcheck SC2016 (literal Markdown backticks) so
actionlint runs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 21-gpu_permissions.sh: distinguish the expected getent exit 2
(unnamed GID) from other getent failures, warning instead of
silently masking unrelated NSS/database errors.
- SIGN_IN.md: state explicitly that --password-store=basic trades
away OS-backed at-rest protection.
Addresses CodeRabbit nitpicks from the PR review. The symlink-following
concern on 85-openbox_autostart.sh raised by CodeRabbit and Codex is left
open for maintainer review (see PR comment).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Session logs on a live install showed safeStorage unavailable (no keyring daemon
behind the forced gnome-libsecret store) causing recurring "sign in again" prompts,
and the resulting stale session parking the cowork/dispatch bridge — surfacing as
the desktop showing offline in the Claude app when opened from mobile first.
Switch to --password-store=basic (no daemon, no first-boot prompt) and sync the
persistent openbox autostart from the image on every boot so the fix reaches
existing installs, not just fresh ones.
Also fix 21-gpu_permissions.sh exiting 2 at boot: getent's expected exit-2 for an
as-yet-unnamed DRI group, combined with bashio's pipefail + set -e, skipped the
script's own unnamed-group fallback before it could run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the obsolete Yarn startup path with direct Linkwarden runtime commands, run the worker directly, and release add-on version 2.15.1.2 with backup guidance.
Codacy flagged 2 new markdownlint issues against the repo's 0-max gate:
the new heading and its following bullet list need a blank line between
them, matching the spacing every other CHANGELOG entry already uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI failed the build: apt refused to install docker.io because it pulls
in Debian's containerd/runc, which Conflicts with the containerd.io
already installed by the base image's own Docker-in-Docker support
(docker-ce + containerd.io from Docker's apt repo, toggled by the
pre-existing START_DOCKER env var — the reason that option existed
before this PR). qemu-system-x86 and ovmf are unaffected and stay.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds qemu-system-x86, ovmf, and docker.io (Bookworm main) plus virtiofsd
for sharing the workspace into the sandbox microVM. virtiofsd has no
Bookworm/backports package and its trixie .deb would GLIBC-mismatch the
runtime, so it's built from the pinned crates.io release in a dedicated
builder stage, mirroring the existing rtk/tokensave pattern.
Also updates the repo versioning convention in CLAUDE.md: local patch
counters should use a dot (X.Y.Z.N) instead of a hyphen (X.Y.Z-N), since
the hyphen form parses as a semver pre-release and Supervisor won't offer
the update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The healthcheck CMD hardcoded http://, but 92-ssl.sh switches Caddy to
https://:8081 when ssl=true, so the probe would fail with the wrong scheme.
Select the scheme from the ssl env var and pass -k (the probe hits
127.0.0.1, not the certificate's real name).
Also replace `&>/dev/null` with `>/dev/null 2>&1`. HEALTHCHECK's shell form
runs under /bin/sh, which in this image is dash, not bash. Dash parses
`cmd &>/dev/null` as `cmd &` (backgrounded) followed by a separate no-op
`>/dev/null`, discarding curl's exit status entirely -- so the healthcheck
always reported healthy regardless of whether the WebUI actually responded.
Verified under dash directly: no listener -> exit 1, http server -> exit 0,
forced scheme mismatch -> exit 1, https with self-signed cert + -k -> exit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Standalone Docker (no Supervisor) disables nginx and serves the WebUI via
Caddy on 8081; nothing listens on port 80 when ssl=false, so the Docker
HEALTHCHECK failed and the container reported "unhealthy" although it worked.
Point HEALTH_PORT at 8081. HA mode is unaffected (Supervisor ignores Docker
health-checks).
Also drop rootfs/etc/sudoers.d/birdnet-abc-systemctl. A chroot boot harness
against the published image confirmed the standalone container already boots
and serves the WebUI (HTTP 200), and that nothing ever runs as the `abc`
user: pi has `NOPASSWD:ALL` and caddy has NOPASSWD:ALL via the pre-existing
010_caddy-nopasswd drop-in, so the abc rule was valid but inert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review feedback on #2888 without widening the revert scope
back to a moving target:
- prebuild-sanitize's own [nobuild] commit lands on top of HEAD_SHA
within the same run, so before..HEAD_SHA doesn't include it. It's
still this push's own fallout, not a neighboring push's, so capture
its SHA via job output and revert it explicitly (reverted first,
since it's on top).
- git rebase in the push-retry loop could conflict and get killed
silently by set -e, burning the remaining retry attempts. Abort the
rebase and fail loudly instead.
revert-on-failure re-fetched master and reverted before..HEAD, but HEAD
was the live tip of master, not the head of the failing push. When the
updater bot pushes one addon per commit in quick succession, a single
addon's build failure would sweep in every successful commit pushed
while the revert job was still running and revert them too.
Revert before..github.sha instead, and retry the push with a rebase in
case master moves again before we push.
Confirmed on the live run: a depth-1 (default) shallow checkout of the
merge commit truncates parent refs at that boundary commit entirely, so
git rev-parse HEAD^1 fails with "unknown revision" even though the merge
commit itself is checked out fine. Bumping this job's checkout to
fetch-depth: 2 pulls in both the merge commit and its two parents, making
HEAD^1 resolvable with a real tree to diff against.
github.event.pull_request.base.sha is fixed at the time the triggering
event fired. In a repo with frequent direct-to-master pushes, master can
advance between that event and job checkout, while the actions/checkout
merge commit (github.sha) is always built against the *current* master
tip. Diffing the stale event SHA against the live merge commit picked up
unrelated upstream commits — observed live on this PR: scrutiny and
scrutiny_fa showed up as "changed" and failed their changelog check, even
though this PR only touches the workflow file.
HEAD^1 is the actual base the checked-out merge commit was built from
(verified: parents are [live master tip, PR head]), so it can't go stale.
- Stop masking git fetch/diff failures with a blanket `|| true`. That
swallowed real errors (bad ref, network failure) into an empty
changed_addons result, the same silent-skip failure mode this PR
exists to fix. Capture the diff separately from the grep filter so
`|| true` only covers grep's expected "no match" exit code, while
fetch/diff failures now abort the job via the runner's default
`set -eo pipefail`.
- Write changelogs_files using the GITHUB_OUTPUT multiline delimiter
syntax instead of a plain `key=value` echo. A PR touching more than
one addon's CHANGELOG.md produced a value with embedded newlines,
which corrupts the output file under the single-line format.
Addresses review comments from coderabbitai and chatgpt-codex-connector
on PR #2887.
github.event.before is only populated on push events, but this workflow
triggers on pull_request, where it's empty. This made every git diff call
fail (fatal: ambiguous argument) and changed-addons resolve to [], so
addon linting, build testing, and changelog checks were silently skipped
on every PR regardless of what changed.
Use github.event.pull_request.base.sha instead, which is always populated
for pull_request-triggered runs.
The updater tracks elastic/elasticsearch GitHub tags and bumps as soon as a
tag appears, but the build pulled `FROM elasticsearch:<v>` (the Docker Hub
`library/elasticsearch` mirror), whose arm64/aarch64 tag lags hours behind
the release. This broke the aarch64 build right after "updated to 8.19.19"
(and would recur on every release): docker.io/library/elasticsearch:8.19.19
had only linux/amd64 at build time, no arm64.
Switch to Elastic's own registry, which publishes the multi-arch image
atomically with the GitHub tag the updater watches. The image is otherwise
identical (same User 1000:0, tini entrypoint, eswrapper cmd), so the
entrypoint patch and uid handling are unchanged, and the updater's blanket
version sed over the Dockerfile is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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
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
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>
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>
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>
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>
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>
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>
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
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
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
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>
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>
- 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>
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>
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>
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>
Remove standalone web terminal (ttyd/tmux service, port 7681, terminal_*
options, claude-direct/claude-headroom wrappers). Claude Code stays and
powers Desktop cowork/dispatch sessions.
Fix Headroom dashboard: proxy bound 127.0.0.1 only, mapped port 8787
refused external connections; bind 0.0.0.0.
Fix dispatch/sign-in persistence: gnome-keyring package was never
installed, so the autostart keyring bootstrap no-oped and Electron
safeStorage was unavailable (allowlist cache + auth grants lost).
Add tokensave MCP (pinned 7.2.0, source-built like RTK), real HA MCP
bridge via mcp-proxy (enable_ha_mcp + ha_mcp_url/ha_mcp_token), uv for
additional_pip. Register managed MCP servers in Desktop and Claude Code
configs without clobbering user entries. Drop orphan options
ha_smart_context/dangerously_skip_permissions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The published 8.19.18 images are correct (verified: real ES 8.19.18,
run as root, migration + privilege-drop in place). But some upgrades
were left running a stale cached Elasticsearch 7.17.9 image that starts
as uid 1000, producing the reported "mv: cannot move '/data/config' ...
Permission denied" and "AccessDeniedException[.../data/nodes/0]".
- Bump version to 8.19.18-3 to force Home Assistant / Docker to pull a
fresh image tag instead of reusing the cached one.
- Add an explicit root check on the first init pass (before any move or
chown) so a non-root start fails with a clear, actionable message
instead of the cryptic permission error, and wrap the config-archive
mv with the same clear failure. The re-exec'd uid-1000 pass returns
before this check, so the privilege drop still works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Elasticsearch refuses to bootstrap as root ("can not run elasticsearch
as root"). The previous fix in this PR kept the container root at
runtime to fix the /data permission failure, but never dropped
privileges again afterward — unlike 7.17.9, whose own entrypoint used
`chroot --userspec=1000:0` before launching Elasticsearch, the upstream
8.x entrypoint no longer does that. So every start, fresh or upgrade,
would fail once addon-init.sh's setup finished.
Fix: after addon-init.sh completes its root-only work (migration guard,
data/config relocation, chown), it re-execs the entrypoint itself as
uid 1000 via `chroot --userspec=1000:0 / ...` — the same mechanism
7.17.9 used, and exactly what the add-on's AppArmor profile already
grants (sys_chroot, setuid, setgid). On the re-exec'd pass the script
returns immediately (guarded by an exported sentinel) so none of the
setup work repeats; exported env vars (env_vars, the security default)
survive the exec normally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reported: on upgrade from an existing 7.17.9 install, the add-on failed
to start with "mv: cannot move '/data/config' to
'/data/config.bak-7.17.9': Permission denied".
Root cause: a previous fix in this same release restored `USER 1000:0`
at the end of the Dockerfile to match the upstream base image's own
final USER directive. But the upstream 8.19 entrypoint no longer drops
privileges itself (confirmed: it execs elasticsearch directly, no
gosu/chroot dance), and existing installs have /data owned by root
(7.17.9's default image variant runs fully as root). A non-root
container can never chown or move that data.
Revert to root at runtime, matching how this add-on always ran and
matching its own AppArmor profile (chown, setuid, setgid, sys_chroot,
mount capabilities — all meaningless for a non-root process anyway).
Root stays required for the build-time entrypoint patch too, unchanged
from the prior fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
curl -f treated any 4xx as failure, including 401. Users who enable
xpack.security (a supported override via ES_SETTING_XPACK_SECURITY_ENABLED)
got 401 on the unauthenticated healthcheck request, so the version marker
was never written and every restart re-logged the one-time migration
notice. Read the HTTP status directly and accept 200 or 401.
Reviewed and skipped: the cp -rn merge-into-existing-directory concern —
verified empirically (both locally and against the image's Debian/GNU
coreutils base) that GNU cp merges correctly into a pre-existing
same-named destination without nesting; the existing test suite already
exercises this exact path (legacy 7.x data preserved during migration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- The 8.19.18 base image ends the build as USER 1000:0 with a
root-owned, read-only (0555) entrypoint, so the sed patch and later
chmod/package-install steps failed. Switch to root for the build and
restore the Elasticsearch user before runtime.
- Tighten the env_vars name check to require a leading letter/underscore
(shell identifier rules) instead of allowing a leading digit, which
made `export "$name"=...` fail and abort startup under `set -e`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The add-on reported version 8.14.3 but the shipped image was still
Elasticsearch 7.17.9 — the Dockerfile BUILD_UPSTREAM was never bumped,
and the builder uses that ARG. The homeassistant-elasticsearch
integration requires 8.14+, so configuration failed (#2849).
- Upgrade to Elasticsearch 8.19.18 (latest 8.x; 9.x cannot read indices
created in 7.x)
- Add automatic 7.x -> 8.x data migration with a guard that aborts on
unsupported paths (downgrade, or data more than one major behind).
The version marker is written only after ES answers on 9200, so a
failed upgrade never masks the true on-disk data lineage
- Default xpack.security.enabled=false to preserve plain-HTTP behavior
the HA component expects; override via ES_SETTING_XPACK_SECURITY_ENABLED
- Fix the env_vars option, which never worked (the image has no
s6-overlay, so the cont-init stack never ran)
- Remove the ingest-attachment plugin install (bundled since ES 8.0,
which broke the 8.x build)
- Replace line-number-based entrypoint patching with a proper init
script sourced via a pattern-anchored injection
- Add updater.json pinned to the 8.19 line to prevent version drift and
accidental 9.x jumps
Fixes#2849
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The build-time hook that removes `sudo` from the BirdNET-Pi scripts was
injected at line 2 of newinstaller.sh, i.e. before the repo is cloned, so it
was a no-op and `sudo` remained in 25 scripts. Outside Home Assistant this
breaks every script invoked by a non-sudoers user (php-fpm's `caddy` user for
the WebUI System Controls, or `abc`) with "X is not in the sudoers file",
which is what prevented standalone (no-Supervisor) operation. Move the strip
to run after the installer clones the repo. Also drop the unused (and, in this
fork, incorrect) `$my_dir` -> `/config` rewrite.
Additionally create /run/php before starting PHP-FPM: it is normally created
by systemd-tmpfiles, which does not run in this container, so on a fresh/tmpfs
/run the socket cannot be bound and the WebUI never comes up.
Verified against the published 2026.07.10 image: the WebUI serves HTTP 200 and
restart_services.sh runs cleanly as the non-sudoers `caddy` user.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creates a new Home Assistant add-on that installs from zach7036's BirdNET-Pi-Enhanced-Version fork instead of the Nachtzuster/alexbelgium fork. Leaves the existing birdnet-pi addon untouched.
Key changes from birdnet-pi:
- Installer repointed to zach7036/BirdNET-Pi-Enhanced-Version/main/newinstaller.sh
- Removed alexbelgium repo rename sed (zach7036 fork already clones itself)
- Removed merge_open_prs PR-merging step (install stable main)
- Updated slug to birdnet-pi-zach, image to birdnet-pi-zach-{arch}
- Updated README/docs upstream description
- Unique apparmor profile name
The generic Dockerfile patches (strip sudo, remap my_dir, systemctl shims) apply unchanged to the new fork since it follows standard BirdNET-Pi layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Gate the headroom section on install_headroom (bashio::config.true), same
as svc-headroom, instead of only checking whether the binary is on PATH
(it's pip-installed unconditionally at build time, so it's always
present). have_headroom is now just a secondary availability guard.
Fixes noisy/stale headroom output when the option is disabled.
- Switch the shebang to with-contenv bashio so HOME comes from the s6
envdir instead of a hardcoded /data/data. 20-folders.sh only rewrites
/data/data references under /defaults, /etc/cont-init.d,
/etc/services.d and /etc/s6-overlay/s6-rc.d — not /usr/local/bin — so
a custom data_location previously left this script reading/writing the
wrong home directory.
Verified live: real cron firing confirms with-contenv correctly resolves
HOME and bashio::config from the s6 envdir when invoked by cron; isolated
gating-logic test covers all four enabled/binary-present combinations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add claude-gains-report.sh + /defaults/crontabs/root: an hourly rtk gain +
headroom savings snapshot to the add-on log (heartbeat + accumulated gains).
- Add svc-headroom longrun: run the headroom proxy as a local MCP backend
(127.0.0.1:8787, no client routing) so headroom_compress/headroom_retrieve
actually store/retrieve content and record savings. Backend only, so the
Claude Desktop app's traffic is untouched (headroom #869).
- Nudge headroom tool usage via a managed, idempotent CLAUDE.md block.
- Bump version 1.6 -> 1.7 and update CHANGELOG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The removal block's json.loads() was wrapped in a bare try/except that set
data = None on any parse error, then silently no-op'd (isinstance(data, dict)
is False) with exit 0 -- so a malformed claude_desktop_config.json meant the
headroom entry was never removed and bashio::log.warning never fired.
Drop the try/except so parse errors propagate naturally and the script exits
non-zero, matching the sibling rtk removal block's existing convention and
triggering the bashio::log.warning fallback.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMcG1QjsSHcDQBxgPpRUw
headroom's proxy/wrap routing relies on ANTHROPIC_BASE_URL, which Claude
Desktop force-overrides (headroom #869), so it cannot transparently compress
the desktop app. Register headroom's MCP server (headroom mcp serve) in Claude
Desktop's claude_desktop_config.json instead -- the supported integration --
exposing the headroom_compress/headroom_retrieve/headroom_stats tools inside
the app. The plain desktop launch is left untouched.
The JSON merge is idempotent, preserves any other MCP servers and top-level
keys, backs up malformed config, and removes the entry again when
install_headroom is disabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMcG1QjsSHcDQBxgPpRUw
headroom "wrap" only accepts coding-agent CLIs (claude, codex, cursor, ...)
with agent args after "--", so it cannot wrap the claude-desktop Electron app.
Route the launch through headroom's standalone compression proxy instead: the
command file now starts "headroom proxy" and points Claude Desktop at it via
ANTHROPIC_BASE_URL, while the autostart still falls back to a plain launch if
that fails.
Claude Desktop currently force-overrides ANTHROPIC_BASE_URL (headroom #869), so
transparent compression only engages once upstream adds Desktop support; until
then the app launches normally. Update the README/CHANGELOG accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMcG1QjsSHcDQBxgPpRUw
install_headroom defaults to true and headroom is baked into the image, so
82-claude_tools.sh rewrote the desktop launch to "headroom wrap claude-desktop
--no-sandbox ...". headroom "wrap" only accepts coding-agent CLIs (claude,
codex, cursor, ...) and expects agent arguments after a "--" separator, so
"claude-desktop" plus the Electron flags is an invalid wrap target/options.
The autostart ran only that command with no fallback, leaving the desktop app
unlaunched for default users.
- Keep the plain claude-desktop launch; just expose headroom with a usage hint
(matches the documented "make headroom available and log a usage hint").
- Harden autostart to fall back to the default launch if a custom/wrapped
command fails to start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMcG1QjsSHcDQBxgPpRUw
Startup crash loop ("All subprocesses terminated. Exiting."):
- Drop the misplaced shm_size env var (Home Assistant ignores it) and run Claude
Desktop with --disable-dev-shm-usage so the Electron renderer survives the default
64 MB /dev/shm.
- Make the Selkies desktop init oneshots (init-video, init-selkies-config) tolerant so
a partially-permitted device/permission op in the HA sandbox no longer fails add-on
bringup and crash-loops the container.
- Pre-create /tmp/selkies_js.log so the base image's "chmod 777 /tmp/selkies*" calls
never fail on an empty glob; reconcile XDG_RUNTIME_DIR to the tmpfs runtime dir.
Sign-in persistence ("Your sign-in won't be saved on this device"):
- Bundle gnome-keyring/libsecret-1-0/dbus-x11 and start an unlocked Secret Service in
the desktop session, launching with --password-store=gnome-libsecret. The keyring DB
lives on persistent storage (/config/data), so the session survives restarts.
Docs: add SIGN_IN.md documenting both problems and the (deferred) in-desktop browser
option needed to complete the initial OAuth login.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XycVEAj8oZgmQszn9gdQ2E
- Exit with a clear error if the /app/config migration fails, rather than
continuing to run against the non-persistent source directory (user
changes would otherwise silently vanish on the next restart).
- Exit with a clear error if /app is missing instead of swallowing a
broken upstream image layout with `|| true`.
Addresses further review feedback from PR #2816.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZTfSk3GQRU7oD85TnjsmW
- Generate ENCRYPTION_KEY as 64 hex chars instead of base64: upstream reads
it via Buffer.from(ENCRYPTION_KEY, 'hex') for aes-256-cbc, so a base64
value silently broke Spotify token encryption for anyone leaving the
option blank (the default path).
- Store the key without a trailing newline and chmod 600 it.
- Only delete /app/config after a successful copy into /config, so a
failed migration (permissions, disk full) can't silently wipe the
upstream default config.
Addresses review feedback from PR #2816.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZTfSk3GQRU7oD85TnjsmW
New add-on wrapping the upstream jjdenhertog/spotify-to-plex Docker image
(requested in issue #2814). Keeps Spotify playlists synced to Plex.
- Wraps the multi-arch upstream image via BUILD_FROM (amd64 + aarch64)
- Uses the shared ha_entrypoint framework with 00-global_var.sh so add-on
options (Spotify credentials, redirect URI, encryption key) become the
environment variables the app expects
- 99-run.sh persists /app/config into the add-on config dir, auto-generates
and stores a stable ENCRYPTION_KEY when left blank, then hands off to the
upstream supervisord
- Web UI exposed directly on port 9030
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZTfSk3GQRU7oD85TnjsmW
Addresses review feedback on the sqlite persistence fix (PR #2812):
- validate_safe_path now rejects ".." path segments so a relative
output.sqlite.path can't traverse outside /config when rewritten.
- The rewrite now creates the destination's parent directory, since
SQLite won't create one itself for a path like "db/birdnet.db".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHJ4o22cdcgdNtb81tBTPJ
Upstream's shipped default config.yaml explicitly sets
output.sqlite.path to the relative "birdnet.db", so the missing-only
("//=") seeding of that key never fired. A relative path resolves
against the app's ephemeral container working directory instead of the
persistent /config volume, so the database was silently recreated
empty on every restart.
Rewrite any relative output.sqlite.path to live under /config on
startup, leaving already-absolute (user-customized) paths untouched.
Fixes https://github.com/tphakala/birdnet-go/discussions/3774
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHJ4o22cdcgdNtb81tBTPJ
awesomeversion (what HA's update entity uses since core's is-newer
check) parses "0.8.2-1" as semver with pre-release "1", and pre-releases
sort BELOW the base version: AwesomeVersion("0.8.2-1") >
AwesomeVersion("0.8.2") is False. Users stuck on the broken 0.8.2 nginx
release therefore see 0.8.2-1 as "Up-to-date" with a disabled Update
button and get no update notification. Four-segment 0.8.2.1 compares
strictly greater than both 0.8.2 and 0.8.2-1, and less than the next
upstream 0.8.3, so the fix becomes installable for everyone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016acGazhHvyCNUFt71xzFxm
Per Copilot review feedback on #2807: add a build-time assertion for `psql`
so a future base-image change that drops postgresql-client is caught at
`docker build` time with a clear error, instead of surfacing as a cryptic
runtime failure in 99-run.sh. Also drop the hard-coded "14 through 18"
client version range from the comment, since that's specific to the
current base image and could go stale.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcbbgB7A5LLbPuPRjLUouk
The imagegenius v3 base image moved to a newer Ubuntu release where the
apt-key binary no longer exists, breaking the Dockerfiles' manual
PGDG-repo install of postgresql-client-15 (`wget ... | apt-key add -`).
That install was already redundant: imagegenius's own Dockerfile installs
postgresql-client-14 through 18 itself (via the modern signed-by keyring
method), so the psql CLI used by 99-run.sh is already present in the base
image. Remove the downstream install entirely rather than patching it to
use a keyring, since it duplicated work the base image already does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcbbgB7A5LLbPuPRjLUouk
- Fix check_vchord_extension (and check_vector_extension) to query
pg_available_extensions against the actual immich database instead of
pg_extension on the default connection database. Immich creates the vchord
extension itself on first startup, so checking pg_extension before Immich
ever runs produced a false warning on every fresh install; checking
pg_available_extensions reports whether the server CAN provide the
extension, which is what the startup diagnostic actually needs.
- Drop the vestigial `services: - mysql:want` Supervisor service-discovery
hint from the four Immich config.yaml files: nothing in the add-on reads
it, and the scripts are hard-coded to PostgreSQL via psql — Immich has
never supported MySQL. Also fix the matching misleading line in the base
README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcbbgB7A5LLbPuPRjLUouk
imagegenius/docker-immich stopped publishing GitHub releases (its newest release
is a "2026.0.0" announcement to that effect) and ships v3 only to GHCR, so
lastversion was stuck returning 2.7.5 and the add-ons never updated past v2.
Point the four Immich updater.json files at the real product repo
immich-app/immich, which publishes clean stable vX.Y.Z releases (currently
v3.0.1). The build.json images stay on the imagegenius :3 rolling tag, which
serves the newest 3.x build, keeping the tracked version and the pulled image
coherent. Also clear the now-obsolete github_exclude "2026" guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcbbgB7A5LLbPuPRjLUouk
Upstream 0.8.2 added http-level limit_req_zone directives to its
nginx.conf. The ingress config was a wholesale copy of that file, and
both land in the same http context via servers/*.conf — nginx refuses
to declare a named shared-memory zone twice and dies at startup with
'limit_req_zone "api_rl" already bound' (502 on every page). Extract
everything from the column-0 "server {" onward instead, so maps and
zones stay declared once; zone/variable references resolve across
included files regardless of include order. Also comment out the new
Content-Security-Policy header in the ingress copy, matching the
existing X-* handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016acGazhHvyCNUFt71xzFxm
- Pin build.json to the imagegenius v3 image line (:3, :3-cuda, :3-noml, :3-openvino)
- Bump config.yaml version to 3.0.1 and record upstream_version 3.0.1 in updater.json
- Add a non-fatal VectorChord (vchord) startup check in the shared 99-run.sh so users
on a non-VectorChord database get a clear diagnostic (v3 drops pgvecto.rs)
- Document the Immich v3 database (VectorChord) and CPU requirements in each README and
point users at the Postgres 15 / Postgres 17 add-ons
- CHANGELOG entries for all four add-ons
The Postgres 15 / Postgres 17 add-ons already ship the official immich VectorChord image
(vectorchord0.4.3, matching Immich v3's pinned database) and are intentionally left
unchanged to preserve the pgvecto.rs to VectorChord migration path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcbbgB7A5LLbPuPRjLUouk
- Grant NET_ADMIN capability and /dev/net/tun device so the ZEROTIER option
works at runtime (Codex review)
- Use a JSON boolean for updater.json "paused" to match the documented format
(CodeRabbit review)
- Document the ZeroTier requirements in README and CHANGELOG
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXZfzWxhkWHq8i8P7fnDvL
Implements the Zoraxy general-purpose reverse proxy (issue #1946) as a new
add-on following the repository conventions:
- Based on the upstream zoraxydocker/zoraxy image (Alpine)
- Web management UI exposed on port 8000 (webui link); reverse proxy on 80/443
- Persistent config/db/logs/plugins relocated to /config (addon_config) so they
survive add-on updates, by patching the upstream entrypoint working directory
- Options NOAUTH/ZEROTIER/FASTGEOIP/MDNS mapped to upstream env vars via the
shared 00-global_var module, plus env_vars passthrough for advanced settings
- Standard 6-section Dockerfile, supervised services.d launcher, healthcheck
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXZfzWxhkWHq8i8P7fnDvL
Both variants share the same image; aligning the version ensures the
Supervisor pulls the updated build that suppresses the misleading
NET_RAW/NET_ADMIN alert for Full Access users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfZ5JKAR2NHP1KjoAGyBBD
Under the Home Assistant Supervisor the required NET_RAW/NET_ADMIN
capabilities are granted, but NetAlertX's upstream capabilities-audit
script cannot read them and prints a "🚨 ALERT: capabilities are missing"
banner. Users repeatedly mistook this informational message for the cause
of unrelated issues. Neutralise the audit script at build time (kept in
place as a no-op for the entrypoint runner) and bump version/changelog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfZ5JKAR2NHP1KjoAGyBBD
The folded block scalar was ~532 chars, exceeding the CodeRabbit v2
schema constraint and causing the config to be rejected. Trimmed to
238 chars; detailed context already lives in path_instructions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195h5b5zBHAA6urgJtp4aJP
Configure CodeRabbit (.coderabbit.yaml) tailored to this Home Assistant
add-on repository:
- tone_instructions establishing the HA add-on context for both issue
help and PR reviews
- chat.auto_reply + knowledge_base (issues/PRs/learnings) to support
issue triage and first-line help
- path_instructions encoding the add-on conventions (config.yaml,
Dockerfile, S6 cont-init/services scripts, updater.json, CHANGELOG,
workflows) from CLAUDE.md
- labeling/path filters aligned with the repo's existing labels and
generated artifacts
- review tools matching CI: shellcheck, hadolint, markdownlint,
yamllint, actionlint, gitleaks, checkov
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195h5b5zBHAA6urgJtp4aJP
- netbird-server: new profile `netbird-server_addon` (includes capability sys_chroot)
- netalertx_fa: symlink apparmor.txt to base netalertx profile, matching the
repo's existing _fa convention (e.g. scrutiny_fa -> scrutiny)
- signalk intentionally left without a profile (config sets apparmor: false)
No version bumps — profiles apply on each add-on's next update.
Co-Authored-By: Claude <noreply@anthropic.com>
- Add `capability sys_chroot` to all add-on AppArmor profiles (prophylactic
fix; required by any service that uses privilege-separation chroot, e.g.
sshd, Elasticsearch JVM, postgres)
- Fix AppArmor profile name collisions where add-ons had copy-pasted a wrong
profile name (inadyn_addon, db21ed7f_qbittorrent, radarr_addon,
db21ed7f_scrutiny, addon_db21ed7f_emby_nas, addon_updater, chromium_addon,
fireflyiii_addon, webtop_addon, joplin, gitea_addon) causing AppArmor to
silently apply the wrong profile
No version bumps — profiles apply on next add-on update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7P3Nbem7n9FqGTXrLMadP
Gitea's install wizard uses an atomic SaveTo that replaces the symlink
at /data/gitea/conf/app.ini with a real file containing the completed
install config. On the next restart /config/app.ini (the pre-wizard
template) already exists, so the previous guard skipped the copy and
the rm deleted the real installed config, wiping DB/security settings.
Always copy the real file over /config/app.ini regardless of whether
the target already exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D46Ef3nZZdbVuwUpPhCd4T
Symlink /data/gitea/conf/app.ini -> /config/app.ini so users can read
and edit the full Gitea configuration via the HA file editor without
needing shell access. Existing installs migrate their app.ini on first
restart. Closes#1907.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D46Ef3nZZdbVuwUpPhCd4T
Both add-ons failed because their AppArmor profiles did not list
`capability sys_chroot`, which AppArmor then denied even though it is
part of Docker's default capability set:
- gitea: sshd privilege-separation chroot("/var/empty") failed with
"Operation not permitted [preauth]", breaking git-over-SSH (#2653)
- elasticsearch: upstream startup chroot failed with
"chroot: cannot change root directory" (#2709)
Also rename the elasticsearch AppArmor profile from the copy-pasted
`inadyn_addon` (shared with several other add-ons) to
`elasticsearch_addon` to avoid profile-name collisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7P3Nbem7n9FqGTXrLMadP
Upstream paperless-ngx switched to an s6-overlay v3 init system at v2.15.0
(ENTRYPOINT ["/init"]) and removed the legacy /sbin/docker-entrypoint.sh that
this add-on patched. The add-on had therefore been broken since the 2.15.x bump.
- Inject initialization via S6_STAGE2_HOOK=/ha_entrypoint.sh instead of patching
the now-removed upstream entrypoint
- Export runtime variables to the s6 container_environment so the upstream
supervised services (svc-webserver, svc-worker, svc-scheduler, svc-consumer)
pick them up
- Use the canonical ha_entrypoint.sh template (remove the outdated bundled copy)
- Add 00-global_var module + jq for env_vars passthrough
- Guard the ImageMagick policy patch and the optional nginx/redis startup
- Bump version and updater tracking to 2.20.15
tr reads from the infinite /dev/urandom stream; head exits after N bytes,
closing the pipe, which sends SIGPIPE to tr (exit 141). With set -euo pipefail
at the top of 99-run.sh, pipefail surfaces that as the script exit code and
the container never starts. Suppress it with || true on both occurrences in
the MinIO credential generation block.
https://claude.ai/code/session_01MaLKhb2CJiF9Fb3Dyr585r
- Rename AppArmor profile from the leftover qbittorrent name to ente_addon
to avoid colliding with the qbittorrent add-on's profile
- Map the Accounts (3001), Auth (3003) and Cast (3004) ports so the login,
2FA and cast web apps served by nginx are actually reachable
- Default the external Postgres port to 5432 when DB_PORT is left blank
- Write the resolved DB host/port to museum.yaml so external databases are
configured correctly on disk, not just via env overrides
- Exclude minio-data and postgres from Home Assistant backups to avoid
pulling the whole photo library and database into every backup
https://claude.ai/code/session_01MaLKhb2CJiF9Fb3Dyr585r
1. Generate random MinIO credentials on first run, persist to /config/minio-creds,
reuse on restart. Export MINIO_ROOT_USER/PASSWORD env vars for MinIO server.
2. Make nginx web startup idempotent by checking if web.bak exists before moving.
3. Bind MinIO console to 127.0.0.1:9001 with --console-address.
4. Expose port 3002 (albums) in config.yaml and derive ENTE_ALBUMS_ORIGIN from
the API endpoint host with the mapped external port 8302.
Add a dedicated nginx health endpoint on 127.0.0.1:3001 with access_log
off that returns 200 at /health. Update HEALTHCHECK to verify both that
the filebrowser process is running (pgrep) and nginx is serving (curl to
health endpoint), avoiding direct HTTP requests to filebrowser that caused
log spam every 5 seconds.
Adds clear step-by-step instructions in the Mounting Drives section
of all Immich addon READMEs explaining how to use a mounted local
disk for Immich storage by combining localdisks and data_location.
Removing /config/config.yaml after a failed first-boot curl left the
next yq read (.realtime.audio.export.path) trying to open a missing
file; under set -e that aborts the entire cont-init script, so the
addon would never get to seed its defaults or start BirdNET-Go.
Seed an empty YAML document ({}) instead. The existing "//=" defaults
block then populates output.sqlite.path, logging.file_output.*, and
the migration block writes realtime.audio.export.path. Result: an
offline first boot now produces a valid minimal config.yaml and the
container starts cleanly.
Also harden the yq read with "// """ so a freshly seeded "{}" doc
returns an empty string (caught by the existing :-DEFAULT fallback)
rather than the literal string "null".
Replace mqtt_disable / mariadb_disable (opt-out, default-on) with
mqtt_auto_config / mariadb_auto_config (opt-in, default-off). When the
HA addon is detected but the option is off, still log the broker /
database credentials and a hint pointing the user at the option — so
discoverability stays the same without surprise config rewrites.
Bugs fixed in 01-structure.sh:
- Database backup created during BIRDSONGS_FOLDER migration was written
to the script's CWD instead of /config, and the restore path was
recomputed with a fresh timestamp — so any second-boundary crossing
between backup and restore left the user unable to recover. Backup
path is now absolute and reused for restore.
- Path inputs are validated against [A-Za-z0-9._/-]+ before being
interpolated into the SQL UPDATE statement.
- Default-config download tolerates network failure instead of leaving
an empty config.yaml behind.
- output.sqlite.path and logging.file_output.* are now seeded with the
"set-if-missing" idiom (`//=`) so user edits to config.yaml survive
restarts. (Breaking: addon options for log rotation now only seed
defaults on first run.)
- Path normalization centralized; trailing-slash juggling removed.
UX upgrades:
- 33-mqtt.sh now auto-configures realtime.mqtt.* in config.yaml from
the HA Mosquitto addon (with new mqtt_disable opt-out option).
- 33-mariadb.sh now auto-switches output.mysql.* to the HA MariaDB
addon and disables SQLite (with mariadb_disable opt-out option).
Cleanup:
- Dockerfile: upstream entrypoint sed-patch now warns (not silently
succeeds) when the target pattern is missing in a new nightly.
- Removed dead nginx upstream.conf pointing at unused port 8096.
- Trimmed redundant nginx HTML-attribute sub_filters; upstream
birdnet-go handles those itself via X-Ingress-Path. JS string
rewrites kept since the upstream HTML rewriter does not touch JS.
(Breaking UI-side if upstream regresses — see CHANGELOG.)
- Change cronupdate shebang from bashio to /bin/bash (cron PATH lacks bashio)
- Remove bashio API calls from cron script (no Supervisor access in cron)
- Source /.env in cron script to load all env_vars from 00-global_var.sh
- Persist SILENT_MODE to /etc/environment for cron access
- Remove destructive `sed 's|root|www-data|g'` on /etc/crontab
- Fix /etc/environment permissions from 600 to 644 for cron readability
/etc/asound.conf is read-only in the addon environment, so both the
shipped overrides and the user-supplied override are now written to
/root/.asoundrc (the app runs as root with HOME=/root). ALSA loads
~/.asoundrc as an additive layer on top of the system config, so the
override behavior is unchanged.
Add addon options to control birdnet-go log file rotation:
- LOG_MAX_SIZE_MB (default: 50): maximum size per log file before rotation
- LOG_MAX_AGE_DAYS (default: 7): maximum days to retain old log files
On startup, the addon:
1. Configures birdnet-go's logging.file_output settings in config.yaml
2. Sets max_rotated_files to 3 and enables compression
3. Trims existing log files exceeding the configured age
Fixes#1922
Advanced users who legitimately need JACK, a custom dsnoop chain, or any
other ALSA setup can drop their own asound.conf into the addon config
folder. The cont-init script copies it over /etc/asound.conf before
launching the app, replacing the addon-shipped defaults.
- Patch upstream entrypoint via sed so chmod on the read-only /dev/snd
mount no longer prints "Read-only file system" lines.
- Add /etc/asound.conf overriding the JACK, OSS, dsp, and dsnoop PCM
plugins to type "null" with hint.show off. This hides them from
snd_device_name_hint(), so miniaudio's device enumeration no longer
probes them at launch and the corresponding libjack/pcm_oss/pcm_dsnoop
errors disappear.
Two build-time bugs prevented the immich image from being built:
1. The find . chmod command ran from WORKDIR=/ and descended into
/app/immich/server/node_modules/, hitting files it could not chmod
(exit code 1 at build step 5). Fixed by scoping find to only
/etc /usr/local/bin /usr/local/lib /usr/local/share — the actual
directories populated by COPY rootfs/ /.
2. sed -i tried to patch /etc/s6-overlay/s6-rc.d/init-test-run/run
which no longer exists in the imagegenius base image (removed in
an earlier upstream update). Fixed with a [ -f ... ] guard so the
sed is silently skipped when the file is absent.
Reproduced both bugs locally with docker build, verified fix on a
bare-metal VPS — all 17 build steps complete cleanly.
Fixes#2718
When image: is set in config.yaml, HA pulls that image directly and
never runs the Dockerfile, so the rootfs overlay (including the
passthrough entrypoint) was never applied. Removing image: forces HA
to build from the Dockerfile, which COPYs rootfs/ and applies our
run script and entrypoint override.
Also switch run script from symlink approach to AURRAL_DATA_DIR env
var, which avoids the race condition where docker-entrypoint.sh runs
before s6 and tries to chown a broken symlink target.
/data is HA's built-in private persistent storage for every addon - no
user configuration needed. Only download_folder needs to be user-facing
since that's where the music lives and users need to know the path.
The upstream docker-entrypoint.sh chowns /app/backend/data which fails
when it's a symlink to a host-mounted HA path. Instead, use the env vars
that Aurral natively supports to redirect paths:
- AURRAL_DATA_DIR -> data_folder config option
- DOWNLOAD_FOLDER -> download_folder config option
- WEEKLY_FLOW_FOLDER -> download_folder/weekly-flow
This lets the entrypoint chown the real (container-internal) /app/backend/data
unmolested, while node writes persistent data directly to the HA share paths.
Previously image: pointed directly at the upstream ghcr.io/lklynet/aurral,
meaning the Dockerfile was never built and the rootfs overlay (including the
entrypoint fix) was never applied. Changed to ghcr.io/alexbelgium/aurral-{arch}
which is where the CI build pushes the image built FROM the upstream via build.json.
docker-entrypoint.sh runs chown -R on /app/backend/data which fails when
that path is a symlink to a host-mounted HA volume. Replace it with a
passthrough script that just exec's its arguments, letting the s6 run
script handle all setup.
The upstream docker-entrypoint.sh runs chown -R on /app/backend/data before
the s6 run script can replace it with a symlink. When the symlink target is a
host-mounted HA path (/share, /data, etc.), the chown fails with
"Operation not permitted".
Overriding ENTRYPOINT with /init hands control directly to s6-overlay, which
then runs the run script that creates the symlinks before node starts.
- config.yaml: strip back to download_folder, data_folder, port only
- README.md: match alexbelgium style, merge upstream link into About,
remove badge buttons, remove Navidrome references
- config.yaml: fix url to point to master, add ingress support,
remove download_folder/data_folder from options (set via UI),
match lidarr schema style with env_vars passthrough
- updater.json: add with upstream v1.76.12 tracking lklynet/aurral
- run script: use proper ingress port env var, cleaner startup
- README.md: fix install instructions to reference master branch
Replace the legacy Werkzeug dev server (python -m core.api) with a single
gunicorn GeventWebSocketWorker, matching upstream BirdNET-PiPy 0.7.0's
production-server migration (the add-on already builds 0.7.0 source).
Single worker is mandatory: live detections fan out via an in-process
Socket.IO emit (no Redis message_queue), so >1 worker would silently drop
Live Feed broadcasts. Heavy maintenance jobs cooperatively yield to keep the
worker responsive. Bound to 127.0.0.1:5002 for single-container use. Also
fixes the now-stale 'Python core.api' comment in nginx/run.
Add-on packaging change only -> 0.7.0 -> 0.7.0-1 (suffix convention; base
tracks upstream via the updater bot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent failure modes can cause the restart loop on arm64:
1. /dev/stdout may be inaccessible in some ARM container runtimes; replaced
with /proc/1/fd/1 which LSIO images already use for direct container stdout.
2. s6-notifyoncheck writes to fd 3 on check success, but s6-rc only opens
that fd when notification-fd exists in the service directory. LSIO arm64
images ship svc-qbittorrent without it, so s6-notifyoncheck exits with
EBADF and s6 restarts the service in a loop. Guard with a file check and
fall back to exec'ing qbittorrent-nox directly when the file is absent.
https://claude.ai/code/session_01F16ThtZyfXj6ZKFPkrSAAq
/dev/stdout resolves via /proc/self/fd/1 and can be inaccessible in some
ARM container runtimes, causing the exec redirect to fail and s6 to restart
the service in a loop. /proc/1/fd/1 is the path LSIO images already use
for direct container stdout (see nginx silent-mode handling) and is reliable
across architectures.
https://claude.ai/code/session_01F16ThtZyfXj6ZKFPkrSAAq
s6-rc opens fd 3 for a longrun service only when a notification-fd file is
present in the service directory. The LSIO aarch64 image ships svc-qbittorrent
without that file, so fd 3 is never opened. s6-notifyoncheck exits with EBADF
after its readiness check passes, s6 treats the supervised process as dead and
immediately restarts it — producing the infinite "Starting qBittorrent..." loop.
Check for the notification-fd file at runtime: use the full s6-notifyoncheck
path when it is present (amd64, preserving existing behaviour), and fall back
to exec'ing qbittorrent-nox directly when it is absent (aarch64).
https://claude.ai/code/session_01F16ThtZyfXj6ZKFPkrSAAq
s6-notifyoncheck writes to fd 3 when its readiness check passes, but
the LSIO aarch64 image's svc-qbittorrent service has no notification-fd
file so s6-rc never opens fd 3. s6-notifyoncheck exits with EBADF, s6
sees the supervised process die and immediately restarts it, producing
the infinite "Starting qBittorrent..." loop seen on odroid-c2 and other
aarch64 boards.
Drop s6-notifyoncheck entirely and exec qbittorrent-nox directly under
s6-setuidgid so s6 supervises the real process. Silent mode is handled
by redirecting the shell's fds before exec rather than passing a
/dev/stdout path (avoids a separate class of ARM container fd issues).
https://claude.ai/code/session_01F16ThtZyfXj6ZKFPkrSAAq
Seafile's check_init_admin.py looks for SEAFILE_ADMIN_EMAIL/PASSWORD
in the env, then falls back to conf/admin.txt, and only prompts
interactively if neither is available. The upstream init.sh writes
admin.txt, but it is skipped when conf/ccnet.conf or conf/revision
already exist (e.g. after a partial previous install) and the env
vars do not always reach the seahub subprocess via su. Write
admin.txt directly and inject the values into seafile.env so admin
creation succeeds (#2685).
https://claude.ai/code/session_01EwuoFH7aHJMySr9J775XQP
s6-notifyoncheck exits with EBADF when notification-fd 3 isn't opened by
s6-rc (can happen depending on LSIO image layer order), killing the supervised
qBittorrent process and causing the 2-second restart loop. Dropping it lets
s6 supervise qBittorrent directly without the fragile fd notification path.
Also probe /app, /usr/bin, and /usr/local/bin for the binary so the addon
works across LSIO image builds that place qbittorrent-nox in different spots.
https://claude.ai/code/session_015eiGSjWjSVtKbBFhBHUeDt
On HAOS >=17.3 the Supervisor Docker network gained IPv6, so
core-mariadb resolves to an IPv6 address first. The official MariaDB
addon only grants its service user from the IPv4 supervisor subnet, so
connections from IPv6 fail with "Access denied".
Resolve the hostname to its IPv4 address before connecting in every
addon that consumes bashio::services 'mysql' 'host': photoprism,
monica, fireflyiii, seafile, zoneminder. Fall back to the raw hostname
if resolution fails so IPv4-only setups keep working unchanged.
Replaces the 0.6.6-3 sleep+ingress-file-loop in services.d/nginx/run
with bashio::net.wait_for on 127.0.0.1:5002 (core.api). Under
s6-overlay all services.d/* services start concurrently, so nginx
could accept requests before the API had bound its port — proxy paths
(/api/, /socket.io/, /internal/auth) would 502, and that 502 could be
cached by an upstream service worker / edge cache (e.g. Cloudflare-
fronted HA), leaving the UI blank.
Matches the sister-addon pattern (bazarr, jellyfin, radarr). Also
switches to `exec nginx` for proper s6 supervision of the nginx PID.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump birdnet-pipy to 0.6.6 (upstream tags v0.6.5 + v0.6.6; skipping
0.6.5 same as the updater bot would).
Minor CHANGELOG.md cleanup while here: normalize 10 entries from
DD-MM-YYYY to YYYY-MM-DD, and remove a stale pre-ingress block (0.2,
0.6.1-0.6.6 dated Jan 2026) that was colliding with real version
numbers further up the file.
When DB_CONNECTION is set to mariadb_addon, the script now checks if the user
has explicitly configured DB_USERNAME, DB_PASSWORD, or DB_DATABASE in addon
options. If set, those values are used instead of the MariaDB addon service
discovery credentials. This fixes authentication failures when the service
account doesn't have proper access.
Fixes: Firefly III access denied for user 'service' issue
Agent-Logs-Url: https://github.com/alexbelgium/hassio-addons/sessions/7cacda5b-d03e-47c5-b4fc-4cfb4ef2a3dc
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
Adds `homeassistant_api: true` so the addon can call HA Core's
`update.install` service. Required for in-app self-update —
Supervisor blocks `/store/addons/<self>/update`, so the backend
routes through Core.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The app derives its timezone from station lat/lng via timezonefinder
(Settings → Location in the Web UI), so a separate addon-level TZ only
ever created mismatch with the UI-derived zone. All app-facing timestamps
(dashboard, API, DB) and Python service stdout (api/main/birdnet) already
honor the UI zone via logging_config.py's formatter, and the frontend log
viewer re-converts Icecast timestamps on read.
Net effect in the HA addon log pane: Python services still show the
correct local time; only Icecast's raw stdout now prints in UTC (accepted
trade for a single source of truth). Deletes
rootfs/etc/cont-init.d/02-timezone.sh added in 0.5.6-2.
Also cleans up the options YAML in DOCS.md/README.md: drops
RECORDING_MODE and RTSP_URL (never wired — app reads them from
user_settings.json), drops http_stream from the modes list, and moves
STREAM_BITRATE under an env_vars: example since it's honored by
start-icecast.sh but not a schema option.
Bumped to 0.6.3-2 (addon-only, no upstream app change).
Upstream 0.6.3 consolidates deployment-path handling into a single
<base href> declaration in index.html with Vite base: './' and
relative paths for all internal URLs. The previous seven sub_filter
rules (href/src/'/api'/'/socket.io') are no longer needed — one
<base href> replacement is sufficient.
Also removes the incidental brittleness from byte-level sub_filter
matches in minified JS bundles (the '/stream/' rule inadvertently
double-prefixed the literal 'api.get("/stream/config")' string).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the nginx rewrite directive that could cause URL normalization/re-encoding
issues with special characters in query strings (like spaces encoded as +).
HA Supervisor strips the ingress prefix before forwarding, making the rewrite
unnecessary. Without it, proxy_pass uses the raw $request_uri which preserves
URL encoding.
Also fix Connection header from hardcoded "upgrade" to $connection_upgrade
map variable for proper WebSocket vs regular HTTP request handling.
Agent-Logs-Url: https://github.com/alexbelgium/hassio-addons/sessions/3982b002-dfcb-4eb5-98c2-913f665b6a07
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
Two fixes for the birdnet-pipy addon:
1. Icecast log permission error: 01-structure.sh creates /app/data/logs
as root, but start-icecast.sh runs via gosu icecast2. The first log
write fails with "Permission denied", crashing icecast in a restart
loop (502 Bad Gateway on the Live Feed page). Fix: chown the log dir
and file to icecast2 before dropping privileges.
2. Live Feed double-prefixed request in ingress mode: the "/stream/"
sub_filter rule also matches 'api.get("/stream/config")' in the JS
bundle, producing a request like
/hassio_ingress/TOKEN/api/hassio_ingress/TOKEN/stream/config (404).
The upstream frontend (BirdNET-PiPy >= 0.6.2) now strips the leading
slash from stream URLs so they resolve via <base href> — the
sub_filter rule is no longer needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The calibre-web addon relies on calibredb to retrieve book metadata for
downloads. Previously, calibre was only installed at runtime via
DOCKER_MODS (linuxserver/mods:universal-calibre), which could fail due
to network issues or changes in the init sequence.
Installing calibre at build time ensures calibredb is always available
in the container, fixing the 500 Internal Server Error when downloading
books.
Agent-Logs-Url: https://github.com/alexbelgium/hassio-addons/sessions/6f6e6795-4a2c-4c6a-88b2-931def081d20
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
The upstream Maintainerr image declares /opt/data as a Docker VOLUME.
Attempting to rm -rf /opt/data fails with "Resource busy" because mount
points cannot be removed. Instead, we now:
1. Copy seed data from /opt/data to /config (persistent storage)
2. Clear contents inside /opt/data (rm -rf /opt/data/*)
3. Symlink each item in /config back into /opt/data
This ensures the VOLUME directory stays intact while all data is
redirected to persistent storage.
Agent-Logs-Url: https://github.com/alexbelgium/hassio-addons/sessions/82a46feb-2e9c-4c40-b193-614167e6d5c3
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
Added transmissionic webUI, moved transmission-web-control to the end - is removed in the current version, but there are discussions ongoing so not removing it for the moment
- Add General display options (Interval, ShowClock, etc.) to addon config schema
- Add per-Account filter options (Albums, People, ShowFavorites, etc.) to Accounts schema
- Rewrite 99-run.sh to generate complete Settings.yaml with General and Accounts sections
- env_vars are automatically classified as General or Account-level settings
- Schema options take precedence over env_vars
- Full backward compatibility: existing env_var configs continue to work
- Update README with comprehensive options documentation
- Bump version to 1.0.32.0-4
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
The upstream haugene/transmission-openvpn v5.4.0 image uses systemd-resolved
via D-Bus in its update-resolv-conf script, which fails in HA containers
that lack systemd/D-Bus with 'sd_bus_open_system: No such file or directory'.
This adds a custom update-resolv-conf script that directly modifies
/etc/resolv.conf by parsing OpenVPN's foreign_option_* environment variables.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
- Add Accounts list schema to config.yaml for multi-account support
- Make ApiKey and ImmichServerUrl optional when using Accounts list
- Generate Settings.yaml from addon options in 99-run.sh
- Fix /app/Config symlink direction for proper config persistence
- Set IMMICHFRAME_CONFIG_PATH for reliable config discovery
- Update README with multi-account documentation and examples
- Bump version to 1.0.32.0-2
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
- New init script 02-timezone.sh: reads TZ from addon config, validates
against /usr/share/zoneinfo, applies to container (/etc/localtime,
/etc/timezone, s6 container env)
- Change default TZ from Etc/UTC to Europe/Paris
- Increment version to 0.5.6-2
- Update CHANGELOG.md and DOCS.md
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
The probe script requires bashio::addon.version to return non-empty output,
but the Supervisor API may not be ready during container init. This causes
all shebang candidates to be rejected even though the interpreter works fine.
Fix: output a 'PROBE_OK' marker when the script executes successfully but
the version is empty. Also check /.bashio-standalone.sh as fallback path
(some containers store it there instead of /usr/local/lib/).
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
When the environment is too large for exec (E2BIG), scripts fail with
'env: can't execute bashio: Argument list too long'. This adds a fallback
that sources the script in a subshell with bashio preloaded, avoiding exec
entirely. Applied to both init scripts and service runners.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
The script failed with 'find: /homeassistant/addons_config: No such file or directory'
because it accessed the directory without checking if it exists. Since the script uses
set -e, the failing find command caused the entire init script to exit.
This follows the same pattern already used in the filebrowser addon's 20-folders.sh.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
- Use DATA_DIR=/config (config:rw map) per maintainer request
- Scope chmod to /entrypoint.sh and cont-init.d instead of whole FS
- Gate chown -R behind .initialized marker to avoid slow restarts
- Bump version to 0.133.1 (latest upstream manyfold-solo release)
- Remove image: field from config.yaml so HA builds via Dockerfile
- Pin Dockerfile and build.yaml to 0.133.1 instead of :latest
- Add Secret Key Base section to README covering all install scenarios
- Add Migration section to README with steps to preserve data when
switching addon slug or reinstalling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add hassio_api and hassio_role: manager to config.yaml so the addon
can call /addons/self/restart via the Supervisor API
- Resolve branch to commit SHA before downloading source for build
traceability (displayed in the app's Settings page)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The upstream Gramps Web project deprecated EMAIL_USE_TLS in favor of
EMAIL_USE_SSL (for port 465) and EMAIL_USE_STARTTLS (for port 587).
This updates the addon config schema and documentation accordingly.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
- Dynamically detect render device GID using stat instead of hardcoding 104
- chmod 666 render devices to ensure accessibility for non-root PUID/PGID
- Only run render setup when /dev/dri exists
- Use GID-specific group naming to avoid conflicts
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
The addons_updater script's global sed replacement corrupted Dockerfiles
when the upstream_version was empty (""), replacing all empty double-quoted
strings with version strings. This corrupted:
- HEALTH_URL values (causing malformed health check URLs and container
instability from failed health checks)
- ASCII art comments in Dockerfile headers
Fixed affected addons: guacamole, tdarr, photoprism, enedisgateway2mqtt_dev,
gazpar2mqtt, seafile
Also added a guard in the updater script to skip updates when version
strings are empty, preventing this corruption from recurring.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
On aarch64, native Node.js modules (sharp, @next/swc, etc.) may have
been incorrectly cross-compiled via Docker BuildKit QEMU emulation.
Add an npm rebuild step that runs at startup on aarch64 to re-download
the correct prebuilt native binaries for the actual hardware.
Fixes #XXXX
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
Add ingress: true, ingress_port: 0, and ingress_stream: true to
config.yaml. Update ingress_params.conf with sub_filter rules for
Vue.js SPA: rewrite API paths, stream paths, Socket.IO paths, and
inject base href for Vue Router history mode.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
Replace `usermod -o -u 0 node` and `groupmod -o -g 0 node` with direct
sed modifications to /etc/passwd and /etc/group. The usermod/groupmod
commands can hang indefinitely in container environments due to lock
file contention, NSS cache daemon (nscd) interactions, or PAM module
issues. The sed approach achieves the same result without these risks.
Fixes #XXXX
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
The sudoers entry for the wger user was written to /etc/sudoers before
the sudo package was installed. When apt-get installs sudo, its default
/etc/sudoers conffile may overwrite the entry, causing sudo to fail at
runtime. This prevented ha_entrypoint.sh from running as root, so
/data/media was never created with correct permissions, resulting in
PermissionError when downloading exercise images.
Fix: re-add the sudoers entry after the sudo package is installed.
Also improve symlink handling in 90-run.sh to avoid self-referencing
copies on subsequent container starts.
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
- ha_entrypoint.sh: probe_script_content now tries regular bashio first,
falls back to bashio-standalone.sh if Supervisor API unreachable
- 00-banner.sh: source bashio-standalone before calling bashio functions
in standalone branch (prevents undefined function errors with set -e)
- 01-custom_script.sh: same probe fix + add bashio-standalone.sh to
fallback source chain
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
Sonarr v4 configures FFMpegCore to look for ffprobe exclusively in its own
binary directory (/app/sonarr/bin/) via AppDomain.CurrentDomain.BaseDirectory.
The addon already installs ffmpeg via apt (providing /usr/bin/ffprobe), but
Sonarr never looks there. This init script creates a symlink so Sonarr can
find the system-installed ffprobe.
Fixes#2451
Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
# Max 250 chars. Detailed context lives in path_instructions below.
tone_instructions:"HA add-on repo with 120+ Docker add-ons for Home Assistant Supervisor. Be concise, practical and friendly; most contributors are hobbyists. Link to add-on READMEs and the repo wiki. Follow existing conventions over generic best practices."
early_access:false
enable_free_tier:true
reviews:
# "chill" keeps reviews helpful without nitpicking the many small upstream
# version-bump PRs that dominate this repo.
profile:"chill"
# Don't block merges; this repo merges frequently and relies on CI gating.
You are triaging a new issue on `alexbelgium/hassio-addons`, a monorepo of
100+ Home Assistant add-ons. Each add-on is a thin wrapper (Dockerfile,
`run.sh`, s6 services, nginx config, `config.yaml`) around an upstream
application that Alex does not maintain.
Your entire output is one JSON object, returned as the run's structured output
and matching the schema below. You have read-only tools by design: you do not
comment, label, write files, or edit anything.
## Rule 0 — ownership short-circuit
Read the existing comments in the context bundle first. The
`on_issues_ping_submitter` workflow signals ownership by posting a **comment**
(authored by `github-actions[bot]`) that pings the add-on's original submitter.
Its exact, machine-stable format is:
```
<!-- addon-submitter-ping:<addon> -->
Heads up @<user>: this issue appears to mention `<addon>`.
```
Match it on the literal marker `<!-- addon-submitter-ping:` — that string is
the reliable signal; do not infer ownership from prose. The bundle renders
each comment under a `### @<login>` heading — the marker only counts when that
heading reads `### @github-actions[bot]`. A marker pasted inside the issue
body, or inside a comment from any other login, is not the workflow's signal
and must be ignored. If a comment satisfying both conditions is present **and**
the pinged `@<user>` is not `alexbelgium`, stop immediately and emit:
```json
{"verdict":"owned","confidence":"high"}
```
Do not spend turns on anything else. (The workflow only ever pings a mapped
submitter, so in practice `@<user>` is always someone other than `alexbelgium`;
the check is a guard, not a common case.)
## Rule 1 — pick exactly one verdict
| verdict | when |
|---|---|
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |
| `upstream-bug` | The fault is in the upstream application or its image, not in this repo's wrapper. |
| `addon-bug` | The fault is in something this repo owns: the Dockerfile, `run.sh`, s6 service files, nginx config, `config.yaml` schema, or an option that is not being passed through. |
| `feature-request` | New capability, new add-on, new option. |
**The `upstream-bug` / `addon-bug` split is the one that matters.** Only
`addon-bug` triggers the expensive fix pass. Getting it wrong means the bot
opens a pull request against code that does not exist in this repository.
Test it explicitly: name the file in this repo you would have to change. If you
cannot name one, it is not `addon-bug`.
## Rule 2 — confidence is a real signal
Set `confidence` to `low` whenever any of these hold:
- The add-on could not be resolved from the title (`UNRESOLVED` in the bundle).
- The issue mixes several unrelated problems.
- You are choosing between `upstream-bug` and `addon-bug` and could argue both.
- The report is in a language you are not confident reading.
`low` confidence suppresses the comment entirely and flags a human instead.
Prefer that over a fluent guess. A wrong answer on a support issue costs Alex
more trust than no answer.
## Rule 3 — writing the comment
Only `duplicate`, `needs-info`, and `question` get a comment. The other verdicts
are labelled silently and handled later.
- **duplicate** — one line, link the other issue, no explanation.
- **needs-info** — ask only for what is *strictly* required to proceed, as a
short checklist. Never more than four items. Say where to find each one
(e.g. the add-on log tab, the Configuration tab). Do not ask for anything
already present in the issue body.
- **question** — answer only from files in the context bundle, and quote the
file path you took it from. If the bundle does not contain the answer, this
is `needs-info`, not `question`. Never invent option names.
Never close an issue. Never promise a timeline. Never say a fix is coming.
"Blocked automatically: this PR modifies shared infrastructure (\`.github/\` or \`.templates/\`), which is inherited by every add-on in the repo. Needs manual review before it goes anywhere."
stale-issue-message:'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
@@ -29,5 +29,8 @@ jobs:
stale-pr-label:'stale'
days-before-stale:'5'
days-before-close:'2'
exempt-issue-labels:'prevent stale'
# AI triage labels are exempt: an issue waiting on @alexbelgium's
# `ai:approved`, or queued for the tier-2 sweep, must not be auto-closed
# out from under the pipeline before it is acted on.
--system-prompt "You are @claude on alexbelgium/hassio-addons, invoked manually by the maintainer, so you take precedence over the automated triage tiers. Each add-on is a thin wrapper around an upstream app. For a small, confident change: make the edit on a branch ai-fix/<addon>-<issue>, run shellcheck on any shell, add a CHANGELOG.md entry, and open a ready PR that Closes the issue. For anything large or uncertain: do NOT grind on it here on Sonnet — post your analysis and recommend applying the ai:approved label (if a tier-2 plan exists) or dispatching the AI fix sweep. Never modify .github/ or .templates/. Never touch the version or upstream fields in config.yaml. Never merge or enable auto-merge."
"No AI plan (\`<!-- ai-plan -->\`) from the triage bot was found on this issue, so \`ai:approved\` has nothing to execute (removed). Run the tier-2 sweep on it first (\`AI fix sweep\` → issue $ISSUE), then approve the plan it posts."
# GATE 1 — did the action itself run? This is checked BEFORE looking
# at the payload, because the action can fail *after* having written
# a valid structured output: the object would sail through the shape
# check below, labels and a comment would be applied, and the step
# would exit 0 — a green run on a failed action, which is the exact
# silent-failure mode this workflow was rebuilt to eliminate.
# A failed action means its output is not trustworthy, full stop.
#
# This branch is also deliberately label-neutral. An action failure
# (auth, config, an outage) is systemic — it hits every issue the
# same way — so quarantining here would silently bury a batch a day
# while the real fault sits in the workflow. Fail red, add nothing,
# let the catch-up retry once it's fixed. Classify carries
# continue-on-error so this step still runs at all; without the
# explicit exit 1 the job would report success.
if [ "${CLASSIFY_OUTCOME:-}" = "failure" ]; then
restore_needs_info
# Checked before anything else, because it is the one failure with
# a specific remedy and it takes down every tier at once — tier 1
# cannot label, so tier 2's batch is empty and the whole pipeline
# goes quiet while each run still fails in a way that reads like a
# per-issue problem. Say plainly what is wrong and what to do.
if hit_auth_failure; then
echo "::error::CLAUDE_CODE_OAUTH_TOKEN is rejected (HTTP 401 / authentication_failed). This is NOT a problem with issue #$ISSUE — every AI workflow is down until the credential is replaced. Regenerate it with 'claude setup-token' and update the CLAUDE_CODE_OAUTH_TOKEN secret in the CR_PAT environment. Set the AI_DISABLED repo variable to 'true' to silence these runs meanwhile."
exit 1
fi
# ...with one exception. Exhausting the turn budget is NOT a
# workflow fault: the action ran fine and this particular issue was
# just too tangled to finish inside the turn budget. Treating it as systemic
# meant #2949 failed red and stayed unlabelled, so the catch-up
# re-dispatched it every day forever — and being the newest issue
# it took the first of only five daily slots each time.
# So it is handled like GATE 2 below instead: one retry, then a
# human. Warning rather than error, because a red run per day for a
# per-issue condition is alarm fatigue, and the outcome is recorded
# durably on the issue itself rather than only in a run log.
# A green run is only ever justified once the outcome is recorded
# somewhere durable. On the automated second look that is the
# ai:needs-human label, and only if it actually landed. On a first
# attempt nothing is recorded anywhere but this annotation, so
# exiting 0 there would be precisely the "green run, work silently
# dead" state that left triage broken for weeks. It costs at most
# one red run per problem issue, not one per day, because the
# second look ends the retry rotation either way.
if hit_max_turns; then
if is_automated_retry; then
echo "::warning::second attempt for #$ISSUE also ran out of turns, handing it to a human"
if ! escalate_to_human; then
echo "::error::could not label #$ISSUE ai:needs-human — it is NOT escalated and stays in the retry rotation"
exit 1
fi
exit 0
fi
echo "::error::classification for #$ISSUE ran out of turns; leaving it for the catch-up to retry once, after which it goes to a human"
exit 1
fi
echo "::error::the Classify action failed for #$ISSUE — this is usually a workflow-level fault affecting every issue, so the issue is left untouched for a retry. See the Classify step."
exit 1
fi
# The verdict arrives as the action's schema-validated structured
# output rather than a file the model wrote — see the Classify step.
printf '%s' "${STRUCTURED:-}" > "$F"
# GATE 2 — the action ran, but is the payload usable? `jq -e .` alone
# accepts any truthy JSON, so a verdict of `[1,2]` or `"hi"` would
# pass and then die on `.verdict` below with "Cannot index array with
# string", killing the step under set -e before the restore. Require
# an object.
#
# Reaching here means the failure is specific to THIS issue — the
# model looked at it and produced nothing usable — so a retry is
# worth exactly one attempt. Only a dispatch carrying source=catchup
# counts as that second attempt (is_automated_retry above); a manual
# workflow_dispatch is a first look and does NOT escalate, leaving
# the issue unlabelled so the catch-up still gets its own go. On the
# automated retry, hand it to a human rather than re-dispatching the
# same issue every day forever; ai:needs-human is in the catch-up
# exclusion search, so it drops out of the queue instead of starving
# newer issues behind it.
if [ ! -s "$F" ] || ! jq -e 'type == "object"' "$F" >/dev/null 2>&1; then
restore_needs_info
echo "::warning::no usable verdict produced for #$ISSUE"
if is_automated_retry; then
echo "::warning::second attempt produced no verdict, handing #$ISSUE to a human"
if ! escalate_to_human; then
echo "::error::could not label #$ISSUE ai:needs-human — it is NOT escalated and stays in the retry rotation"
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
@@ -69,11 +69,16 @@ If you want to do add the repository manually, please follow the procedure highl
- %%STATS_AMD64%%
- %%STATS_AARCH64%%
- %%STATS_ARMV7%%
### Stars evolution
### Star History
[](https://star-history.com/#alexbelgium/hassio-addons&Date)
<imgalt="Star History Chart"src="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w"/>
if ! command -v bashio::addon.version >/dev/null 2>&1;then
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh;do
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh;do
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
fi
fi
####################################
# Bashio library for source fallback
####################################
BASHIO_LIB=""
BASHIO_LIB_FULL=false
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh;do
if[ -f "$f"];then
BASHIO_LIB="$f"
# The real library, which talks to the Supervisor. The standalone shim below only reads
# environment variables, which matters to wait_for_supervisor().
BASHIO_LIB_FULL=true
break
fi
done
if[ -z "$BASHIO_LIB"];then
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh;do
if[ -f "$f"];then
BASHIO_LIB="$f"
break
fi
done
fi
##############################
# Wait for the Supervisor API #
##############################
# Many cont-init scripts build their nginx ingress config out of bashio::addon.ip_address and
# bashio::addon.ingress_port. Both come from one GET /addons/self/info, and when that is answered
# before the Supervisor is ready bashio prints nothing: the add-on then either writes
# "listen : default_server;" -- which nginx rejects with `invalid port in ":"` -- or aborts under
# set -e and leaves the %%port%% placeholders in place. Either way the add-on cannot serve ingress.
# Ask for the same values here, through the same bashio calls, until they come back usable --
# rather than making 48 add-ons defend themselves against the same empty answer.
#
# Going through bashio rather than curl is what makes this reliable rather than merely likely:
# bashio caches a successful /addons/self/info under ${CACHE_DIR:-/tmp/.bashio}, so once this
# returns, every later bashio::addon.* call in every cont-init script reads that file instead of
# asking the Supervisor again. A probe that only proved the API was up a moment ago would leave
# the very next call free to fail.
#
# Bounded and never fatal: an add-on with no SUPERVISOR_TOKEN, or a Supervisor that stays
# unreachable, still has to start. HA_SUPERVISOR_WAIT (seconds, default 30) sets the ceiling; 0
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
wait_for_supervisor(){
localmax="${HA_SUPERVISOR_WAIT:-30}"
local started deadline remaining attempt announced=0
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
# the probe and would burn the whole ceiling on every boot.
[ -n "${SUPERVISOR_TOKEN:-}"]||return0
["${BASHIO_LIB_FULL:-false}"="true"]||return0
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
command -v timeout >/dev/null 2>&1||return0
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
# spin the loop below forever.
case"$max" in ''| *[!0-9]*)return0;;esac
max=$((10#$max))
["$max" -gt 0]||return0
started=$SECONDS
deadline=$((started + max))
while :;do
remaining=$((deadline - SECONDS))
if["$remaining" -le 0];then
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
return0
fi
# No single attempt may outlive the ceiling it is bounded by.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
This is a Home Assistant add-on repository containing 120+ Docker-based add-ons for the Home Assistant Supervisor. Each add-on is a self-contained directory with a Dockerfile, config schema, and S6-overlay init scripts. The repository uses GitHub Actions for CI/CD, linting, and automated upstream version tracking.
## Add-On Directory Structure
Most add-ons follow this common layout, though exceptions exist (e.g. some archived add-ons use `config.json` instead of `config.yaml`, some add-ons have `build.yaml` instead of `build.json` or no build file at all, and not every add-on includes a `rootfs/` tree):
```
addon_name/
├── config.yaml # HA add-on metadata, schema, ports, maps
├── build.json # Base Docker images per architecture (may be build.yaml, or absent)
├── CHANGELOG.md # Required; must be updated on every PR
└── rootfs/ # Optional; absent in some add-ons
└── etc/
├── cont-init.d/ # S6-overlay init scripts (numbered, run in order)
└── services.d/ # S6-overlay supervised services (some add-ons use
# s6-overlay v3 layout at etc/s6-overlay/s6-rc.d/ instead)
```
## Dockerfile Convention
Most Dockerfiles follow this 6-section pattern (some add-ons deviate slightly, e.g. using a pinned upstream image directly instead of `ARG BUILD_FROM`):
4.**Entrypoint**– Set `S6_STAGE2_HOOK=/ha_entrypoint.sh`
5.**Labels**– Standard OCI + HA labels from build args
6.**Healthcheck**– curl-based check suppressed from nginx/apache logs
Shared build-time scripts are pulled from `.templates/` at build time:
-`ha_automodules.sh`– Downloads module scripts listed in `ARG MODULES=`
-`ha_autoapps.sh`– Installs packages listed in `ENV PACKAGES=`
-`ha_entrypoint.sh`– S6 stage-2 hook; launches the cont-init stack at container start
-`ha_lsio.sh`– Patches LinuxServer.io base images for HA compatibility
-`bashio-standalone.sh`– Bashio library for scripts outside Supervisor context
The `ARG MODULES=` line lists template scripts to download at build time (e.g., `00-banner.sh 01-custom_script.sh 00-smb_mounts.sh`). Commonly-used modules in `.templates/` (not exhaustive):
-`00-banner.sh`– Print the add-on startup banner
-`00-global_var.sh`– Initialize global env vars from HA options
-`00-local_mounts.sh`– Mount local disks (localdisks option)
-`00-smb_mounts.sh`– SMB/CIFS network mount support
-`00-deprecated.sh`– Print a deprecation warning for add-ons superseded by official ones
-`01-config_yaml.sh`– Map HA options → app's `config.yaml`
-`01-custom_script.sh`– Run user-provided custom scripts
-`99-custom_script.sh`– Run a user `script.sh` from the add-on config dir at startup
Other helper scripts in `.templates/` used at build/run time: `ha_automatic_packages.sh` (resolve package names across distros), `ha_entrypoint_modif.sh`, `00-aaa_dockerfile_backup.sh`, plus `config.template`/`script.template`/`show_text_color` (templates/assets copied into add-ons).
## config.yaml Schema
Key fields in every add-on's `config.yaml`:
```yaml
arch:[aarch64, amd64]
image:ghcr.io/alexbelgium/{slug}-{arch}
version:"X.Y.Z"# upstream version (format varies; see Versioning section)
ingress:true/false
ingress_port:8000
map:
- addon_config:rw # /addon_configs/<hostname>/
- share:rw
- media:rw
- ssl
schema:
env_vars:# Allows arbitrary env var passthrough
- name:match(^[A-Za-z0-9_]+$)
value:str?
PUID:int
PGID:int
TZ:str?
networkdisks:str? # SMB mounts
localdisks:str? # Local disk mounts
```
The `env_vars` schema key enables the env-var passthrough mechanism. At runtime the `00-global_var.sh` cont-init module reads `/data/options.json` and exports each key as an environment variable (writing to `/.env` and `/etc/environment`). `ha_entrypoint.sh` is the S6 stage-2 hook that launches the cont-init stack but does not itself perform the JSON-to-env conversion.
## Versioning
Add-on versions in `config.yaml` closely follow the upstream release tag and do not conform to a single fixed format. Common patterns include:
-`X.Y.Z`– plain upstream semver (e.g. `0.137.0`)
-`X.Y.Z.N`– upstream version with a local patch counter (e.g. `0.6.26.2`)
For the local patch counter, use a dot (`X.Y.Z.N`), not a hyphen. `X.Y.Z-N` parses as a semver pre-release tag, which Home Assistant Supervisor treats as *older* than plain `X.Y.Z` — it will not offer the update. New and updated add-ons should use `.N`; existing `-N` versions should be migrated to `.N` opportunistically (e.g. when that add-on is next touched), not as a standalone repo-wide sweep.
When an upstream version is bumped, update `version` in `config.yaml`. If the add-on's `Dockerfile` contains an `ARG BUILD_UPSTREAM` line, update that value too — it is the canonical place that records the upstream version at build time (it is **not** stored in `build.json`/`build.yaml`). Some add-ons do not use `BUILD_UPSTREAM` at all. The `updater.json` file tracks which upstream source/repo to monitor and records the last seen version.
Add-ons support end-user customization without rebuilding the image (see the repo wiki). At startup, `99-custom_script.sh` looks in the add-on's config directory for a user-provided `script.sh` (seeded from `.templates/script.template`) and executes it. Combined with the `env_vars` passthrough and the custom-script modules, this lets users inject commands and environment without forking the add-on.
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
@@ -56,35 +56,47 @@ If you want to do add the repository manually, please follow the procedure highl
### Number of addons
- In the repository : 124
- Installed : 102680
- In the repository : 143
- Installed : 334103
### Top 3
1.Filebrowser (17876x)
2.Portainer (12086x)
3.Netalertx (4442x)
1.Arpspoof (105194x)
2.Flaresolverr (91401x)
3.Portainer (19664x)
### Architectures used
- amd64: 64%
- aarch64: 36%
- %%STATS_ARMV7%%
- amd64: 84%
- aarch64: 16%
### Stars evolution
### Star History
[](https://star-history.com/#alexbelgium/hassio-addons&Date)
<imgalt="Star History Chart"src="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w"/>
</picture>
</a>
## Add-ons provided by this repository
%%ADDONS_LIST%%
✓ [Arpspoof](arpspoof/) : block internet connection for local network devices
✓ [Arpspoof (105194x)](arpspoof/) : block internet connection for local network devices
✓ [Aurral](aurral/) : Self-hosted music discovery, request management, flows, and playlist importing for Lidarr with library-aware recommendations.
@@ -113,15 +125,23 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ [Bazarr NAS](bazarr/) : Companion application to Sonarr and Radarr to download subtitles
✓ [Bazarr NAS](bazarr/) : Companion application to Sonarr and Radarr to download subtitles
✓  [BentoPDF](bentopdf/) : Privacy-first PDF toolkit. 50+ tools, all processing client-side in the browser. Files never leave your device.
@@ -162,6 +194,18 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓  [Birdnet-go (customized and built from source)](birdnet-go-dev/) : Realtime BirdNET soundscape analyzer, compiled from the alexbelgium/birdnet-go fork with all open PRs merged, with OpenVINO enabled for Intel CPU/iGPU acceleration (amd64-only test build)
@@ -221,6 +265,24 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![ingress][ingress-badge]
✓  [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom, RTK, and TokenSave optimization
✓  [Cleanuparr](cleanuparr/) : Automatically removes stuck and unwanted downloads from your *arr and download clients
✓  [Cloudcommander](cloudcommander/) : Cloud Commander a file manager for the web with console and editor
@@ -248,6 +310,16 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓  [Comicarr](comicarr/) : Automated comic book and manga downloader and library manager with a modern React UI
@@ -299,7 +372,7 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓  [FileBrowser Quantum](filebrowser_quantum/) : FileBrowser Quantum provides a modern, responsive file manager with multi-source support, advanced authentication options, and realtime indexing for your Home Assistant files.
✓  [FileBrowser Quantum](filebrowser_quantum/) : FileBrowser Quantum provides a modern, responsive file manager with multi-source support, advanced authentication options, and realtime indexing for your Home Assistant files
@@ -309,7 +382,7 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓  [Filebrowser (17876x)](filebrowser/) : filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files
✓  [Filebrowser](filebrowser/) : filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files
@@ -355,7 +428,7 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓ [Free Games Claimer (NoVNC not working)](free_games_claimer/) : automatically claims free games on the Epic Games Store, Amazon Prime Gaming and GOG
✓ [Free Games Claimer](free_games_claimer/) : Claims free games from Epic, Prime, GOG, Steam, Ubisoft and more
@@ -502,6 +571,16 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓  [Kapowarr](zzz_archived_kapowarr/) : Comic book library manager, fitting in the *arr suite of software
✓ [Kometa](kometa/) : Python script to update metadata information for movies, shows, and collections as well as automatically build collections
✓  [LibreSpeed](librespeed/) : A very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers
@@ -533,9 +622,17 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓  [Maintainerr](maintainerr/) : Rule-based media cleanup tool for Plex, Jellyfin and Emby. Creates collections and optionally deletes unwatched content.
@@ -600,7 +697,7 @@ If you want to do add the repository manually, please follow the procedure highl
![ingress][ingress-badge]
![mqtt][mqtt-badge]
✓  [NetAlertX Full Access](netalertx_fa/) : 🖧🔍 WIFI / LAN scanner, intruder, and presence detector
✓  [NetAlertX Full Access](netalertx_fa/) : 🖧🔍 Centralized network visibility and continuous asset discovery.
@@ -626,6 +723,12 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ [Nginx Proxy Manager + Static Web Server](nginx_webserver_proxy/) : Nginx Proxy Manager with a built-in configurable static file server. Manage reverse proxies via NPM UI on port 81 while serving files from HA storage on port 80.
@@ -636,6 +739,27 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ [Obsidian Sync Server](obsidian_syncserver_solo/) : Self-hosted Obsidian LiveSync backend on CouchDB. Plain HTTP — put your own reverse proxy in front for TLS.
✓ [Obsidian Sync Server NPM](obsidian_syncserver_npm/) : Self-hosted Obsidian LiveSync backend on CouchDB, bundled with Nginx Proxy Manager for TLS and certificate management.
✓ [Obsidian Sync Server SSL](obsidian_syncserver_ssl/) : Self-hosted Obsidian LiveSync backend on CouchDB, serving HTTPS with your own certificates from /ssl. Supports mobile Obsidian.
@@ -730,6 +854,14 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![full_access][full_access-badge]
✓  [Portainer (19664x) Business Edition](portainer_be/) : Manage your Docker environment with ease (Business Edition)
@@ -744,7 +876,7 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓ [Prowlarr NAS](prowlarr/) : Torrent Trackers and Usenet indexers offering complete management ofSonarr, Radarr, Lidarr, and Readarr indexers with no per app setup required
✓ [Prowlarr NAS](prowlarr/) : Torrent Trackers and Usenet indexers offering complete management ofSonarr, Radarr, Lidarr, and Readarr indexers with no per app setup required
✓  [Spotweb](spotweb/) : Spotweb is a decentralized usenet community based on the Spotnet protocol
✓ [zzz_archived : Code-server (VScode)](zzz_archived_code-server/) : Deprecated : Code-server is VS Code running on a remote server, accessible through the browser
## description: Automatic addons update by aligning version tag with upstream releases 3.19.16 (2026-01-10)
## 2026.08 (2026-08-01)
- Addon versions written in config.yaml now always comply with Home Assistant versioning: an upstream tag Home Assistant cannot order (`version-bf9e0b4f`, `ubuntu-2026-06-01`, ...) or would sort as older (`1.2.3-2`, `1.2.3+4`) no longer lands in config.yaml. The addon number is incremented instead, while the raw upstream tag stays in updater.json so the same release is never published twice
- Pre-release markers become a version section, `5.0.0b5` is published as `5.0.0.5`
- A tag Home Assistant cannot order keeps every number it carries, `v26.2-ls256` is published as `v26.2.256`
- Upstream tags are escaped before being replaced in Dockerfile/build files
## 2026.06 (05-06-2026)
- Minor bugs fixed
## 2026.05 (30-05-2026)
- Update lastversion to 3.6.12
## 3.19.16 (2026-01-10)
- Add config option to choose ISO8601 (YYYY-MM-DD) or DD-MM-YYYY dates for last_update/changelog entries
@@ -73,6 +73,22 @@ You can add the following tags in the file :
- dockerhub_by_date: in dockerhub, uses the last_update date instead of the version
- dockerhub_list_size: in dockerhub, how many containers to consider for latest version
### Addon version numbering
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is *older*), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
The addon version is therefore derived from the upstream tag:
- a tag Home Assistant can order and that is newer is used as it is
-`1.2.3-4` and `1.2.3+4` become `1.2.3.4`
- a pre-release marker becomes a section of its own, so the number it carries keeps ordering the addon: `5.0.0b5` -> `5.0.0.5`
- a tag it cannot order keeps every number it carries, in order: `v26.2-ls256` -> `v26.2.256`, `nightly-2.6.1.5509-ls8` -> `2.6.1.5509.8`, `4.16-r0-ls94` -> `4.16.0.94`, `ubuntu-2026-07-28` -> `2026.07.28`. Words holding no number, an architecture, and anything else such as a commit hash are left out
- a tag holding no number at all (`version-bf9e0b4f`, `sts`) increments the current addon version (`1.37` -> `1.38`), or uses the date when there is nothing to increment (`2026.08.01`, then `2026.08.01.1` for a second update the same day)
`updater.json` always keeps the raw upstream tag, so the next run still compares upstream with upstream and a single upstream release never triggers two addon updates. The raw tag is also kept in the Dockerfile and the build files, and is added to the changelog entry when it differs from the addon version.
These rules are checked by `python3 /usr/bin/ha_version.py --selftest`, which can be run from a terminal in the addon container.
### Addon configuration
Here you define the values that will allow the addon to connect to your repository.
bashio::log.error "... $SLUG : version $ADDONVERSION could not be written in the addon config, reverting"
git checkout -- "$ADDONFOLDER"
continue
fi
# Replace upstream tag and date, keeping the file intact if jq
# fails as a truncated updater.json would lose the addon source
if ! UPDATERJSON="$(jq --arg version "$LASTVERSION" --arg date "$DATE"'.upstream_version = $version | .last_update = $date'"$ADDONFOLDER/updater.json")";then
bashio::log.error "... $SLUG : updater.json could not be updated, reverting"
[Aurral](https://github.com/lklynet/aurral) is a self-hosted music discovery, request management, flows, and playlist importing app for Lidarr with library-aware recommendations.
This addon is based on the docker image <https://github.com/lklynet/aurral>
## Configuration
| Option | Default | Description |
|---|---|---|
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |
## Installation
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)
[](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
2. Install this add-on.
3. Click the `Save` button to store your configuration.
4. Set the `download_folder` option to your preferred path.
5. Optionally set `weekly_flow_folder` to customise the weekly flow subfolder name.
6. Start the add-on.
7. Check the logs of the add-on to see if everything went well.
- Update to Baikal 0.12.1 from 0.10.1 (changelog : <https://github.com/sabre-io/Baikal/releases>). This includes the 0.12.1 fix for an XSS vulnerability that let an authenticated user take over the admin interface by renaming a calendar
- ⚠ After the update, open the Baikal web admin once : Baikal asks to confirm the upgrade before it serves calendars again
- The application is now taken from the release published by sabre-io instead of from the ckulka/baikal-docker image, which stopped at 0.10.1. The base image still provides nginx, php-fpm and msmtp. Automatic version tracking is enabled again, following sabre-io/Baikal
- The Baikal application files in the addon data folder are now replaced on every start instead of being kept. Calendars, contacts, users and the Baikal configuration are untouched ; any manual edit made inside the application folders themselves is lost
- The Home Assistant project has deprecated support for the armv7, armhf and i386 architectures. Support wil be fully dropped in the upcoming Home Assistant 2025.12 release
&& sed -i '/^ \/\/ This property contains a VCALENDAR with a single$/,/^ \$vtimezoneObj->destroy();$/c\ $calendarTimeZone = new DateTimeZone($tzResult[$tzProp]);'"$DAVPLUGIN"\
&& grep -qxF ' $calendarTimeZone = new DateTimeZone($tzResult[$tzProp]);'"$DAVPLUGIN"\
@@ -33,7 +33,9 @@ _Thanks to everyone having starred my repo! To star it click on the image below,
---
[Baikal](https://sabre.io/baikal/) is a lightweight CalDAV+CardDAV server. It offers an extensive web interface with easy management of users, address books and calendars. It is fast and simple to install and only needs a basic php capable server. The data can be stored in a MySQL or a SQLite database.
It is based on the docker image : https://github.com/ckulka/baikal-docker
It ships the release published by [sabre-io](https://github.com/sabre-io/Baikal/releases), running on the nginx and php-fpm image built by <https://github.com/ckulka/baikal-docker>.
After an update of Baikal itself, open the web admin once : Baikal asks to confirm the upgrade before it serves calendars again. Calendars, contacts, users and the Baikal configuration are kept, but a manual edit made inside the application folders themselves is replaced on every start.
- Fix base_url sed patterns rewriting *every*`base_url` key in Bazarr's config.yaml (radarr.base_url, sonarr.base_url, and any other configured integration), instead of only Bazarr's own under `general:`. This silently broke the Radarr/Sonarr connections inside Bazarr on every addon restart when ingress was enabled
## 1.6.0.1 (2026-07-27)
- Fix ingress: nginx rewrote Bazarr's redirects into an absolute `http://<host>:8099/...` URL, which the browser blocked as mixed content when Home Assistant is served over HTTPS. Redirects now stay relative and point at the ingress path
- Fix fallback base_url in the nginx service script missing its leading `/`, which crashed Bazarr on startup
## 1.6.0 (2026-07-08)
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
## 1.5.6-4 (2026-04-22)
- Fix Bazarr crash on startup: base_url must start with '/' for Flask blueprint registration
## 1.5.6-3
- Add Ingress support with nginx reverse proxy for sidebar integration
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
## 1.5.5 (2026-02-04)
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.