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:
Alexandre
2026-08-20 11:18:40 +02:00
committed by GitHub
parent 4f8c0f2ed1
commit 9960a04304
5 changed files with 188 additions and 26 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"