Compare commits

..

22 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
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
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
186 changed files with 1708 additions and 124 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 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

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.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

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

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.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.6 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: 2.9 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.1 KiB

After

Width:  |  Height:  |  Size: 1.5 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.5 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.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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