mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-07 02:13:32 +02:00
fix(claude_desktop): install the complete Codex package so tool calls work (#3000)
* fix(claude_desktop): install the complete Codex package, not just the executable Since codex-cli 0.147.0 the CLI delegates every shell and file-read tool call to a companion codex-code-mode-host binary that it looks up next to its own executable. 81-codex_cli.sh downloaded the codex-<target>.tar.gz release asset, which contains only the codex executable, so that binary was never installed and every tool call failed with "failed to spawn code-mode host ...: No such file or directory" while the run still exited 0. Download the codex-package-<target>.tar.gz asset instead — the complete package tree upstream's own installer uses — and install all of it into the existing /data/codex prefix, which already satisfies Codex's layout contract. Make the "already installed" test require the code-mode host and the package manifest so existing incomplete installs repair themselves, and report layout completeness in claude-tools-doctor.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(claude_desktop): advertise Codex only when its package tree is complete 82-claude_tools.sh registers the Codex MCP server whenever the launcher at /data/codex/bin/codex is executable and re-checks nothing else, while the launcher and the package tree persist in /data independently of each other. Three paths therefore reached that launcher next to an install that cannot run a tool call: a boot that cannot reach the release metadata and keeps a pre-existing install missing the code-mode host or the manifest, the same boot finding a stamp-less tree left by an interrupted replacement, and a launcher surviving from an earlier boot after the install was dropped. All three reproduced against the real script with stubbed bashio/s6 and an unreachable metadata endpoint. Define completeness once (executable, code-mode host, package manifest, version stamp) and gate the launcher on it, removing the launcher and the /usr/local/bin symlink when it does not hold. Nothing else is deleted, so a later boot completes the install without another download or another login. The doctor's layout check now includes the stamp for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(claude_desktop): install the Codex package tree by position, not by name The whole reason for downloading the 118 MB codex-package asset instead of the lone executable is that a helper Codex needs must not be left out — that is the bug this branch fixes. install_codex_package still enumerated the five paths release 0.148.0 happens to ship, so a helper added by a future release would be downloaded, extracted and then discarded, failing exactly the way the missing code-mode host does today. Verified against the extracted function: with a staged tree carrying an unknown bin/ helper and an unknown top-level directory, the previous code installed neither. Move whatever the archive contains instead: every staged entry beside bin/ into /data/codex, every staged bin/ entry except the entrypoint into /data/codex/bin, then the entrypoint to codex-real last, so the ordering guarantee the stamp relies on is unchanged. Only paths the archive actually contains are touched, because /data/codex also holds this install's staging directory, and the existing launcher is skipped by name while the version stamp is a dot file that no glob matches. Removing each destination before moving onto it also drops files an older release left behind. Exercised with a scaffold around the extracted function: fresh install with unknown helpers present, upgrade over an existing install with a stale helper and a launcher to preserve, a minimal package with no optional directories, and an unwritable prefix to confirm failure is reported rather than swallowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(claude_desktop): scope the two deletion claims to what the code does Both overstated. install_codex_package() replaces every path the new release ships, but does not prune a path upstream stops shipping, so "files an older release left behind are removed with it" was wrong for exactly that case; and "nothing is deleted beyond the launcher" read as if the /usr/local/bin/codex symlink named in the previous sentence survived, when it is removed with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,62 @@
|
||||
## 07308545.3 (19-08-2026)
|
||||
- Fix an incompletely installed Codex CLI, which silently broke every Codex tool call. Since
|
||||
codex-cli 0.147.0 the CLI does not execute shell commands or file reads itself; it delegates
|
||||
them to a companion `codex-code-mode-host` binary that it looks up next to its own executable.
|
||||
`81-codex_cli.sh` downloaded the `codex-<target>.tar.gz` release asset, which contains only the
|
||||
`codex` executable, so that companion binary was never installed. Measured on the running add-on
|
||||
(codex-cli 0.147.0, `/data/codex/bin` holding only `.version`, the launcher and `codex-real`):
|
||||
`codex exec` starts, authenticates and answers, but every tool call fails with
|
||||
`failed to spawn code-mode host /data/codex/bin/codex-code-mode-host: No such file or directory`
|
||||
and the run still exits 0 — so Codex answered from the prompt text alone and the failure looked
|
||||
like success. `--disable code_mode` does not avoid it.
|
||||
- The installer now downloads the `codex-package-<target>.tar.gz` release asset, which is the
|
||||
complete package tree upstream's own installer uses, and installs all of it. Not a list of
|
||||
known file names: whatever the archive contains is moved into place by position, so a helper
|
||||
added by a future release arrives beside the entrypoint on its own instead of being extracted
|
||||
and then dropped — cherry-picking today's two binaries works today, but it is the same mistake
|
||||
at a smaller scale. For release 0.148.0 that means the entrypoint as
|
||||
`/data/codex/bin/codex-real`, `codex-code-mode-host` beside it, and `codex-package.json`,
|
||||
`codex-resources/` (bundled bubblewrap and zsh) and `codex-path/` (bundled ripgrep) in
|
||||
`/data/codex`. The installed tree grows from ~246 MB to ~300 MB, and `/data/codex` is now
|
||||
explicitly add-on-owned in its entirety: every path the new release ships replaces the
|
||||
installed copy of that path outright rather than merging into it, so nothing should be kept
|
||||
there by hand. A path upstream stops shipping altogether is not pruned — it is left behind as
|
||||
dead weight that the new entrypoint no longer looks for. Codex's own state stays in
|
||||
`~/.codex` and is never touched.
|
||||
- An incomplete install is no longer advertised. `82-claude_tools.sh` registers the Codex MCP
|
||||
server whenever the launcher at `/data/codex/bin/codex` is executable and re-checks nothing
|
||||
else, and both the launcher and the package tree persist in `/data` independently of each
|
||||
other. The launcher is therefore now written only for an install that has its executable, its
|
||||
code-mode host, its package manifest and its version stamp, and is removed together with the
|
||||
`/usr/local/bin/codex` symlink otherwise. The stamp is part of that test because it is deleted
|
||||
before the first file of a replacement is moved and written after the last, so a stamp-less
|
||||
prefix is exactly the tree that may mix two releases. This covers the cases that reach the
|
||||
launcher without a fresh install: a boot that cannot reach the release metadata and finds a
|
||||
pre-existing incomplete install, and a launcher left behind by an interrupted replacement.
|
||||
Nothing under `/data/codex` is deleted beyond that launcher — the executable, the package
|
||||
tree and the ChatGPT sign-in stay, so a later boot completes the install without another
|
||||
download or another login.
|
||||
- That layout is load-bearing, so the install prefix was chosen to satisfy it rather than
|
||||
changed. Codex canonicalises its own executable path, requires the parent directory to be
|
||||
named `bin`, and reads the manifest and helper directories from that directory's parent — the
|
||||
existing `/data/codex/bin` prefix already matches, and the executable's file name is not part
|
||||
of the contract, so `codex-real` and the subscription-only `codex` launcher wrapping it are
|
||||
both unchanged, as is the `/usr/local/bin/codex` symlink and the MCP registration.
|
||||
- Existing installs repair themselves. The "already installed, skip the download" test now also
|
||||
requires the code-mode host and the package manifest to be present, so an add-on that already
|
||||
has a working `codex-real` and no helpers reinstalls on the next start instead of staying
|
||||
quietly broken.
|
||||
- `claude-tools-doctor.sh` now reports whether the package layout is complete, because the
|
||||
failure mode this fixes is invisible in `codex --version`, in the version stamp and in the
|
||||
exit code.
|
||||
- Known limitation, unchanged by this release and not caused by it: Codex's own Linux sandbox
|
||||
cannot start in this container. Running both the system `bwrap` 0.8.0 and the bundled one
|
||||
directly with `--dev-bind / / --unshare-net /bin/true` fails identically with
|
||||
`bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`, so it is a container capability
|
||||
limitation rather than a packaging one. With the shipped `codex_sandbox_mode: workspace-write`
|
||||
default, tool calls therefore still fail with that bwrap error; `danger-full-access` is the only
|
||||
mode that currently executes commands, and the container is already the security boundary.
|
||||
|
||||
## 07308545.1 (17-08-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
@@ -209,14 +209,25 @@ 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.
|
||||
Codex is not baked into the image because its Linux distribution 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 package into
|
||||
persistent `/data/codex` only when the installed release is missing, incomplete
|
||||
or outdated, verifies the GitHub-published SHA-256 digest before extraction or
|
||||
execution, and validates the staged package with `--version` before it replaces
|
||||
the installed one. The complete upstream package is installed, not just the
|
||||
`codex` executable: Codex delegates every shell and file-read tool call to a
|
||||
companion `codex-code-mode-host` binary that it looks up next to itself, so an
|
||||
executable installed on its own can answer but can never run anything. If
|
||||
release metadata or the download is unavailable, startup continues and a
|
||||
previously working installation is retained.
|
||||
|
||||
`/data/codex` belongs to the add-on: everything below it — `bin/`,
|
||||
`codex-package.json`, `codex-resources/` and `codex-path/` — is replaced as a
|
||||
unit whenever a new release is installed, so it is not a place to keep files by
|
||||
hand. Codex's own state (`auth.json`, `config.toml`) lives in `~/.codex` and is
|
||||
never touched by an install. The installed package is roughly 300 MB, and an
|
||||
upgrade briefly needs room for the archive and both releases at once.
|
||||
|
||||
### Signing in with a ChatGPT subscription
|
||||
|
||||
|
||||
@@ -136,5 +136,5 @@ schema:
|
||||
slug: claude_desktop
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "07308545.1"
|
||||
version: "07308545.3"
|
||||
video: true
|
||||
|
||||
@@ -10,10 +10,20 @@ set -o pipefail
|
||||
# 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 is distributed as a package tree, not as a lone executable: since 0.147.0 every shell and
|
||||
# file-read tool call is executed by a companion binary, codex-code-mode-host, that Codex looks up
|
||||
# next to itself. Upstream publishes that tree as the codex-package-<target> release asset, and the
|
||||
# whole tree is installed here. Its layout is load-bearing and must not be flattened: Codex
|
||||
# canonicalises its own executable path, requires the parent directory to be named `bin`, and then
|
||||
# reads codex-package.json, codex-resources/ and codex-path/ from that directory's parent. The
|
||||
# executable's file name is not part of that contract, which is why codex-real keeps its name.
|
||||
CODEX_ROOT="/data/codex"
|
||||
CODEX_PREFIX="${CODEX_ROOT}/bin"
|
||||
CODEX_BIN="${CODEX_PREFIX}/codex"
|
||||
CODEX_REAL="${CODEX_PREFIX}/codex-real"
|
||||
CODEX_HOST="${CODEX_PREFIX}/codex-code-mode-host"
|
||||
CODEX_MANIFEST="${CODEX_ROOT}/codex-package.json"
|
||||
CODEX_STAMP="${CODEX_PREFIX}/.version"
|
||||
CODEX_LINK="/usr/local/bin/codex"
|
||||
CODEX_RELEASE_API="https://api.github.com/repos/openai/codex/releases/latest"
|
||||
@@ -28,6 +38,60 @@ run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$@"
|
||||
}
|
||||
|
||||
# What "installed" means, in one place. A Codex that is missing its code-mode host, its package
|
||||
# manifest or its version stamp still starts, authenticates and answers — it simply cannot run a
|
||||
# single tool call — so presence of the executable alone is not a usable install. The stamp counts
|
||||
# because it is removed before the first file of a replacement is moved and written after the last,
|
||||
# so its absence next to an executable means the tree may mix two releases.
|
||||
codex_install_is_complete() {
|
||||
[ -x "$CODEX_REAL" ] \
|
||||
&& [ -x "$CODEX_HOST" ] \
|
||||
&& [ -f "$CODEX_MANIFEST" ] \
|
||||
&& [ -f "$CODEX_STAMP" ]
|
||||
}
|
||||
|
||||
# Move a verified package tree from staging into the install prefix. Called only from an `if`
|
||||
# condition, where `set -e` does not apply, so every step reports failure explicitly.
|
||||
#
|
||||
# Whatever the package ships is installed, rather than the file names this add-on happens to know
|
||||
# about today: a helper added by a future release has to arrive beside codex-real on its own, or it
|
||||
# fails exactly the way the missing code-mode host did. Only paths the archive actually contains are
|
||||
# touched — /data/codex also holds this install's staging directory, so the tree below it is never
|
||||
# cleared wholesale.
|
||||
#
|
||||
# The long, failure-prone part of an install — the download and its digest check — is already done
|
||||
# by the time this runs; what is left is same-filesystem renames of an already validated tree. They
|
||||
# are not one atomic operation, so the version stamp is removed first: any interruption leaves a
|
||||
# stamp-less prefix, which the next boot treats as "not installed" and replaces wholesale. The
|
||||
# entrypoint is moved last, so a prefix whose codex-real is the new release is a prefix whose
|
||||
# helper binaries are the new release too.
|
||||
install_codex_package() {
|
||||
local staged="$1"
|
||||
local entry name
|
||||
rm -f -- "$CODEX_STAMP" || return 1
|
||||
# Everything beside bin/ first — the manifest and the helper directories (codex-resources/ and
|
||||
# codex-path/ today, holding bubblewrap, zsh and ripgrep) — then everything the package puts in
|
||||
# bin/ except the entrypoint, then the entrypoint. The existing launcher and version stamp are
|
||||
# never matched: the launcher is skipped by name and the stamp is a dot file.
|
||||
for entry in "${staged}"/*; do
|
||||
name="${entry##*/}"
|
||||
if [ ! -e "$entry" ] || [ "$name" = "bin" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf -- "${CODEX_ROOT:?}/${name}" || return 1
|
||||
mv -f -- "$entry" "${CODEX_ROOT}/${name}" || return 1
|
||||
done
|
||||
for entry in "${staged}"/bin/*; do
|
||||
name="${entry##*/}"
|
||||
if [ ! -e "$entry" ] || [ "$name" = "codex" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf -- "${CODEX_PREFIX:?}/${name}" || return 1
|
||||
mv -f -- "$entry" "${CODEX_PREFIX}/${name}" || return 1
|
||||
done
|
||||
mv -f -- "${staged}/bin/codex" "$CODEX_REAL" || return 1
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -44,7 +108,7 @@ case "$(uname -m)" in
|
||||
;;
|
||||
esac
|
||||
|
||||
CODEX_ASSET="codex-${CODEX_TARGET}.tar.gz"
|
||||
CODEX_ASSET="codex-package-${CODEX_TARGET}.tar.gz"
|
||||
mkdir -p "$CODEX_PREFIX"
|
||||
|
||||
# Migrate the PR's earlier direct-binary layout to the enforced wrapper layout without another
|
||||
@@ -116,38 +180,47 @@ PY
|
||||
fi
|
||||
|
||||
if [ -z "$release_info" ]; then
|
||||
if [ -x "$CODEX_REAL" ] && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
|
||||
if codex_install_is_complete && 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
|
||||
bashio::log.warning "Unable to resolve the latest verified Codex release; the installed Codex is missing or incomplete and stays unavailable until a boot can reach the release metadata"
|
||||
fi
|
||||
else
|
||||
IFS=$'\t' read -r CODEX_WANTED CODEX_SHA256 CODEX_URL <<< "$release_info"
|
||||
|
||||
if [ -x "$CODEX_REAL" ] \
|
||||
# An install is complete only if the code-mode host and the package manifest are there too:
|
||||
# every install made before this add-on switched to the package asset has a working codex-real
|
||||
# and no helpers, and repairs itself here rather than needing a fresh /data. Running the binary
|
||||
# also rejects one built for another architecture, which a restored backup could leave behind.
|
||||
if codex_install_is_complete \
|
||||
&& [ "$(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}"
|
||||
staged="${codex_tmp}/package"
|
||||
|
||||
# Fail open for add-on startup but fail closed for the candidate binary: its official
|
||||
# Fail open for add-on startup but fail closed for the candidate release: 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" \
|
||||
# after the staged tree is complete and its entrypoint successfully runs. The candidate is
|
||||
# exercised in staging with its own codex-package.json and helper directories in place, so
|
||||
# the layout Codex will resolve at runtime is the layout that was validated.
|
||||
if mkdir -p "$staged" \
|
||||
&& chmod 0755 "$staged" \
|
||||
&& 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
|
||||
&& tar -xzf "$archive" -C "$staged" \
|
||||
&& [ -f "${staged}/codex-package.json" ] \
|
||||
&& [ -f "${staged}/bin/codex" ] \
|
||||
&& [ -f "${staged}/bin/codex-code-mode-host" ] \
|
||||
&& chmod 0755 "${staged}/bin/codex" "${staged}/bin/codex-code-mode-host" \
|
||||
&& run_as_runtime_user "${staged}/bin/codex" --version > /dev/null 2>&1 \
|
||||
&& install_codex_package "$staged"; 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
|
||||
elif codex_install_is_complete; 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"
|
||||
@@ -155,7 +228,16 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$CODEX_REAL" ]; then
|
||||
# The launcher is the add-on's single "Codex is usable" signal: 82-claude_tools.sh registers the
|
||||
# Codex MCP server when it is executable and re-checks nothing else. Write it only for a complete
|
||||
# install, and remove it — together with the PATH symlink — for an incomplete one. Both the launcher
|
||||
# and the package tree live in /data and survive restarts independently, so a launcher left from an
|
||||
# earlier boot would otherwise outlive the install it was written for and advertise a Codex whose
|
||||
# every tool call fails. The executable, the package tree and the ChatGPT sign-in are all left in
|
||||
# place: a later boot completes the install without another download or another login.
|
||||
if ! codex_install_is_complete; then
|
||||
rm -f -- "$CODEX_BIN" "$CODEX_LINK"
|
||||
bashio::log.warning "Codex is not completely installed; not registering it this boot"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -183,6 +183,16 @@ if bashio::config.true 'install_codex_cli'; then
|
||||
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')"
|
||||
# Codex runs every shell and file-read tool call through this companion binary. When it is
|
||||
# absent the CLI still starts, authenticates and answers, but each tool call fails and the
|
||||
# run still exits 0 — so report it explicitly rather than leaving it to be inferred.
|
||||
if [ -x /data/codex/bin/codex-code-mode-host ] \
|
||||
&& [ -f /data/codex/codex-package.json ] \
|
||||
&& [ -f /data/codex/bin/.version ]; then
|
||||
printf '%-30s %s\n' "package layout" "complete"
|
||||
else
|
||||
printf '%-30s %s\n' "package layout" "INCOMPLETE - tool calls will fail; restart the add-on to reinstall"
|
||||
fi
|
||||
printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified"
|
||||
printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user