Compare commits

...

43 Commits

Author SHA1 Message Date
claude[bot]
042e3bf05a fix(webtop_kde): tolerate init-video/init-selkies-config failures to stop crash loop
The Selkies desktop init oneshots do best-effort device/permission setup
(mknod /dev/input/*, chmod /tmp/selkies*, /dev/dri perms) that is only
partially permitted in the HA add-on sandbox. A non-zero exit from a
oneshot crash-loops the container along with svc-selkies. Apply the same
tolerant-oneshot fix already used in claude_desktop's 20-folders.sh.

Closes #2918

Co-authored-by: Alexandre <44178713+alexbelgium@users.noreply.github.com>
2026-07-28 07:00:14 +00:00
github-actions
663bf5dfa4 GitHub bot: changelog [nobuild] 2026-07-28 06:11:09 +00:00
Alexandre
3f9c7f8dd6 Update config.yaml 2026-07-28 07:55:03 +02:00
Alexandre
55a07c8ba5 Update 81-codex_cli.sh 2026-07-28 07:54:51 +02:00
GitHub Actions
89d6952c46 Revert "Update config.yaml"
This reverts commit 58c3fd61b8.
2026-07-27 20:30:11 +00:00
Alexandre
58c3fd61b8 Update config.yaml 2026-07-27 22:20:35 +02:00
Alexandre
f10a566b4c claude_desktop: fix Codex install failing on every boot (#2915)
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>
2026-07-27 22:09:21 +02:00
Alexandre
0ead28a7bf feat: add Portainer Business Edition add-on (#873) (#2916)
* 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>
2026-07-27 22:08:17 +02:00
dependabot[bot]
5f9ecb7b05 chore(deps): bump anthropics/claude-code-action from 1.0.181 to 1.0.183 (#2914)
Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.181 to 1.0.183.
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](44423bdec7...be7b93b190)

---
updated-dependencies:
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.183
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 21:35:07 +02:00
dependabot[bot]
99a55c2109 chore(deps): bump actions/checkout from 5.1.0 to 7.0.1 (#2913)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.1.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5.1.0...v7.0.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 21:34:56 +02:00
Alexandre
b2ada7e4a7 claude_desktop: add subscription-only Codex MCP delegation (#2911)
* 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>
2026-07-27 19:49:11 +02:00
Alexandre
2e0db91c2b Merge pull request #2912 from alexbelgium/ai-fix/bazarr-base-url-scope
Fix Bazarr base_url sed clobbering Radarr/Sonarr's own base_url
2026-07-27 19:42:42 +02:00
Alexandre
16931942b9 Fix Bazarr base_url guard scope 2026-07-27 19:41:16 +02:00
Claude
3b67bef373 Fix Bazarr base_url sed clobbering Radarr/Sonarr's own base_url
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>
2026-07-27 17:46:30 +02:00
Alexandre
278eeb931b Fix Bazarr ingress: keep redirects relative so they aren't blocked as mixed content (#2910)
* 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>
2026-07-27 15:52:00 +02:00
Alexandre
83aa854206 fix(ci): drop track_progress where the event has no issue/PR context (#2909)
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>
2026-07-27 14:43:32 +02:00
Alexandre
885b055768 fix(ci): pass github_token to claude-code-action so AI workflows can auth (#2908)
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).
2026-07-27 12:41:16 +02:00
Alexandre
79fb5a93ef Merge pull request #2907 from alexbelgium/claude/elegant-nobel-8ca7b6
fix(templates): stop corrupting option values in shell_quote / dotenv_quote
2026-07-27 12:21:25 +02:00
alexbelgium
b1f238a024 fix(templates): don't swallow bash -n's parse error in --self-test
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>
2026-07-27 11:24:05 +02:00
github-actions
a2a3583a1d Github bot : image compressed 2026-07-26 23:23:15 +00:00
github-actions
37f73b124b GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-26 19:58:42 +00:00
Alexandre
497691007b Merge pull request #2905 from alexbelgium/fix/collabora-domain-options
fix(collabora): fix domain/aliasgroup options, rebuild on Debian base (upstream went distroless)
2026-07-26 21:57:50 +02:00
Alexandre
f79ae66d73 docs(collabora): document authoritative ssl handling 2026-07-26 19:48:57 +02:00
Alexandre
ff4df36fba fix(collabora): enforce the ssl option independently of extra_params 2026-07-26 19:47:15 +02:00
Alexandre
ae2c19074a fix(collabora): stop masking required setup failures 2026-07-26 19:41:22 +02:00
Claude
c10801ae75 fix(collabora): pin BUILD_FROM per arch and restore the payload capabilities
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>
2026-07-26 19:34:30 +02:00
alexbelgium
c203703ead fix(collabora): declare SHELL with pipefail explicitly
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>
2026-07-26 19:26:22 +02:00
alexbelgium
eef927d485 fix(collabora): check linkage with ldd instead of booting coolwsd
"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>
2026-07-26 19:26:01 +02:00
alexbelgium
0e431c8281 fix(collabora): run the build smoke test as the cool user
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>
2026-07-26 18:54:27 +02:00
alexbelgium
10d32afa69 fix(collabora): keep collabora/code:latest as BUILD_FROM in build.json
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>
2026-07-26 18:30:45 +02:00
alexbelgium
041356e68b fix(templates): escape $ and backtick when writing /.env and /etc/environment
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>
2026-07-26 18:24:21 +02:00
alexbelgium
789f8813d3 fix(collabora): rebuild on a Debian base, upstream image is now distroless
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>
2026-07-26 18:23:14 +02:00
alexbelgium
85bc9c723d fix(templates): stop corrupting option values containing a single quote
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>
2026-07-26 17:45:39 +02:00
alexbelgium
dd650156f3 fix(collabora): simplify server_name log condition
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 17:28:12 +02:00
alexbelgium
6a27361cef fix(collabora): pass server_name to Collabora, fix aliasgroup escaping and version numbering
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>
2026-07-26 17:26:24 +02:00
GitHub Actions
26d922de3e Revert "Update config.yaml"
This reverts commit 6f017de2e0.
2026-07-26 09:29:21 +00:00
Alexandre
6f017de2e0 Update config.yaml 2026-07-26 11:24:55 +02:00
GitHub Actions
5f330bb971 Revert "Update config.yaml"
This reverts commit 2cc76e19d4.
2026-07-26 06:06:50 +00:00
Alexandre
2cc76e19d4 Update config.yaml 2026-07-26 08:03:59 +02:00
Alexandre
8e864f032a Merge pull request #2904 from alexbelgium/fix/filebrowser-healthcheck-ssl
filebrowser: fix healthcheck protocol detection with ssl enabled
2026-07-26 08:02:24 +02:00
alexbelgium
278818970f filebrowser: fix healthcheck protocol detection with ssl enabled
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>
2026-07-26 07:57:09 +02:00
Alexandre
b081e94323 Update health check command in Dockerfile
https://github.com/alexbelgium/hassio-addons/issues/2881
2026-07-26 07:28:41 +02:00
github-actions[bot]
5892a8f354 Update stargazer map & cache 2026-07-26 01:23:11 +00:00
198 changed files with 2076 additions and 157 deletions

View File

@@ -105,6 +105,7 @@ BigTwoFly,
Bigdaddy1990,Germany
BilboBagCoder,
Bill-Dung,Germany
Blueyzachary,
BobMcCloy,
Bobdugrand8,France
Boilerplate4u,
@@ -234,6 +235,7 @@ EricCorleone,
Erreur32,
Errox,Netherlands
Espagona,
EsteSama,
EthanVCS,United Kingdom
EtienneMD,
Evel270,
@@ -347,6 +349,7 @@ Jaber7,
JackTyson,United Kingdom
JackoKic,
JaggedJax,United States
JairCosta00,
Jak12-3,
Jango024,
JaroSu,Poland
@@ -738,6 +741,7 @@ TimInTech,Germany
Timbo74,
Tohtli,
Tokahiro,Germany
TokenLimitReached,
TomCasavant,
TomHuber1,
TopsideWings,
@@ -1105,6 +1109,7 @@ cln-io,
cloogshizer,
cloudlena,Switzerland
clubxtc,
clutchthrower,
cndoit18,China
cnrlmr,
coaster3000,United States
@@ -1162,6 +1167,7 @@ danctrl,Germany
danez,United States
danieldotnl,Netherlands
danishru,
danmulvey,United States
dannybeeckman,
dannybloomfield,United States
danveitch76,
@@ -1234,6 +1240,7 @@ dmanifold,
dmostert,
dnoggle,United States
dobrjaha,
docker-alex-ai,
docker-master,
doctorvanmartin,
dollannn,Sweden
@@ -1372,6 +1379,7 @@ frizzi42,
froggy974,France
frostworx,
frsantos,Spain
frunkad,
fuatakgun,
fullstackdelay,Germany
funar,United States
@@ -1584,6 +1592,7 @@ jhhbe,
jhiegel,
jhron,
jiadongjiang,
jiange1236,
jimmyang1992,
jine,Sweden
jj-csg,
@@ -2141,6 +2150,7 @@ quirbiefe,
qun-media,
r0rqual,United States
rJUUSO,
rafaelcruzmartins,
ralong777,
ranjitrajkumar,Canada
raphael1688,
@@ -2212,6 +2222,7 @@ rstruminski,
rtizzy,United States
rtmlp,
rucas,United States
rummik,United States
ruok911,
rvbg,Germany
rwagnervm,Brazil
@@ -2359,6 +2370,7 @@ sudo-shubham,India
suiciety,
sunshine-hass,
superiuspi,France
supersonic-jet,
supersonical,
superyass,
sweetmeats83,United States
1 username country
105 Bigdaddy1990 Germany
106 BilboBagCoder
107 Bill-Dung Germany
108 Blueyzachary
109 BobMcCloy
110 Bobdugrand8 France
111 Boilerplate4u
235 Erreur32
236 Errox Netherlands
237 Espagona
238 EsteSama
239 EthanVCS United Kingdom
240 EtienneMD
241 Evel270
349 JackTyson United Kingdom
350 JackoKic
351 JaggedJax United States
352 JairCosta00
353 Jak12-3
354 Jango024
355 JaroSu Poland
741 Timbo74
742 Tohtli
743 Tokahiro Germany
744 TokenLimitReached
745 TomCasavant
746 TomHuber1
747 TopsideWings
1109 cloogshizer
1110 cloudlena Switzerland
1111 clubxtc
1112 clutchthrower
1113 cndoit18 China
1114 cnrlmr
1115 coaster3000 United States
1167 danez United States
1168 danieldotnl Netherlands
1169 danishru
1170 danmulvey United States
1171 dannybeeckman
1172 dannybloomfield United States
1173 danveitch76
1240 dmostert
1241 dnoggle United States
1242 dobrjaha
1243 docker-alex-ai
1244 docker-master
1245 doctorvanmartin
1246 dollannn Sweden
1379 froggy974 France
1380 frostworx
1381 frsantos Spain
1382 frunkad
1383 fuatakgun
1384 fullstackdelay Germany
1385 funar United States
1592 jhiegel
1593 jhron
1594 jiadongjiang
1595 jiange1236
1596 jimmyang1992
1597 jine Sweden
1598 jj-csg
2150 qun-media
2151 r0rqual United States
2152 rJUUSO
2153 rafaelcruzmartins
2154 ralong777
2155 ranjitrajkumar Canada
2156 raphael1688
2222 rtizzy United States
2223 rtmlp
2224 rucas United States
2225 rummik United States
2226 ruok911
2227 rvbg Germany
2228 rwagnervm Brazil
2370 suiciety
2371 sunshine-hass
2372 superiuspi France
2373 supersonic-jet
2374 supersonical
2375 superyass
2376 sweetmeats83 United States

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.github/stats.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -13,7 +13,7 @@ jobs:
container: ghcr.io/hadolint/hadolint:latest-alpine
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Get changed files
id: changed_files
@@ -34,7 +34,7 @@ jobs:
container: koalaman/shellcheck-alpine:latest
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Get changed files
id: changed_files
@@ -54,7 +54,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Install jq + yq (v4)
run: |

View File

@@ -50,7 +50,6 @@ permissions:
contents: write
issues: write
pull-requests: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-fix-sweep
@@ -66,7 +65,7 @@ jobs:
environment: CR_PAT
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.AI_PR_TOKEN }}
@@ -126,12 +125,19 @@ jobs:
- name: Analyse and fix
if: steps.batch.outputs.count != '0'
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# One sticky, auto-updating status comment per run instead of the
# model narrating its own progress in scattered comments.
track_progress: true
# Skip the OIDC -> Claude App token exchange. The scheduled path
# happens to pass it (github.actor is the maintainer), but
# workflow_dispatch by anyone else would 401. AI_PR_TOKEN, not
# GITHUB_TOKEN, so a PR Claude opens triggers CI.
github_token: ${{ secrets.AI_PR_TOKEN }}
# No track_progress here. It needs an issue or PR to hang its sticky
# comment on, and the action hard-fails validation without one; this
# workflow only ever runs on schedule/workflow_dispatch. Per-issue
# progress still gets reported — issue-fix.md has Claude comment on
# each issue directly via gh.
prompt: |
The batch of issues to work through is /tmp/ai-fix/batch.json.
Follow .github/prompts/issue-fix.md exactly. Do not deviate from

View File

@@ -9,7 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@v7

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Install apps
run: |
git pull --rebase origin master

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -82,7 +82,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

View File

@@ -59,12 +59,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
- name: Run Claude Code
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.

View File

@@ -32,7 +32,6 @@ permissions:
contents: write
issues: write
pull-requests: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
@@ -76,7 +75,7 @@ jobs:
environment: CR_PAT
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.AI_PR_TOKEN }}
@@ -136,10 +135,17 @@ jobs:
- name: Execute the plan
if: steps.bundle.outputs.has_plan == 'true'
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
track_progress: true
# Skip the OIDC -> Claude App token exchange, which 401s whenever
# github.actor lacks write access. AI_PR_TOKEN, not GITHUB_TOKEN, so
# a PR Claude opens triggers CI.
github_token: ${{ secrets.AI_PR_TOKEN }}
# Only the `issues` path has a comment thread to track progress in.
# On workflow_dispatch there is none, and passing true there fails
# the action's input validation outright.
track_progress: ${{ github.event_name == 'issues' }}
prompt: |
The approved plan is /tmp/ai-exec/plan.md and the issue it belongs
to is /tmp/ai-exec/issue.json. Follow .github/prompts/issue-execute-plan.md

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Assign issues
run: |
# Init

View File

@@ -41,7 +41,6 @@ on:
permissions:
contents: read
issues: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-triage-${{ github.event.issue.number || inputs.issue || github.run_id }}
@@ -124,7 +123,7 @@ jobs:
# (issues.opened, dispatch) never set claim, so they always proceed.
- name: Checkout tooling
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
@@ -145,9 +144,15 @@ jobs:
id: classify
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
continue-on-error: true
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Without this the action falls back to the OIDC -> Claude App token
# exchange, which 401s ("User does not have write access on this
# repository") whenever github.actor is the outside reporter who
# opened the issue or replied to a needs-info request. Same token the
# step already exports as GH_TOKEN; classify only reads.
github_token: ${{ secrets.GITHUB_TOKEN }}
show_full_output: true
prompt: |
Read /tmp/ai-triage/context.md, then follow the instructions in

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Ping mapped submitter when add-on is mentioned
env:

View File

@@ -25,7 +25,6 @@ permissions:
contents: write
pull-requests: write
issues: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-coderabbit-${{ github.event.pull_request.number }}
@@ -66,7 +65,7 @@ jobs:
- name: Checkout PR branch
if: steps.claim.outputs.go == 'true'
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
@@ -80,9 +79,14 @@ jobs:
- name: Address CodeRabbit comments
if: steps.claim.outputs.go == 'true'
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever
# github.actor lacks write access — here github.actor is
# coderabbitai[bot], the review submitter. AI_PR_TOKEN, not
# GITHUB_TOKEN, so the pushed fixes re-trigger CI on the PR.
github_token: ${{ secrets.AI_PR_TOKEN }}
prompt: |
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
on ${{ github.repository }}. You are on that PR's branch. Follow

View File

@@ -18,7 +18,7 @@ jobs:
changedChangelogFiles: ${{ steps.changed-files.outputs.changelogs_files }}
steps:
- name: Checkout repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
# Need the merge commit's parents resolvable (HEAD^1 below): a depth-1 shallow
# checkout truncates parent refs entirely at the boundary commit.
@@ -88,7 +88,7 @@ jobs:
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: 🔎 Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2
@@ -106,7 +106,7 @@ jobs:
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Copy templates into addon build context
env:

View File

@@ -22,7 +22,7 @@ jobs:
changedAddons: ${{ steps.find_addons.outputs.changed_addons }}
steps:
- name: Checkout repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -59,7 +59,7 @@ jobs:
sanitizeCommitted: ${{ steps.sanitize_commit.outputs.committed }}
sanitizeCommitSha: ${{ steps.sanitize_commit.outputs.commit_long_sha }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -112,7 +112,7 @@ jobs:
matrix:
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
- name: Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2
with:
@@ -137,7 +137,7 @@ jobs:
- arch: aarch64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
@@ -335,7 +335,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -406,7 +406,7 @@ jobs:
contents: write
steps:
- name: Checkout repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository contents
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Use action to check for CRLF endings
uses: erclu/check-crlf@v1
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest # Use a Linux runner
steps:
- name: Checkout repository contents
uses: actions/checkout@v7 # Use the checkout action
uses: actions/checkout@v7.0.1 # Use the checkout action
- name: Find files with CRLF endings
uses: erclu/check-crlf@v1.2.0 # Use the check-crlf action
id: check-crlf # Assign an id to this step

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Compress Images
id: calibre

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Create stats
run: |
echo "Starting"

View File

@@ -3,6 +3,160 @@
set -e
################################################################################
# Block markers, temp helper, quoting and export-block builders
#
# Everything here is free of side effects and defined before the Supervisor
# guard, so that the self-test below can exercise the whole value path outside a
# container, where bashio is not available.
################################################################################
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
BLOCK_END="# --- END ADDON ENV (generated) ---"
mktemp_safe() {
local tmpdir="${TMPDIR:-/tmp}"
mkdir -p "$tmpdir"
mktemp "$tmpdir/tmp.XXXXXXXXXX"
}
dotenv_quote() {
# For /.env and /etc/environment: double quotes + minimal escaping.
#
# These files are read back by sourcing them from a shell, so every
# character that is still special inside double quotes has to be escaped.
# $ and ` used to be left alone, which meant a value was expanded instead of
# being read literally: a password like pa$$w0rd came back with the shell
# PID spliced into it, and a value containing backticks ran as a command.
#
# Backslash must be doubled first, so that the backslashes added below are
# not doubled in turn.
local v="$1"
v="${v//\\/\\\\}"
v="${v//\"/\\\"}"
v="${v//\$/\\\$}"
v="${v//\`/\\\`}"
v="${v//$'\n'/\\n}"
v="${v//$'\r'/\\r}"
printf '"%s"' "$v"
}
shell_quote() {
# Single-quote for safe injection into shell code.
#
# Inside single quotes every character is literal, so the only thing a value
# needs escaping for is the quote character itself: close the quote, emit an
# escaped quote, reopen it. Backslashes must be left untouched.
#
# This used to double every backslash and to replace ' with '"'"' followed by
# a stray space. The stray space corrupted every value containing a quote
# (O'Brien pass arrived as O' Brien pass); the doubling was undone further
# down the path by the "awk -v" in append_export, so backslashes survived by
# accident. Both halves are fixed together -- see the note in append_export.
local s="$1"
printf "'%s'" "${s//\'/\'\\\'\'}"
}
append_export() {
# Plain append, deliberately not awk: "awk -v q=$value" runs the value
# through awk's escape processing, which turns \t into a tab, \b into a
# backspace and \\ into a single backslash. That used to be cancelled out by
# shell_quote doubling every backslash, so the two bugs hid each other --
# fixing only one of them corrupts the value.
printf 'export %s=%s\n' "$1" "$(shell_quote "$2")" >> "$EXPORT_BODY"
}
compose_export_block() {
{
echo "$BLOCK_BEGIN"
echo "# Generated from $JSONSOURCE"
cat "$EXPORT_BODY"
echo "$BLOCK_END"
} > "$EXPORT_BLOCK"
}
################################################################################
# Self-test: bash .templates/00-global_var.sh --self-test
#
# Builds a real export block and sources it, which is exactly what happens once
# the block is injected at the top of a service run script, then checks that
# every value came back byte for byte. Testing the whole path matters: the two
# defects this guards against (shell_quote doubling backslashes and append_export
# passing values through "awk -v") cancelled each other out, so a test of either
# helper alone reported success while the pair was wrong.
#
# Runs before the Supervisor guard and exits, so it never affects startup.
################################################################################
if [[ "${1:-}" == "--self-test" ]]; then
# Literal test data: the single quotes and metacharacters are the point.
# shellcheck disable=SC2016
self_test_values=(
'plain.host'
'next\.duckdns\.org' # regex, dots escaped once
'next\\.duckdns\\.org' # regex, dots escaped twice by the user
'C:\Users\bob\share' # windows path, \U and \b are awk escapes
'\\server\share' # UNC path
'col\tsep' # \t is an awk escape
"O'Brien pass" # embedded quote
"it's a 'quoted' word" # several embedded quotes
"'leading"
"trailing'"
'a$b`c"d' # shell metacharacters
'*.example.com|^foo\d+$'
$'sp ace\ttab'
''
)
JSONSOURCE="self-test"
EXPORT_BODY="$(mktemp_safe)"
EXPORT_BLOCK="$(mktemp_safe)"
self_test_env="$(mktemp_safe)"
trap 'rm -f "$EXPORT_BODY" "$EXPORT_BLOCK" "$self_test_env"' EXIT
self_test_rc=0
self_test_check() {
# $1 name of the variable that was read back, $2 expected value, $3 how
local self_test_got="${!1}"
[[ "$self_test_got" == "$2" ]] && return 0
printf 'FAIL (%s): <%s> came back as <%s>\n' "$3" "$2" "$self_test_got"
self_test_rc=1
}
# 1. The export block, sourced the way an injected run script would
for self_test_i in "${!self_test_values[@]}"; do
append_export "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}"
done
compose_export_block
# shellcheck source=/dev/null
. "$EXPORT_BLOCK"
for self_test_i in "${!self_test_values[@]}"; do
self_test_check "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "export block"
done
# 2. /.env, sourced the way browserless_chrome and wger read it back.
# Values holding a newline are out of scope: dotenv_quote writes them as a
# literal \n, which a dotenv parser unescapes but a shell does not.
for self_test_i in "${!self_test_values[@]}"; do
printf 'DOTENVTEST_%s=%s\n' \
"$self_test_i" "$(dotenv_quote "${self_test_values[$self_test_i]}")"
done > "$self_test_env"
if ! bash -n "$self_test_env"; then
# An unescaped backtick or quote leaves the file unparseable, which would
# abort the sourcing shell instead of just yielding a wrong value.
echo "FAIL (dotenv): generated env file is not valid shell"
self_test_rc=1
else
# shellcheck source=/dev/null
. "$self_test_env"
for self_test_i in "${!self_test_values[@]}"; do
self_test_check "DOTENVTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "dotenv"
done
fi
[[ "$self_test_rc" -eq 0 ]] &&
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
exit "$self_test_rc"
fi
################################################################################
# Guard: only run inside Supervisor-managed add-ons
################################################################################
@@ -30,15 +184,6 @@ command -v jq >/dev/null || bashio::exit.nok "jq is required"
mkdir -p /etc
touch "$ETC_ENV_FILE"
################################################################################
# Temp helper
################################################################################
mktemp_safe() {
local tmpdir="${TMPDIR:-/tmp}"
mkdir -p "$tmpdir"
mktemp "$tmpdir/tmp.XXXXXXXXXX"
}
################################################################################
# Secrets support
################################################################################
@@ -68,54 +213,13 @@ resolve_secret() {
printf '%s' "$line"
}
################################################################################
# Quoting
################################################################################
dotenv_quote() {
# For /.env and /etc/environment: double quotes + minimal escaping
local v="$1"
v="${v//\\/\\\\}"
v="${v//\"/\\\"}"
v="${v//$'\n'/\\n}"
v="${v//$'\r'/\\r}"
printf '"%s"' "$v"
}
shell_quote() {
# Single-quote for safe injection in shell code
local s="$1"
s="${s//\\/\\\\}"
s="${s//\'/\'\"\'\"\' }"
s="${s% }"
printf "'%s'" "$s"
}
################################################################################
# S6 + script injection block
################################################################################
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
BLOCK_END="# --- END ADDON ENV (generated) ---"
EXPORT_BLOCK="$(mktemp_safe)"
EXPORT_BODY="$(mktemp_safe)"
KV_FILE="$(mktemp_safe)"
trap 'rm -f "$EXPORT_BLOCK" "$KV_FILE"' EXIT
{
echo "$BLOCK_BEGIN"
echo "# Generated from $JSONSOURCE"
echo "$BLOCK_END"
} > "$EXPORT_BLOCK"
append_export() {
local k="$1" v="$2" q
q="$(shell_quote "$v")"
awk -v k="$k" -v q="$q" -v e="$BLOCK_END" '
$0==e { print "export " k "=" q }
{ print }
' "$EXPORT_BLOCK" > "$EXPORT_BLOCK.tmp"
mv "$EXPORT_BLOCK.tmp" "$EXPORT_BLOCK"
}
trap 'rm -f "$EXPORT_BLOCK" "$EXPORT_BODY" "$KV_FILE"' EXIT
inject_block() {
local f="$1" tmp
@@ -235,6 +339,8 @@ cp "$ENV_FILE" "$ETC_ENV_FILE"
################################################################################
# Inject into scripts and shells (best-effort)
################################################################################
compose_export_block
for f in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run /etc/cont-init.d/*.sh /entrypoint.sh /etc/bash.bashrc "${GLOBAL_VAR_FILES:-}"; do
[[ -f "$f" ]] && inject_block "$f"
done

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,4 +1,13 @@
## 1.6.0.2 (2026-07-27)
- 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)

View File

@@ -112,4 +112,4 @@ schema:
slug: bazarr_nas
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/bazarr
version: "1.6.0"
version: "1.6.0.2"

View File

@@ -35,16 +35,20 @@ if [ -f "$CONFIG_LOCATION" ]; then
ingress_noauth)
bashio::log.green "Ingress is enabled, authentication is disabled"
bashio::log.yellow "WARNING : Make sure that the port is not exposed externally by your router to avoid a security risk !"
# Set base_url (must start with / for Flask blueprint registration)
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
# Set base_url (must start with / for Flask blueprint registration).
# Scoped to the general: block only -- config.yaml also carries a
# base_url under each configured *arr integration (radarr.base_url,
# sonarr.base_url, ...) and those must not be touched.
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
# Disable auth
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: null/ }' "$CONFIG_LOCATION"
;;
# Ingress mode, with authentication
ingress_auth)
bashio::log.green "Ingress is enabled, and external authentication is enabled"
# Set base_url (must start with / for Flask blueprint registration)
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
# Set base_url (must start with / for Flask blueprint registration).
# Scoped to the general: block only -- see note above.
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
# Enable Bazarr auth when leaving ingress_noauth
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
;;
@@ -52,7 +56,8 @@ if [ -f "$CONFIG_LOCATION" ]; then
noingress_auth)
bashio::log.green "Disabling ingress and enabling authentication"
bashio::log.yellow "WARNING : Ingress is disabled so the app won't be available from HA itself !"
sed -i "s/ base_url:.*/ base_url: ''/" "$CONFIG_LOCATION"
# Scoped to the general: block only -- see note above.
sed -i "/^general:/,/^[^ ]/{ s/ base_url:.*/ base_url: ''/; }" "$CONFIG_LOCATION"
# Enable Bazarr auth when leaving ingress_noauth
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
;;

View File

@@ -19,6 +19,19 @@ server {
proxy_set_header Connection $http_connection;
#auth_basic off;
# Adjust Location headers in backend redirects
# Bazarr is Flask-based and answers /bazarr with a redirect to /bazarr/,
# made absolute against the Host nginx sends upstream, so it reads
# http://127.0.0.1:6767/bazarr/. proxy_redirect strips that prefix, and
# nginx then re-absolutises the result as $scheme://$host:$server_port/...
# i.e. http://<ha_host>:8099/bazarr/ -- blocked by the browser as mixed
# content inside the ingress iframe. absolute_redirect off keeps it
# relative; the proxy_redirect rules re-prefix it with the ingress entry
# (the second rule also covers a redirect that was relative already).
absolute_redirect off; # Do not add port to redirect
proxy_redirect http://127.0.0.1:6767/ %%ingress_entry%%/;
proxy_redirect / %%ingress_entry%%/;
# Correct base_url
proxy_set_header Accept-Encoding "";
sub_filter_once off;

View File

@@ -15,9 +15,13 @@ bashio::net.wait_for "$port" localhost 900
if [ -f "$CONFIG_LOCATION" ]; then
if ! bashio::config.true "ingress_disabled"; then
if ! bashio::config.has_value "connection_mode" || [ "$(bashio::config 'connection_mode')" != "noingress_auth" ]; then
if ! grep -q "base_url.*$slug" "$CONFIG_LOCATION"; then
if ! sed -n "/^general:/,/^[^ ]/ { /^ base_url: \/$slug$/p; }" "$CONFIG_LOCATION" | grep -q .; then
bashio::log.warning "BaseUrl not set properly, restarting"
sed -i "s/ base_url:.*/ base_url: $slug/" "$CONFIG_LOCATION"
# Must start with / for Flask blueprint registration. Scoped to
# the general: block only -- config.yaml also carries a base_url
# under each configured *arr integration (radarr.base_url,
# sonarr.base_url, ...) and those must not be touched.
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
bashio::addon.restart
fi
fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,4 +1,19 @@
## 1.36.2 (28-07-2026)
- Minor bugs fixed
## 1.36.1 (27-07-2026)
- Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, 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 executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. 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.
## 1.36 (27-07-2026)
- Add optional OpenAI Codex CLI support, so a Claude session in this add-on can delegate work to ChatGPT Codex. Three parts: an `install_codex_cli` switch, a browserless way to activate a ChatGPT subscription on it, and an MCP registration that makes Codex callable as a tool from Claude.
- **Install (`install_codex_cli`, default off).** Codex is deliberately *not* baked into the image: the Linux release binary is ~310 MB extracted, which is not worth carrying in every installation for an off-by-default option, and updating it would then require an add-on rebuild. The new `81-codex_cli.sh` cont-init script downloads the pinned static-musl release (`ARG`/`ENV CODEX_VERSION`, currently `0.145.0`) into `/data/codex/bin` and symlinks it onto `PATH`. `/data` is the add-on's own persistent volume — independent of the configurable `data_location` — so the ~113 MB download happens once, not per boot and not again after an add-on update unless the pinned release changes. The install prefix is outside `$HOME` on purpose: the managed-MCP merge in `82-claude_tools.sh` treats any server `command` under `$HOME` as user-installed and refuses to manage it, so an add-on-owned binary living there could never be updated or removed by the add-on. The download fails open like the Claude Desktop update check — an offline boot, a GitHub outage or a bad pin logs a warning and leaves any existing binary in place instead of aborting startup, and the new binary is validated by actually running `--version` from the staging directory before it replaces the current one, so a truncated download can never overwrite a working install. Staging deliberately happens under `/data` rather than the default `/tmp`: `/tmp` in this image is a RAM-backed tmpfs, so holding the 110 MB archive plus the 310 MB binary there during boot is a real risk on a small Home Assistant host, and it is mounted `noexec`, which would make the validation step impossible anyway. Staging on the destination filesystem also turns the final move into an atomic rename instead of a second 310 MB copy. Disabling the option is non-destructive: the binary and the completed sign-in are kept, so a disable/re-enable cycle needs neither another download nor another login.
- **Subscription activation (`codex-login`).** Codex's default sign-in serves an OAuth callback on `localhost:1455` and expects a local browser, which cannot work here — the image ships no browser, and the add-on is reached through Home Assistant ingress. The new `/usr/local/bin/codex-login` helper runs `codex login --device-auth` instead, the flow OpenAI documents for headless machines: it prints a verification URL and a short-lived one-time code to approve on any other device, then polls until done. It can be run from the desktop's xterm (openbox right-click menu), from a Claude Code session, or from the container console, and it drops to the `abc` runtime user first so `~/.codex/auth.json` is not created root-owned — the same failure mode `83-github_cli.sh` documents for `~/.gitconfig`. Output is line-buffered so the URL and code appear immediately even without a TTY. If a session is already authenticated the helper reports the account and exits rather than starting a second flow. Credentials live in the persistent home, so the sign-in survives restarts and updates.
- **MCP bridge.** `codex mcp-server` exposes Codex itself as a stdio MCP server (tools `codex` and `codex-reply`, verified by driving the 0.145.0 binary over stdio), which is registered through the *existing* managed-MCP merge in `82-claude_tools.sh` rather than a second copy of that logic — so it inherits the merge's idempotence, its refusal to clobber a user-customized entry, its removal-when-disabled behaviour, and the `0600` re-tightening of the token-bearing config files. Registration covers both Claude Code (`~/.claude.json`) and Claude Desktop, so every session type gets the tool. A managed `CLAUDE.md` block tells Claude what Codex is and when a second agent is worth the round-trip, since registering a server without guidance rarely gets it used — the same gap the Headroom block exists to close.
- **Sandbox (`codex_sandbox_mode`, default `danger-full-access`).** Codex normally confines itself with a Linux OS sandbox (Landlock, or its bundled bubblewrap), which is unreliable inside a Home Assistant add-on container — and the container is already the security boundary. The default therefore bypasses Codex's own sandbox; `read-only` and `workspace-write` remain selectable. The chosen mode is applied in two places that are kept in sync: as root-level `-c` config overrides in the MCP registration (Codex forwards those to the MCP server, and they must precede the subcommand), and as a managed block at the top of `~/.codex/config.toml` so plain terminal `codex`/`codex exec` runs behave the same. That block is written at position 0 rather than appended, because a bare TOML key placed after a `[table]` header would silently become a key *of that table*. `approval_policy` is always `never` — an MCP- or cron-driven run has nobody to answer a prompt. Claude Code's own permission prompts still gate every `mcp__codex__*` call unless `permission_mode` is `bypass`.
- `84-claude_runtime_ownership.sh` now also reconciles `~/.codex`, and `claude-tools-doctor.sh` reports the Codex binary, the installed-versus-pinned version, the two new switches, and the login status (which prints the auth mode, never the token).
## 1.35 (23-07-2026)
- Fix recurring "For your security, sign in again to keep using Claude." and the Claude app's dispatch tab showing this desktop as offline when opened from mobile first. Root cause (confirmed from `~/.config/Claude/logs/main.log` on a live install): the app launches with `--password-store=gnome-libsecret`, forcing Electron's libsecret/Secret-Service backend, but `gnome-keyring` was removed from the image in a previous commit because it prompted for a keyring password on first boot and blocked the app from launching. With the flag still forcing libsecret and no keyring daemon running, `safeStorage.isEncryptionAvailable()` is `false` — logs showed `session will not persist; app secrets fall back to plaintext` and `cannot store allowlist cache`. The un-persisted session then goes stale, failing the elevated-access OAuth check (`session_stale_relogin`) that the cowork/dispatch bridge needs, so the bridge is "parked until re-login" — which is what the Claude app surfaces as the desktop being offline, until a fresh sign-in (only completable from a computer, see `SIGN_IN.md` Problem A) un-parks it. `rootfs/defaults/autostart` now launches with `--password-store=basic` instead: Electron's built-in fixed-key store needs no daemon and never prompts, and persists under `$HOME/.config/Claude` (`/data/data`, persistent), so the session survives restarts and dispatch stays online regardless of which device connects first. A passwordless keyring was considered and rejected — it would live in the same persistent volume as the ciphertext, adding no real protection in this single-user self-hosted setup. `Dockerfile`'s stale comment (still describing gnome-keyring as installed) is corrected; the package stays removed.

View File

@@ -94,7 +94,7 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
# cannot alter executables elsewhere in the image.
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" -o -name "codex-login" \) -print -exec chmod +x {} \; && \
chmod +x /usr/local/bin/claude
# Uses /bin for compatibility purposes

View File

@@ -78,6 +78,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
- Custom script support through the repository standard `claude_desktop.sh`.
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
available as an opt-in plugin.
- Optional OpenAI Codex CLI, authenticated exclusively with a ChatGPT
subscription and reachable from Claude through the native Codex MCP server.
- Optional Home Assistant MCP bridge so Claude can query and control Home
Assistant.
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
@@ -105,6 +107,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
| `install_codex_cli` | `false` | Install the latest stable OpenAI Codex CLI at startup and register its native MCP server so Claude can delegate work to ChatGPT Codex. |
| `codex_sandbox_mode` | `workspace-write` | Filesystem scope Codex runs with: `read-only`, `workspace-write`, or `danger-full-access`. |
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
@@ -195,6 +199,77 @@ The dashboard is disabled externally by default. To expose it:
The dashboard is unauthenticated. Do not publish this port to the public
internet.
## Codex CLI
Setting `install_codex_cli: true` adds OpenAI's Codex CLI alongside Claude and
registers `codex mcp-server` in both Claude Code and Claude Desktop. A Claude
session can therefore delegate a task to ChatGPT Codex and read its result back
through MCP.
Codex is not baked into the image because its Linux binary is large and the
feature is off by default. At each startup, the add-on resolves the latest
stable upstream release. It downloads the architecture-specific binary into
persistent `/data/codex/bin` only when the installed release is missing or
outdated, verifies the GitHub-published SHA-256 digest before extraction or
execution, validates the staged binary with `--version`, and replaces the
existing binary atomically. If release metadata or the download is unavailable,
startup continues and a previously working installation is retained.
### Signing in with a ChatGPT subscription
The add-on has no browser, so use the bundled device-code helper:
```bash
codex-login
```
Run it from the desktop's xterm, a Claude Code session, or the container
console. It prints a verification URL and one-time code that you approve on
another device. Credentials are stored in the runtime user's persistent
`~/.codex/auth.json`, so the sign-in survives restarts and add-on updates.
This integration is deliberately **subscription-only**. The managed launcher
removes any inherited `OPENAI_API_KEY` and starts every Codex command—including
`codex mcp-server`—with:
```toml
forced_login_method = "chatgpt"
cli_auth_credentials_store = "file"
```
The launcher also removes caller-provided overrides for those two keys before
starting Codex. The same values are maintained in `~/.codex/config.toml`.
Consequently, the MCP server uses the ChatGPT Codex entitlement and cannot
silently fall back to usage-based OpenAI API-key billing.
### Using Codex from Claude
Claude receives two native MCP tools:
- `mcp__codex__codex` starts a task. Pass a self-contained `prompt` and set
`cwd` to the repository Codex should inspect. The result includes a
`threadId`.
- `mcp__codex__codex-reply` continues the same Codex thread with its
`threadId`.
The add-on also installs managed Claude guidance recommending Codex for
independent review, a second diagnosis, or a competing implementation rather
than routine lookups. Codex consumption counts against the signed-in ChatGPT
plan's Codex allowance.
### Sandbox scope
`codex_sandbox_mode` defaults to `workspace-write`, allowing implementation
inside the supplied repository without granting unrestricted access to every
mounted path. Select `read-only` for review-only delegation. Use
`danger-full-access` only as an explicit fallback when Codex's nested Linux
sandbox is unavailable in the Home Assistant add-on container and the mounted
paths are trusted.
`approval_policy` is always `never`, because an MCP-driven Codex process has no
interactive operator to answer a prompt. Claude Code's own permissions still
gate the `mcp__codex__*` call unless `permission_mode` is `bypass`.
## Diagnostics
Run the following inside the add-on through a custom script or container console:
@@ -206,8 +281,8 @@ claude-tools-doctor.sh
The report checks the tool binaries, configuration switches, configured and
effective runtime identities, redacted MCP registrations, Claude hooks,
permission mode, Headroom health, TokenSave indexes, routing, and recorded
savings. It never prints MCP environment values because the Home Assistant MCP
entry can contain a long-lived token.
savings. It never prints MCP environment values or raw Codex authentication
status because either can contain credentials or masked credential fragments.
The hourly report can also be invoked manually:
@@ -288,6 +363,8 @@ Persistent state is stored in the configured `data_location` (default
shared home
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
project
- Codex authentication and configuration: `~/.codex`; the verified executable
and subscription-only launcher live in persistent `/data/codex/bin`
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
`$HOME/.cache`.

View File

@@ -59,6 +59,8 @@ options:
headroom_auto_compress: true
headroom_wrap_claude_code: true
install_caveman: false
install_codex_cli: false
codex_sandbox_mode: workspace-write
install_github_cli: true
install_headroom: true
install_rtk: true
@@ -107,6 +109,8 @@ schema:
headroom_auto_compress: bool?
headroom_wrap_claude_code: bool
install_caveman: bool
install_codex_cli: bool
codex_sandbox_mode: list(read-only|workspace-write|danger-full-access)
install_github_cli: bool
install_headroom: bool
install_rtk: bool
@@ -118,5 +122,5 @@ slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.35"
version: "1.36.2"
video: true

View File

@@ -0,0 +1,328 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
# OpenAI Codex CLI, installed on demand rather than baked into the image: the Linux release
# binary is large and the option is off by default. Runs before 82-claude_tools.sh so the binary
# exists when that script registers the `codex` MCP server.
#
# The install prefix is /data/codex, NOT $HOME/.codex/bin: /data is persistent regardless of the
# configurable data_location, and the managed MCP merge treats commands under $HOME as
# user-installed. Codex state (auth.json, config.toml) remains in the runtime user's home.
CODEX_ROOT="/data/codex"
CODEX_PREFIX="${CODEX_ROOT}/bin"
CODEX_BIN="${CODEX_PREFIX}/codex"
CODEX_REAL="${CODEX_PREFIX}/codex-real"
CODEX_STAMP="${CODEX_PREFIX}/.version"
CODEX_LINK="/usr/local/bin/codex"
CODEX_RELEASE_API="https://api.github.com/repos/openai/codex/releases/latest"
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then
bashio::log.warning "Unable to resolve the abc runtime home; using /data/data"
RUNTIME_HOME="/data/data"
fi
run_as_runtime_user() {
s6-setuidgid abc env HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$@"
}
if ! bashio::config.true 'install_codex_cli'; then
# Non-destructive: preserve the binary and completed ChatGPT sign-in for a later re-enable.
# 82-claude_tools.sh removes only the MCP registration and managed guidance.
bashio::log.info "Codex CLI disabled"
exit 0
fi
case "$(uname -m)" in
x86_64) CODEX_TARGET="x86_64-unknown-linux-musl" ;;
aarch64 | arm64) CODEX_TARGET="aarch64-unknown-linux-musl" ;;
*)
bashio::log.warning "Codex CLI has no release binary for $(uname -m); skipping"
exit 0
;;
esac
CODEX_ASSET="codex-${CODEX_TARGET}.tar.gz"
mkdir -p "$CODEX_PREFIX"
# Migrate the PR's earlier direct-binary layout to the enforced wrapper layout without another
# download. The real binary is kept separately; `codex` becomes a small launcher that always
# forces ChatGPT subscription authentication and removes any inherited API key.
if [ ! -x "$CODEX_REAL" ] \
&& [ -x "$CODEX_BIN" ] \
&& run_as_runtime_user "$CODEX_BIN" --version > /dev/null 2>&1; then
mv -f "$CODEX_BIN" "$CODEX_REAL"
fi
# Resolve the latest stable release and its GitHub-published SHA-256 digest on every boot. This
# follows upstream updates without pinning a version, while downloading the large asset only when
# the installed version changes. A metadata outage never replaces or removes a working binary.
codex_tmp="$(mktemp -d -p "$CODEX_ROOT")"
# mktemp always creates 0700 root:root here, but the candidate binary is validated by running it
# as the abc runtime user, which cannot traverse a root-only directory — that made every install
# fail at the --version step with "unable to exec: Permission denied" (exit 126) and report
# "Codex is unavailable this boot". Make the staging directory traversable. Nothing secret is
# staged here: it holds the public release archive and the extracted binary, both of which are
# world-readable upstream artifacts, and cleanup() removes the directory on exit.
chmod 0755 "$codex_tmp"
cleanup() {
rm -rf "$codex_tmp"
}
trap cleanup EXIT
release_metadata="${codex_tmp}/release.json"
release_info=""
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
-o "$release_metadata" "$CODEX_RELEASE_API"; then
release_info="$(
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true
import json
import os
import re
import sys
from pathlib import Path
metadata = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
tag = metadata.get("tag_name", "")
if not isinstance(tag, str) or not tag.startswith("rust-v"):
raise SystemExit("unexpected release tag")
version = tag.removeprefix("rust-v")
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+){0,2})?", version):
raise SystemExit("unexpected release version")
asset_name = os.environ["CODEX_ASSET"]
asset = next(
(
item
for item in metadata.get("assets", [])
if isinstance(item, dict) and item.get("name") == asset_name
),
None,
)
if asset is None:
raise SystemExit("release asset missing")
digest = asset.get("digest", "")
if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-fA-F]{64}", digest):
raise SystemExit("release asset has no valid SHA-256 digest")
url = asset.get("browser_download_url", "")
if not isinstance(url, str) or not url.startswith("https://github.com/openai/codex/releases/download/"):
raise SystemExit("unexpected release asset URL")
print(f"{version}\t{digest.removeprefix('sha256:').lower()}\t{url}")
PY
)"
fi
if [ -z "$release_info" ]; then
if [ -x "$CODEX_REAL" ] && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
bashio::log.warning "Unable to resolve the latest verified Codex release; keeping the existing install"
else
bashio::log.warning "Unable to resolve the latest verified Codex release; Codex is unavailable this boot"
exit 0
fi
else
IFS=$'\t' read -r CODEX_WANTED CODEX_SHA256 CODEX_URL <<< "$release_info"
if [ -x "$CODEX_REAL" ] \
&& [ "$(cat "$CODEX_STAMP" 2> /dev/null || true)" = "$CODEX_WANTED" ] \
&& run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
bashio::log.info "Codex CLI ${CODEX_WANTED} already installed (latest stable)"
else
bashio::log.info "Installing latest stable Codex CLI ${CODEX_WANTED} (${CODEX_TARGET}); this is a large one-time download"
archive="${codex_tmp}/${CODEX_ASSET}"
extracted="${codex_tmp}/codex-${CODEX_TARGET}"
# Fail open for add-on startup but fail closed for the candidate binary: its official
# release digest must match before extraction or execution, and replacement happens only
# after the staged binary successfully runs.
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \
-o "$archive" "$CODEX_URL" \
&& printf '%s %s\n' "$CODEX_SHA256" "$archive" | sha256sum -c - > /dev/null \
&& tar -xzf "$archive" -C "$codex_tmp" \
&& [ -f "$extracted" ] \
&& chmod 0755 "$extracted" \
&& run_as_runtime_user "$extracted" --version > /dev/null 2>&1 \
&& mv -f "$extracted" "$CODEX_REAL"; then
printf '%s' "$CODEX_WANTED" > "$CODEX_STAMP"
bashio::log.info "Codex CLI installed: $("$CODEX_REAL" --version 2> /dev/null || echo unknown)"
elif [ -x "$CODEX_REAL" ]; then
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; keeping the existing install"
else
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; Codex is unavailable this boot"
fi
fi
fi
if [ ! -x "$CODEX_REAL" ]; then
exit 0
fi
# Every Codex entry point, including the MCP server launched by Claude, goes through this wrapper.
# This is an execution-time guarantee in addition to the managed config below: API-key billing
# cannot be selected even if an API key is present in the surrounding environment. Caller-provided
# overrides for the two authentication guards are stripped before the forced root-level overrides
# are inserted; root-level -c flags must precede Codex subcommands such as `mcp-server`.
{
printf '#!/usr/bin/env bash\n'
printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
cat <<'SH'
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then
echo "codex: unable to resolve the abc runtime home" >&2
exit 1
fi
is_managed_override() {
local assignment="$1"
local key="${assignment%%=*}"
key="${key//[[:space:]]/}"
case "$key" in
forced_login_method | cli_auth_credentials_store) return 0 ;;
*) return 1 ;;
esac
}
filtered_args=()
while [ "$#" -gt 0 ]; do
case "$1" in
-c | --config)
if [ "$#" -lt 2 ]; then
filtered_args+=("$1")
shift
continue
fi
if is_managed_override "$2"; then
shift 2
continue
fi
filtered_args+=("$1" "$2")
shift 2
;;
--config=*)
assignment="${1#--config=}"
if ! is_managed_override "$assignment"; then
filtered_args+=("$1")
fi
shift
;;
-c*)
assignment="${1#-c}"
if ! is_managed_override "$assignment"; then
filtered_args+=("$1")
fi
shift
;;
*)
filtered_args+=("$1")
shift
;;
esac
done
forced_args=(
-c 'forced_login_method="chatgpt"'
-c 'cli_auth_credentials_store="file"'
)
if [ "$(id -u)" -eq 0 ]; then
exec s6-setuidgid abc env -u OPENAI_API_KEY \
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
"$CODEX_REAL" "${forced_args[@]}" "${filtered_args[@]}"
fi
unset OPENAI_API_KEY
export HOME="$RUNTIME_HOME"
export CODEX_HOME="$RUNTIME_HOME/.codex"
exec "$CODEX_REAL" "${forced_args[@]}" "${filtered_args[@]}"
SH
} > "$CODEX_BIN"
chmod 0755 "$CODEX_BIN"
chown -R -- "$(id -u abc):$(id -g abc)" "$CODEX_ROOT" \
|| bashio::log.warning "Unable to set ownership on ${CODEX_ROOT}"
ln -sfn "$CODEX_BIN" "$CODEX_LINK"
# Manage the root-level defaults used by terminal Codex and by `codex mcp-server`.
#
# `forced_login_method = "chatgpt"` makes subscription authentication the only permitted login
# method, so an inherited OPENAI_API_KEY cannot silently switch this integration to API billing.
# File storage is explicit because the container has no supported OS keyring.
#
# The managed block must be first: a bare TOML key after a [table] header belongs to that table.
# Existing top-level definitions of the managed keys are removed before insertion; retaining them
# would create duplicate keys and make the entire Codex configuration invalid.
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')"
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
run_as_runtime_user python3 - <<'PY' \
|| bashio::log.warning "Unable to update the managed Codex configuration block"
import os
import re
import tomllib
from pathlib import Path
BEGIN = "# BEGIN managed by claude_desktop addon"
END = "# END managed by claude_desktop addon"
MANAGED_KEYS = {
"sandbox_mode",
"approval_policy",
"forced_login_method",
"cli_auth_credentials_store",
}
block = "\n".join(
[
BEGIN,
"# Managed defaults for terminal and MCP-driven Codex runs.",
f'sandbox_mode = "{os.environ["CODEX_SANDBOX_MODE"]}"',
'approval_policy = "never"',
"# Require ChatGPT subscription OAuth; do not fall back to API-key billing.",
'forced_login_method = "chatgpt"',
"# This container has no supported OS keyring; keep OAuth credentials in auth.json.",
'cli_auth_credentials_store = "file"',
END,
]
)
path = Path(os.environ["RUNTIME_HOME"]) / ".codex" / "config.toml"
original = path.read_text(encoding="utf-8") if path.exists() else ""
rest = re.sub(
rf"{re.escape(BEGIN)}.*?{re.escape(END)}\n?",
"",
original,
flags=re.DOTALL,
)
table_header = re.compile(r"^\s*\[\[?.+?\]\]?\s*(?:#.*)?$")
assignment = re.compile(
r'''^\s*(?P<key>[A-Za-z0-9_-]+|"[^"]+"|'[^']+')\s*='''
)
kept = []
at_top_level = True
for line in rest.splitlines(keepends=True):
if at_top_level and table_header.match(line):
at_top_level = False
match = assignment.match(line) if at_top_level else None
if match:
key = match.group("key")
if key[:1] in {'"', "'"}:
key = key[1:-1]
if key in MANAGED_KEYS:
continue
kept.append(line)
remainder = "".join(kept).lstrip("\n")
new = block + "\n" + (("\n" + remainder) if remainder else "")
tomllib.loads(new)
if new != original:
path.write_text(new, encoding="utf-8")
path.chmod(0o600)
PY
if [ -f "$RUNTIME_HOME/.codex/auth.json" ]; then
bashio::log.info "Codex CLI is signed in with stored ChatGPT credentials"
else
bashio::log.info "Codex CLI is not signed in yet; run 'codex-login' to activate your ChatGPT subscription"
fi

View File

@@ -293,6 +293,20 @@ if $TOKENSAVE_ENABLED; then
done <<< "$TOKENSAVE_PROJECT_PATHS"
fi
# Codex CLI is installed by 81-codex_cli.sh into /data/codex/bin — deliberately outside $HOME,
# because is_managed() below treats any command under $HOME as user-installed.
CODEX_BIN="/data/codex/bin/codex"
CODEX_ENABLED=false
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'workspace-write')"
if bashio::config.true 'install_codex_cli'; then
if [ -x "$CODEX_BIN" ]; then
CODEX_ENABLED=true
bashio::log.info "codex $("$CODEX_BIN" --version 2> /dev/null || true) available; registering the codex MCP server (sandbox: ${CODEX_SANDBOX_MODE})"
else
bashio::log.warning "codex is not available"
fi
fi
HA_MCP_ENABLED=false
HA_MCP_URL=""
HA_MCP_TOKEN=""
@@ -314,6 +328,7 @@ fi
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
@@ -326,6 +341,7 @@ MANAGED_BASENAMES = {
"headroom": "headroom",
"tokensave": "tokensave",
"homeassistant": "mcp-proxy",
"codex": "codex",
}
desired = {}
@@ -341,6 +357,25 @@ if os.environ["HEADROOM_ENABLED"] == "true":
}
if os.environ["TOKENSAVE_ENABLED"] == "true":
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
if os.environ["CODEX_ENABLED"] == "true":
# `codex mcp-server` exposes Codex itself as an stdio MCP server (tools: codex, codex-reply),
# which is what lets a Claude session hand a task to ChatGPT Codex. The sandbox/approval
# policy is pinned with root-level `-c` overrides, which Codex forwards to the MCP server;
# they must precede the subcommand. approval_policy is always "never" because an MCP-driven
# run has nobody to answer a prompt. The sandbox defaults to workspace-write; users can opt
# into danger-full-access explicitly if the nested sandbox is unavailable in their container.
# 81-codex_cli.sh writes the same values into ~/.codex/config.toml so plain terminal `codex`
# runs behave identically.
desired["codex"] = {
"command": os.environ["CODEX_BIN"],
"args": [
"-c",
f'sandbox_mode="{os.environ["CODEX_SANDBOX_MODE"]}"',
"-c",
'approval_policy="never"',
"mcp-server",
],
}
if os.environ["HA_MCP_ENABLED"] == "true":
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
# mcp-proxy defaults to SSE, so the transport flags are required.
@@ -531,6 +566,32 @@ else
manage_claude_md_block ha-api-helper remove
fi
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
# for a second agent, the same gap the Headroom block above exists to close.
if $CODEX_ENABLED; then
manage_claude_md_block codex add <<'MD'
## Delegating to ChatGPT Codex
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT
subscription. It is a genuinely independent second agent — a different model family, reading the
files itself — not a search tool. It is slow and costs the user's ChatGPT quota, so use it when a
second opinion is worth minutes, not for routine lookups.
Good uses: an independent review of a design or a risky change before it lands; a second
diagnosis of a bug you have a theory about but cannot confirm; a competing implementation of a
self-contained piece you can then compare against your own.
Call `mcp__codex__codex` with `prompt` and always set `cwd` to the repository being discussed —
Codex reads the files itself, so it needs the right working directory and enough context in the
prompt to act without seeing this conversation. Continue an exchange with
`mcp__codex__codex-reply` (note the hyphen) using the `threadId` it returned, rather than
starting a fresh `codex` call. Treat its answers as a peer's opinion: verify claims about this
codebase before acting on them.
MD
else
manage_claude_md_block codex remove
fi
if bashio::config.true 'install_rtk'; then
if command -v rtk &> /dev/null; then
bashio::log.info "Configuring rtk Claude Code integration"

View File

@@ -2,14 +2,22 @@
# shellcheck shell=bash
set -e
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
# root). Reconcile ownership with that identity after all Claude configuration writes are
# complete, as a safety net in case any intermediate step re-owned a managed path.
# Earlier configuration scripts intentionally run as root. Reconcile the paths written by those
# scripts with the final abc runtime identity and its configured persistent home.
RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)"
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
if [ -z "$RUNTIME_HOME" ]; then
bashio::log.warning "Unable to resolve the abc runtime home; skipping runtime ownership reconciliation"
exit 0
fi
for managed_path in \
"$RUNTIME_HOME/.claude" \
"$RUNTIME_HOME/.claude.json" \
"$RUNTIME_HOME/.config/Claude" \
"$RUNTIME_HOME/.codex"; do
if [ -e "$managed_path" ]; then
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"

View File

@@ -1,18 +1,23 @@
#!/usr/bin/with-contenv bashio
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
# printing MCP environment values (which may contain the Home Assistant access token).
# printing MCP environment values or authentication material.
# shellcheck shell=bash
set +e
set -o pipefail
export NO_COLOR=1
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then
RUNTIME_HOME="/data/data"
fi
section() {
printf '\n=== %s ===\n' "$1"
}
section "Installed binaries"
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
for tool in claude claude-desktop headroom rtk tokensave codex git gh rg jq shellcheck yamllint hadolint actionlint; do
resolved="$(command -v "$tool" 2> /dev/null || true)"
if [ -n "$resolved" ]; then
printf '%-16s %s\n' "$tool" "$resolved"
@@ -22,13 +27,14 @@ for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck
done
section "Configured switches"
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_codex_cli codex_sandbox_mode install_caveman enable_tools_health_report; do
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
done
section "Runtime identity"
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
printf '%-30s %s\n' "abc runtime home" "$RUNTIME_HOME"
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
if [ "$(id -u abc)" -eq 0 ]; then
@@ -39,11 +45,12 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
fi
section "Claude Code permission state"
python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
import json
import os
from pathlib import Path
path = Path.home() / ".claude/settings.json"
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
try:
data = json.loads(path.read_text())
except FileNotFoundError:
@@ -59,13 +66,15 @@ else:
PY
section "MCP registrations (environment values redacted)"
python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
import json
import os
from pathlib import Path
home = Path(os.environ["RUNTIME_HOME"])
paths = [
Path.home() / ".claude.json",
Path.home() / ".config/Claude/claude_desktop_config.json",
home / ".claude.json",
home / ".config/Claude/claude_desktop_config.json",
]
for path in paths:
print(path)
@@ -95,11 +104,12 @@ for path in paths:
PY
section "Claude Code hooks"
python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
import json
import os
from pathlib import Path
path = Path.home() / ".claude/settings.json"
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
try:
data = json.loads(path.read_text())
except FileNotFoundError:
@@ -148,18 +158,17 @@ section "TokenSave"
if bashio::config.true 'install_tokensave'; then
tokensave doctor --agent claude || true
tokensave gain --all --range 30d || true
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
# Capture before looping — see the matching comment in 82-claude_tools.sh.
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
repo_root="$(s6-setuidgid abc env HOME="$RUNTIME_HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
if [ -z "$repo_root" ]; then
echo "${configured_path}: not a Git repository"
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
s6-setuidgid abc env HOME="$RUNTIME_HOME" tokensave status "$repo_root" --short || true
else
echo "${repo_root}: NOT INITIALIZED"
fi
@@ -168,6 +177,45 @@ else
echo "disabled"
fi
section "Codex"
if bashio::config.true 'install_codex_cli'; then
codex_bin="/data/codex/bin/codex"
if [ -x "$codex_bin" ]; then
printf '%-30s %s\n' "installed" "$("$codex_bin" --version 2> /dev/null || echo 'FAILED TO RUN')"
printf '%-30s %s\n' "installed version stamp" "$(cat /data/codex/bin/.version 2> /dev/null || echo 'MISSING')"
printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified"
printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"
# Never forward raw `login status` output: non-ChatGPT modes can include masked secret
# fragments. Only print explicitly allow-listed states.
codex_status="$(
s6-setuidgid abc env -u OPENAI_API_KEY \
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
"$codex_bin" login status 2>&1
)"
codex_status_rc=$?
case "$codex_status" in
*"Logged in using ChatGPT"*)
echo "Logged in using ChatGPT"
;;
*"Not logged in"*)
echo "Not logged in; run 'codex-login' to activate a ChatGPT subscription"
;;
*)
if [ "$codex_status_rc" -eq 0 ]; then
echo "Authenticated with a non-ChatGPT method; run 'codex-login' to enforce subscription authentication"
else
echo "Unable to determine Codex login status safely; run 'codex-login'"
fi
;;
esac
else
echo "enabled but ${codex_bin} is MISSING (download failed or add-on not yet restarted)"
fi
else
echo "disabled"
fi
section "Claude routing"
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"

View File

@@ -0,0 +1,48 @@
#!/usr/bin/with-contenv bashio
# Activate a ChatGPT subscription on the Codex CLI from inside the add-on.
#
# The add-on ships no browser, so the normal `codex login` flow cannot complete here.
# `codex login --device-auth` prints a verification URL and one-time code that can be
# approved on another device. Credentials persist in the abc runtime user's home.
# shellcheck shell=bash
set -o pipefail
CODEX_BIN="/data/codex/bin/codex"
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then
echo "codex-login: unable to resolve the abc runtime home" >&2
exit 1
fi
export HOME="$RUNTIME_HOME"
export CODEX_HOME="$RUNTIME_HOME/.codex"
if ! bashio::config.true 'install_codex_cli'; then
echo "codex-login: the install_codex_cli add-on option is disabled" >&2
exit 1
fi
if [ ! -x "$CODEX_BIN" ]; then
echo "codex-login: ${CODEX_BIN} is not installed; enable install_codex_cli and restart the add-on" >&2
exit 1
fi
# Everything runs as abc so auth.json is readable by the same account that launches the MCP
# server. Explicit HOME handling also makes container-console calls safe when the caller is root.
if [ "$(id -u)" -eq 0 ]; then
exec s6-setuidgid abc env -u OPENAI_API_KEY HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$0" "$@"
fi
# The managed Codex config forces `chatgpt` authentication and file credential storage. An
# inherited API key is removed as defense in depth so this helper cannot activate API billing.
unset OPENAI_API_KEY
# Branch on the exit code, not the text: "Not logged in" also contains "logged in".
if "$CODEX_BIN" login status; then
exit 0
fi
echo "codex-login: starting ChatGPT subscription device-code sign-in."
echo "codex-login: open the URL below on any device and enter the displayed code."
# Line-buffered so the URL and code appear while Codex is still polling in non-TTY callers.
exec stdbuf -oL -eL "$CODEX_BIN" login --device-auth "$@"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,4 +1,19 @@
## 26.04.2.4.1 (2026-07-26)
- Rebuild on a Debian base: upstream turned collabora/code into a distroless image with no shell, which broke the addon build entirely. collabora/code stays the tracked upstream image in build.json, but is now a build stage whose payload is copied onto ghcr.io/hassio-addons/debian-base, and the addon ships its own launcher in place of the removed /start-collabora-online.sh
- build.json names the architecture explicitly again (`collabora/code:latest-amd64` and `collabora/code:latest-arm64`). The builder never passes `--platform`, so the tag is the only thing that decides which binaries land in the addon
- Restore the file capabilities on `coolforkit-caps` and `coolmount`. The official image carries them as extended attributes, which `COPY --from` does not transfer, and without them Collabora starts but cannot open any document
- The certificates for `ssl: true` are read from the copies in /etc/coolwsd rather than from /ssl directly, which Collabora could not read as uid 1001 when the private key is root-only
- Make the `ssl` option authoritative even when `extra_params` is empty or customized. `ssl: false` now always disables Collabora's internal HTTPS instead of silently falling back to its default self-signed TLS
- Fix version numbering: 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. The version is tracked from the collabora/code Docker Hub tags again
- `server_name` is now passed to Collabora, fixing `Your browser has been unable to connect to the Collabora server` behind a reverse proxy
- `domain1` was never passed to Collabora at all (the script read a `domain` option that does not exist, and recent Collabora releases dropped that variable). It is now deprecated and applied as `server_name`
- `aliasgroup*` values are normalised: unescaped, escaped and double-escaped dots all produce the correct regex, and the value handed to Collabora is printed in the log
- Added `ssl_termination`, needed when `ssl` is false but Collabora is reached over https through a reverse proxy
- Added `aliasgroup2` and `aliasgroup3` for additional Nextcloud servers
- `cert_domain` is now a string (it is a certificate common name) and is passed to Collabora
- Documented the above, and corrected the README which asked for two backslashes where Collabora expects one
## 1.3.0 (2026-07-16)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)

View File

@@ -16,7 +16,45 @@
ARG BUILD_FROM
ARG BUILD_VERSION
FROM ${BUILD_FROM}
###############################################################################
# Get Collabora Online from the official image (BUILD_FROM, see build.json)
#
# build.json pins the architecture explicitly, collabora/code:latest-amd64 and
# collabora/code:latest-arm64, rather than the multi-arch collabora/code:latest.
# The builder never passes --platform: it runs the amd64 build on a native amd64
# runner and the aarch64 build on a native arm runner, so the only thing that
# decides which Collabora binaries end up in the add-on is this tag. With the
# multi-arch tag that happens to resolve correctly, but only for as long as the
# runner architecture keeps matching the target, and a mismatch would silently
# produce an image full of foreign-architecture binaries. The per-arch tags are
# published in lockstep with latest, so nothing is lost by naming them.
#
# Upstream rebuilt collabora/code as a Nix-based distroless image: /bin and
# /sbin are empty, so it can no longer be the base of the add-on itself, as s6,
# bashio and every RUN need a shell. It stays the tracked upstream image, and
# only the Collabora payload is copied out of it onto a Debian runtime.
#
# Copy only what belongs to Collabora. Do NOT copy /etc or /nix: in that image
# /etc/resolv.conf, /etc/hosts, /etc/passwd, /etc/group and /etc/nsswitch.conf
# are symlinks into /nix/store, and importing them breaks DNS resolution and
# wipes the base image users.
###############################################################################
# hadolint ignore=DL3006
FROM ${BUILD_FROM} AS collabora
###############################################################################
# Build the actual add-on on a base that has a shell
###############################################################################
FROM ghcr.io/hassio-addons/debian-base:9.3.0
# Inherited from the base, declared here so it is visible to hadolint and to
# anyone adding a pipe below. Note that the linkage check does NOT pipe into
# grep: with pipefail an ldd that exits non-zero (a binary it cannot handle at
# all) would make the pipeline fail even though grep matched, and "if" would
# then read that as "no unresolved libraries" -- the one case worth catching.
# Capturing the output and matching it with case avoids the question entirely.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
##################
# 2 Modify Image #
@@ -55,11 +93,88 @@ COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES=""
# coolwsd itself only needs glibc/libstdc++ (max GLIBCXX_3.4.22); the office
# engine bundles its own cairo, fontconfig, curl, icu and fonts under
# /opt/collaboraoffice/program. openssl is used to generate the self-signed
# certificate when the ssl option is off, cpio and findutils by the jail setup.
ENV PACKAGES="ca-certificates cpio findutils fontconfig libcap2-bin libstdc++6 openssl tzdata"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" || true && rm /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
# Collabora Online payload, taken from the official image. COPY --from keeps the
# numeric ownership, so /opt/cool and /etc/coolwsd arrive already owned by 1001.
COPY --from=collabora /usr/bin/coolwsd /usr/bin/coolforkit-caps /usr/bin/coolforkit-ns /usr/bin/coolmount /usr/bin/
COPY --from=collabora /usr/share/coolwsd /usr/share/coolwsd
COPY --from=collabora /etc/coolwsd /etc/coolwsd
COPY --from=collabora /opt/collaboraoffice /opt/collaboraoffice
COPY --from=collabora /opt/cool /opt/cool
# Recreate the runtime user the official image declares (uid/gid 1001), and the
# per-container state upstream sets up in its own final build stage.
RUN \
groupadd --gid 1001 cool && \
useradd --uid 1001 --gid 1001 --no-create-home --home-dir /opt/cool --shell /usr/sbin/nologin cool && \
mkdir -p /opt/cool/child-roots /opt/cool/cache && \
chown -R 1001:1001 /opt/cool /etc/coolwsd && \
chmod 640 /etc/coolwsd/coolwsd.xml && \
touch /var/log/coolwsd.log && \
chown 1001:1001 /var/log/coolwsd.log && \
# the WOPI proof key must be unique per container, not baked into the image
rm -rf /etc/coolwsd/proof_key* && \
(fc-cache /opt/collaboraoffice/share/fonts/truetype > /dev/null 2>&1 || true)
# Restore the file capabilities. The official image carries them as extended
# attributes on two binaries:
# coolforkit-caps cap_chown,cap_fowner,cap_sys_chroot=ep
# coolmount cap_sys_admin=ep
# COPY --from does not transfer extended attributes, so both arrive stripped.
# Nothing about the build notices: coolwsd starts and serves the admin console,
# but every document fails to open because it cannot chroot a kit process. Set
# them again and check they stuck, so a builder without xattr support fails here
# instead of shipping an add-on that only looks like it works.
#
# cap_sys_admin on coolmount only takes effect if the container is given
# SYS_ADMIN, which the add-on does not request; without it Collabora copies its
# child roots instead of bind-mounting them, which is slower but works.
RUN \
setcap "cap_chown,cap_fowner,cap_sys_chroot=ep" /usr/bin/coolforkit-caps && \
setcap "cap_sys_admin=ep" /usr/bin/coolmount && \
caps="$(getcap /usr/bin/coolforkit-caps /usr/bin/coolmount)" && \
case "$caps" in \
*cap_sys_chroot*) ;; \
*) echo "coolforkit-caps lost its capabilities: ${caps}"; exit 1 ;; \
esac && \
case "$caps" in \
*cap_sys_admin*) ;; \
*) echo "coolmount lost its capabilities: ${caps}"; exit 1 ;; \
esac
# Fail the build rather than ship an image with unresolved runtime dependencies.
# The payload was linked against the libraries of the distroless image, so each
# executable and shared library is checked against the Debian runtime.
#
# coolwsd itself cannot be executed as a smoke test. It refuses to run as root
# ("Do not run as root. Please run as cool user.", exit 78), and --version does
# not exit either -- the official entrypoint passes it to the long-running
# server to get the version into the log. Checking that every binary resolves
# its libraries proves the same thing and terminates.
RUN \
command -v openssl > /dev/null && \
command -v su > /dev/null && \
for binary in \
/usr/bin/coolwsd \
/usr/bin/coolforkit-caps \
/usr/bin/coolforkit-ns \
/usr/bin/coolmount \
/opt/collaboraoffice/program/soffice.bin \
/opt/collaboraoffice/program/libmergedlo.so; do \
libs="$(ldd "$binary" 2>&1)"; \
case "$libs" in \
*"not found"*) echo "Unresolved libraries in ${binary}:"; echo "$libs"; exit 1 ;; \
esac; \
done
################
# 4 Entrypoint #

View File

@@ -52,22 +52,63 @@ Webui can be found at `https://homeassistant:9980/browser/dist/admin/admin.html`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `aliasgroup1` | str | | Nextcloud external domain with escaped dots using two \ (e.g. `nextcloud_domain\\.com`) |
| `domain1` | str | | Collabora external domain with escaped dots using two \ (e.g. `code_domain\\.com`) |
| `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) |
| `aliasgroup2` | str | | A second Nextcloud server, same format as `aliasgroup1` |
| `aliasgroup3` | str | | A third Nextcloud server, same format as `aliasgroup1` |
| `server_name` | str | | External hostname (and port) of **this Collabora** server, as the browser reaches it (e.g. `code_domain.com:9980`). Set it when Collabora sits behind a reverse proxy |
| `ssl_termination` | bool | `false` | Set to `true` when `ssl` is `false` but the browser reaches Collabora over `https` through a reverse proxy |
| `extra_params` | str | | Extra parameters passed to the Collabora start script |
| `ssl` | bool | `false` | Enable SSL using certificates from /ssl |
| `certfile` | str | `fullchain.pem` | Certificate file name located in /ssl |
| `keyfile` | str | `privkey.pem` | Private key file name located in /ssl |
| `cert_domain` | str | | Common name of the self-signed certificate generated when `ssl` is `false` |
| `username` | str | | Username for the Collabora admin console |
| `password` | str | | Password for the Collabora admin console |
| `dictionaries` | str | | Space-separated list of dictionary languages to install |
| `domain1` | str | | **Deprecated**, use `server_name` instead |
#### About the escaped dots in `aliasgroup*`
Collabora matches the `aliasgroup*` addresses as **regular expressions**, so a dot
has to be escaped with a **single** backslash: `next\.duckdns\.org`, not
`next\\.duckdns\\.org`. A doubled backslash means "a literal backslash followed by
any character", which never matches a real hostname, and Collabora then rejects the
Nextcloud server.
Earlier versions of this page asked for two backslashes, which was wrong. The add-on
now normalises whatever you type, so `next.duckdns.org`, `next\.duckdns\.org` and
`next\\.duckdns\\.org` all end up as the same correct pattern. The value that is
really handed to Collabora is printed in the add-on log at startup:
```text
Allowed Nextcloud host aliasgroup1: https://next\.duckdns\.org:443
```
Values containing other regex characters (`*`, `|`, `(`, `[`, …) are left untouched,
so hand-written patterns keep working.
`server_name` is **not** a regular expression: write it as a plain hostname, without
backslashes.
### Example configuration
Nextcloud on `https://next.duckdns.org` and Collabora reachable on
`https://code.duckdns.org:9980`, with a reverse proxy handling the certificates:
```yaml
aliasgroup1: nextcloud_domain\\.com
domain1: code_domain\\.com
extra_params: ""
aliasgroup1: https://next\.duckdns\.org:443
server_name: code.duckdns.org:9980
ssl_termination: true
ssl: false
username: admin
password: changeme
```
Same setup, but letting the add-on serve the certificates itself from `/ssl`:
```yaml
aliasgroup1: https://next\.duckdns\.org:443
server_name: code.duckdns.org:9980
ssl: true
certfile: fullchain.pem
keyfile: privkey.pem
@@ -81,7 +122,19 @@ password: changeme
1. Start the add-on and expose the Collabora server to an external domain.
1. Install and configure the Nextcloud add-on.
1. Inside Nextcloud, install the **Nextcloud Office** app.
1. In Nextcloud **Administration Settings → Office**, set the Collabora server URL to `https://yourdomain:9980` and enable **Disable certificate validation**.
1. In Nextcloud **Administration Settings → Office**, set the Collabora server URL to
the **Collabora** address, not the Nextcloud one — with the example above that is
`https://code.duckdns.org:9980` — and enable **Disable certificate validation** if
the add-on serves a self-signed certificate.
1. Add both hostnames to the Nextcloud `trusted_domains`.
The two hostnames have different roles, and swapping them is the most common cause of
`Could not establish connection to the Collabora Online server`:
- `aliasgroup1` is the **Nextcloud** address, it tells Collabora which server is
allowed to ask it to open documents.
- `server_name` is the **Collabora** address, it tells Collabora which URL to hand
back to the browser.
### Custom Scripts and Environment Variables

View File

@@ -15,7 +15,7 @@ options:
env_vars: []
aliasgroup1: ""
certfile: fullchain.pem
domain1: ""
server_name: ""
extra_params:
--o:ssl.enable=false --o:user_interface.use_integration_theme=false
--o:net.proto=IPv4
@@ -33,7 +33,9 @@ schema:
value: str?
TZ: str?
aliasgroup1: str
cert_domain: bool?
aliasgroup2: str?
aliasgroup3: str?
cert_domain: str?
certfile: str
dictionaries: str?
domain1: str?
@@ -42,8 +44,9 @@ schema:
password: password
server_name: str?
ssl: bool
ssl_termination: bool?
username: str
slug: collabora
url: https://github.com/alexbelgium/hassio-addons
version: "1.3.0"
version: "26.04.2.4.1"
webui: "[PROTO:ssl]://[HOST]:[PORT:9980]/browser/dist/admin/admin.html"

View File

@@ -2,9 +2,58 @@
# shellcheck shell=bash
set -e
if bashio::config.has_value 'domain'; then
domain="$(bashio::config 'domain')"
export domain
# coolwsd matches storage.wopi.alias_groups host/alias entries as regular
# expressions, so every dot has to be escaped with a single backslash. The value
# is typed by hand in the add-on options, where it is easy to end up with no
# escaping at all or with doubled backslashes, and a wrong pattern silently
# never matches: Collabora then refuses the Nextcloud host. Accept all three
# spellings and always hand coolwsd the canonical single-escaped form.
REGEX_METACHARACTERS='][(){}|*+?^$'
normalise_wopi_host() {
local value="$1"
# A value containing regex metacharacters was written by someone who knows
# what they are doing, leave it exactly as-is.
if [[ "$value" == *["$REGEX_METACHARACTERS"]* ]]; then
printf '%s' "$value"
return
fi
value="${value//\\/}" # drop whatever escaping was typed, at any depth
value="${value//./\\.}" # re-escape every dot exactly once
printf '%s' "$value"
}
# server_name is a literal "hostname[:port]", not a regex and not a URL
normalise_server_name() {
local value="$1"
value="${value//\\/}" # never escaped, drop backslashes if any were copied over
value="${value#*://}" # strip the scheme
value="${value%%/*}" # strip any path
printf '%s' "$value"
}
for index in 1 2 3; do
if bashio::config.has_value "aliasgroup${index}"; then
aliasgroup="$(normalise_wopi_host "$(bashio::config "aliasgroup${index}")")"
export "aliasgroup${index}=${aliasgroup}"
bashio::log.info "Allowed Nextcloud host aliasgroup${index}: ${aliasgroup}"
fi
done
if bashio::config.has_value 'server_name'; then
server_name="$(normalise_server_name "$(bashio::config 'server_name')")"
export server_name
elif bashio::config.has_value 'domain1'; then
# domain1 predates server_name and was documented as "the Collabora external
# domain", which is what server_name means to coolwsd. It was never actually
# passed to Collabora, so honour it here rather than keep ignoring it.
server_name="$(normalise_server_name "$(bashio::config 'domain1')")"
export server_name
bashio::log.warning "domain1 is deprecated, please use server_name instead"
fi
if [ -n "${server_name:-}" ]; then
bashio::log.info "Collabora public hostname (server_name): ${server_name}"
fi
if bashio::config.has_value 'username'; then
@@ -17,9 +66,9 @@ if bashio::config.has_value 'password'; then
export password
fi
if bashio::config.has_value 'aliasgroup1'; then
aliasgroup1="$(bashio::config 'aliasgroup1')"
export aliasgroup1
if bashio::config.has_value 'cert_domain'; then
cert_domain="$(bashio::config 'cert_domain')"
export cert_domain
fi
if bashio::config.has_value 'dictionaries'; then
@@ -32,6 +81,12 @@ if bashio::config.has_value 'extra_params'; then
extra_params="$(bashio::config 'extra_params')"
fi
# The add-on ssl option is authoritative. coolwsd defaults ssl.enable to true,
# so merely clearing extra_params used to re-enable its self-signed HTTPS even
# when ssl was false, which breaks reverse proxies expecting plain HTTP.
extra_params="${extra_params//--o:ssl.enable=false/}"
extra_params="${extra_params//--o:ssl.enable=true/}"
if bashio::config.true 'ssl'; then
export DONT_GEN_SSL_CERT=true
bashio::config.require.ssl
@@ -45,16 +100,31 @@ if bashio::config.true 'ssl'; then
bashio::log.error "Key file /ssl/${keyfile} not found"
exit 1
fi
cp -f /ssl/${keyfile} /etc/coolwsd/key.pem
cp -f /ssl/${certfile} /etc/coolwsd/cert.pem
cp -f /ssl/${certfile} /etc/coolwsd/ca-chain.cert.pem
extra_params="${extra_params/--o:ssl.enable=false/}"
# Point Collabora at the copies rather than at /ssl. coolwsd runs as uid
# 1001 and /ssl is mounted read-only with whatever ownership the certificate
# tooling left behind, which for a private key is commonly root-only. These
# copies are picked up by the chown below, so they are readable regardless.
cp -f "/ssl/${keyfile}" /etc/coolwsd/key.pem
cp -f "/ssl/${certfile}" /etc/coolwsd/cert.pem
cp -f "/ssl/${certfile}" /etc/coolwsd/ca-chain.cert.pem
chmod 600 /etc/coolwsd/key.pem
extra_params="${extra_params} \
--o:ssl.enable=true
--o:ssl.enable=true \
--o:ssl.termination=false \
--o:ssl.cert_file_path=/ssl/${certfile} \
--o:ssl.key_file_path=/ssl/${keyfile} \
--o:ssl.ca_file_path=/ssl/${certfile}"
--o:ssl.cert_file_path=/etc/coolwsd/cert.pem \
--o:ssl.key_file_path=/etc/coolwsd/key.pem \
--o:ssl.ca_file_path=/etc/coolwsd/ca-chain.cert.pem"
else
extra_params="${extra_params} --o:ssl.enable=false"
if [[ "$extra_params" != *ssl.termination* ]]; then
# With SSL disabled, termination must be enabled when a reverse proxy
# exposes Collabora over https, otherwise it advertises http/ws URLs.
if bashio::config.true 'ssl_termination'; then
extra_params="${extra_params} --o:ssl.termination=true"
elif ! bashio::config.has_value 'ssl_termination'; then
bashio::log.notice "If Collabora is reached over https through a reverse proxy, set ssl_termination to true"
fi
fi
fi
export extra_params
@@ -83,4 +153,9 @@ chown -R 1001 /etc/coolwsd
chmod -R 755 /opt/cool/systemplate
bashio::log.info "Starting Collabora Online..."
su -p -s /bin/bash "$(getent passwd 1001 | cut -d: -f1)" -c "/start-collabora-online.sh"
# coolwsd refuses to run as root. The official image used to ship
# /start-collabora-online.sh, which is gone since it became distroless, so the
# add-on provides its own launcher. It reads everything from the environment,
# which su -p preserves.
export HOME=/opt/cool
su -p -s /bin/bash cool -c /usr/local/bin/collabora-run.sh

View File

@@ -0,0 +1,55 @@
#!/bin/bash
# shellcheck shell=bash
#
# Launch coolwsd.
#
# The official image used to ship /start-collabora-online.sh and set it as its
# entrypoint. Since the move to a distroless image that script is gone, so the
# add-on provides its own equivalent. It is invoked as uid 1001 by
# /etc/cont-init.d/99-run.sh and takes everything from the environment, which
# avoids re-quoting extra_params through su.
set -e
# Collabora serves https itself unless the add-on already installed real
# certificates, in which case 99-run.sh exports DONT_GEN_SSL_CERT.
cert_params=""
if [ -z "${DONT_GEN_SSL_CERT:-}" ]; then
SSL_DIR="/tmp/ssl"
mkdir -p "${SSL_DIR}/certs/ca" "${SSL_DIR}/certs/servers/localhost" "${SSL_DIR}/certs/tmp"
openssl genrsa -out "${SSL_DIR}/certs/ca/root.key.pem" 2048
openssl req -x509 -new -nodes \
-key "${SSL_DIR}/certs/ca/root.key.pem" -days 9131 \
-out "${SSL_DIR}/certs/ca/root.crt.pem" \
-subj "/C=DE/ST=BW/L=Stuttgart/O=Dummy Authority/CN=Dummy Authority"
openssl genrsa -out "${SSL_DIR}/certs/servers/localhost/privkey.pem" 2048
openssl req -new -sha256 \
-key "${SSL_DIR}/certs/servers/localhost/privkey.pem" \
-out "${SSL_DIR}/certs/tmp/localhost.csr.pem" \
-subj "/C=DE/ST=BW/L=Stuttgart/O=Dummy Authority/CN=${cert_domain:-localhost}"
openssl x509 -req -days 9131 \
-in "${SSL_DIR}/certs/tmp/localhost.csr.pem" \
-CA "${SSL_DIR}/certs/ca/root.crt.pem" \
-CAkey "${SSL_DIR}/certs/ca/root.key.pem" -CAcreateserial \
-out "${SSL_DIR}/certs/servers/localhost/cert.pem"
cert_params="--o:ssl.cert_file_path=${SSL_DIR}/certs/servers/localhost/cert.pem \
--o:ssl.key_file_path=${SSL_DIR}/certs/servers/localhost/privkey.pem \
--o:ssl.ca_file_path=${SSL_DIR}/certs/ca/root.crt.pem"
fi
# Flags mirror the entrypoint of the official image. extra_params is expanded
# last so that add-on options and user overrides win.
# shellcheck disable=SC2086
exec /usr/bin/coolwsd \
--version \
--use-env-vars \
${cert_params} \
--o:sys_template_path=/opt/cool/systemplate \
--o:child_root_path=/opt/cool/child-roots \
--o:file_server_root_path=/usr/share/coolwsd \
--o:cache_files.path=/opt/cool/cache \
--o:logging.color=false \
--o:stop_on_config_change=true \
${extra_params:-}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,8 +1,9 @@
{
"last_update": "2026-07-16",
"github_exclude": "sha256",
"last_update": "2026-07-26",
"repository": "alexbelgium/hassio-addons",
"slug": "collabora",
"source": "github",
"upstream_repo": "CollaboraOnline/online",
"upstream_version": "1.3.0"
"source": "dockerhub",
"upstream_repo": "collabora/code",
"upstream_version": "26.04.2.4.1"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,3 +1,5 @@
## 2.63.18.5 (26-07-2026)
- Fixed healthcheck log spam when ssl is enabled (#2881)
## 2.63.18.4 (22-07-2026)
- Minor bugs fixed
## 2.63.18.3 (22-07-2026)

View File

@@ -134,4 +134,4 @@ HEALTHCHECK \
--retries=5 \
--start-period=30s \
--timeout=25s \
CMD if [ "$ssl" = "true" ]; then curl -f -k https://127.0.01:${HEALTH_PORT}${HEALTH_URL}; else curl -f http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}/; fi
CMD curl -A "HealthCheck: Docker/1.0" -s -f -k "$(cat /run/health_protocol 2>/dev/null || echo http)://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}/" >/dev/null 2>&1 || exit 1

View File

@@ -126,4 +126,4 @@ schema:
slug: filebrowser
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "2.63.18.4"
version: "2.63.18.5"

View File

@@ -51,6 +51,10 @@ if bashio::config.true 'ssl'; then
ADDON_PROTOCOL=https
fi
# Expose the protocol to the docker HEALTHCHECK, which runs outside this
# shell and therefore cannot read the addon options
echo -n "${ADDON_PROTOCOL}" > /run/health_protocol
#port=$(bashio::addon.port 80)
ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show More