From d60f17685ec8f02957d960622c72b6b621cb2206 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Fri, 11 Sep 2026 08:05:29 +0200 Subject: [PATCH] skill(hassio-addon-workflow): fix the stage-3 trap's actual mechanism (#3058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * skill(hassio-addon-workflow): fix the stage-3 trap's actual mechanism traps.md attributed the shipped svc-xorg failure to that service's own shebang ("starts #!/usr/bin/env bashio, not with-contenv"). Verified against .templates/ha_entrypoint.sh: that isn't what decides it. ha_entrypoint.sh rewrites the first line of every cont-init.d script and every services.d/s6-rc.d run script to one shebang chosen by probing candidate_shebangs in order. The first candidate is /command/with-contenv bashio; in add-ons that override the base ENTRYPOINT ["/init"] (confirmed live in ente, wger, free_games_claimer), s6 stage 1 never runs, that candidate fails, and the probe falls through to /usr/bin/env bashio for every service in the image, regardless of what shebang it shipped with. So no service reads stage 3 in that case, not just svc-xorg. Surfaced independently by two agents in an unrelated eval run for PR #3057 (both named the rewrite; neither had the conditionality right), then verified against the script and confirmed the ENTRYPOINT-override path is real rather than hypothetical before writing this. No add-on touched; references/ only. markdownlint clean. Co-Authored-By: Claude Sonnet 5 * skill(hassio-addon-workflow): scope the stage-3 trap to the PID-1-entrypoint path Flagged by both Codex and Copilot, reproduced against .templates/ha_entrypoint.sh before accepting. The previous commit's fix still overclaimed: it said ha_entrypoint.sh rewrites every service's shebang, universally. It doesn't. The services.d/s6-rc.d rewrite loop is gated on `if $PID1` (line 430) — it only runs when ha_entrypoint.sh itself is the container's PID 1, i.e. when the add-on replaces the base ENTRYPOINT ["/init"]. Under the normal /init path (the overwhelming majority of add-ons), ha_entrypoint.sh runs as the stage-2 hook with PID1=false, never touches service run files, s6's own stage 1 already created container_environment before any cont-init script ran, and a service's shipped with-contenv shebang reads it normally — the opposite of what the previous wording implied. cont-init.d scripts are unconditional either way (run_one_script has no $PID1 gate), so a cont-init script's own shebang was never informative and that half stands. Also fixed: the previous commit named wger as a live example of the ENTRYPOINT override. Copilot caught that wger's override is written into its Dockerfile but commented out — confirmed (`grep '^#ENTRYPOINT' wger/Dockerfile`). Only ente and free_games_claimer currently have it active; wger dropped from the list. markdownlint clean; no add-on touched. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: BirdNET-Go Addon Builder Co-authored-by: Claude Sonnet 5 --- .../hassio-addon-workflow/references/traps.md | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.claude/skills/hassio-addon-workflow/references/traps.md b/.claude/skills/hassio-addon-workflow/references/traps.md index 7ece6b86b3..aade569a08 100644 --- a/.claude/skills/hassio-addon-workflow/references/traps.md +++ b/.claude/skills/hassio-addon-workflow/references/traps.md @@ -70,10 +70,28 @@ service run scripts including `svc-xorg`, and Xvfb runs with `-vfbdevice /dev/dr - `00-global_var.sh` is cont-init **00**. Any cont-init script numbered higher runs *after* the injection, so it cannot change what a service will see through stage 2. -- LSIO's `svc-xorg` starts `#!/usr/bin/env bashio`, **not** `with-contenv`, so it never reads - stage 3 at all. Writing `container_environment` for it is a silent no-op — that shipped: the - file was written 6 seconds before Xvfb started, and Xvfb still came up at the base-image - default. +- **Whether a service's own shebang still decides stage 3 depends on whether `ha_entrypoint.sh` + runs as PID 1** — i.e. whether the add-on replaces the base `ENTRYPOINT ["/init"]` with one that + makes `ha_entrypoint.sh` itself the container's entrypoint (live today in `ente` and + `free_games_claimer`; `wger` has the override written into its Dockerfile but commented out, so + it is not currently one of these — check the Dockerfile, not this list). Under the normal + `/init` path this script runs as the stage-2 hook, `$PID1` is false, and it never touches + `services.d/*/run` or `s6-overlay/s6-rc.d/*/run` at all (`.templates/ha_entrypoint.sh:430`, + gated on `if $PID1`) — s6's own stage 1 already created `/run/s6/container_environment` before + any cont-init script ran, so a service's shipped `with-contenv` shebang reads it normally. + Only the `ENTRYPOINT`-override path breaks this, and it breaks it twice over: s6 stage 1 never + runs, so nothing ever creates the envdir; and because `ha_entrypoint.sh` is now PID 1, it + rewrites the first line of every service `run` file to whichever shebang its own + `candidate_shebangs` probe landed on. That probe's first candidate, + `/command/with-contenv bashio`, fails precisely because the envdir was never created, so it + falls through to `/usr/bin/env bashio` for every service — the real mechanism behind the shipped + `svc-xorg` failure (its envdir file was written 6 seconds before Xvfb started, and Xvfb still + came up at the base-image default). `cont-init.d` scripts are a separate case: `run_one_script` + rewrites their shebang unconditionally, with no `$PID1` gate, so a cont-init script's own + shebang is never informative either way. `ha_entrypoint.sh` dumps the environment itself to + compensate for the missing envdir, and the envdir writes in `00-global_var.sh` / + `01-config_yaml.sh` are `if [ -d ]` guarded, so they take effect only once something has created + that directory. **Renaming an option to match a base-image env var moves validation out of your script and into the schema.** `00-global_var.sh` exports empty strings (only objects/arrays/nulls are dropped),