Skip to content

[Bug]: Codex "Update now" runs npm install -g against installs it never verified are npm-managed, causing a permanent "still needs an update" loop #5629

Description

@hkarlsen06

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Install the Codex CLI with OpenAI's standalone installer, which places a symlink at ~/.local/bin/codex pointing into ~/.codex/packages/standalone/current/bin/codex.
  2. Leave the Codex provider's binaryPath at its default bare value "codex" (packages/contracts/src/settings.ts:203).
  3. Wait for the standalone install to fall behind the latest @openai/codex npm release.
  4. Open T3 Code, and when the Codex provider reports an update, click Update now.
  5. Observe the result, then click Update now again — it repeats indefinitely.

Expected behavior

Either:

  • update the install T3 Code is actually probing (codex ships a codex update subcommand for exactly this install method), or
  • detect that the install method is not one T3 Code can drive, mark it canUpdate: false, and surface the correct manual command — the same way the path-separator branch already does.

In no case should T3 Code run a package-manager install command against an install it has no evidence that package manager owns.

Actual behavior

The update reports Provider still needs an update / Codex still appears outdated. Check provider settings for details. every time, forever.

What actually happens is worse than a cosmetic loop: T3 Code runs npm install -g @openai/codex@latest, which updates (or newly creates) a different codex install than the one it probes. On my machine it silently maintained an npm copy at /opt/homebrew/lib/node_modules/@openai/codex that I was never running, while the probe kept reading the standalone install on PATH.

Trace through origin/main:

  1. CodexDriver.ts:147 resolves maintenance capabilities with binaryPath: "codex".
  2. resolveProviderMaintenanceCapabilitiesEffect (providerMaintenance.ts:346) resolves that through PATH and realPaths it to ~/.codex/packages/standalone/releases/<ver>/bin/codex.
  3. In resolvePackageManagedProviderMaintenance (providerMaintenance.ts:266) that path matches no heuristic — not /node_modules/, not /Cellar/, not .bun/pnpm/vite-plus — and CodexDriver.ts:67 passes nativeUpdate: null, so the native branch is skipped too.
  4. Control reaches the fallback at providerMaintenance.ts:311: if (!hasPathSeparator(binaryPath)) return makeNpmGlobalProviderMaintenanceCapabilities(...). Because binaryPath is the bare string "codex", an unclassifiable install is assumed to be npm and gets canUpdate: true.
  5. providerMaintenanceRunner.ts:342 runs npm install -g @openai/codex@latest, which exits 0.
  6. providerMaintenanceRunner.ts:356-377 re-probes, still resolves the untouched standalone binary, still sees behind_latest, and correctly records status: "unchanged".
  7. ProviderUpdateLaunchNotification.logic.ts:279 renders the toast.

The runner's verification is working exactly as designed — it refuses to claim a success that did not happen. The defect is upstream of it, in capability resolution.

Two separable defects:

1. CodexDriver never opts into codex's native updater. ClaudeDriver.ts:63-82 already handles the byte-for-byte analogous layout for Claude:

nativeUpdate: {
  executable: "claude",
  args: ["update"],
  lockKey: "claude-native",
  isCommandPath: isClaudeNativeCommandPath,   // matches ~/.local/bin/claude, /.local/share/claude/
},

CodexDriver.ts:63-68 passes nativeUpdate: null despite codex update existing (codex --help: update — Update Codex to the latest version) and despite the standalone installer using the same ~/.local/bin convention. This reads as an unfinished port rather than a deliberate choice.

2. The bare-name fallback guesses instead of declining. The same binary produces opposite behavior depending only on how it is spelled in settings:

binaryPath Resolved real path Branch taken Result
"codex" ~/.codex/packages/standalone/.../bin/codex :311 npm-global, canUpdate: true → runs npm install -g
"/Users/me/.local/bin/codex" identical path :315 manual-only, canUpdate: false → no loop

Identical install, identical failed classification, opposite outcome. The path-separator branch gets it right; the bare-name branch converts "unknown install method" into "npm" and then mutates global state on that guess. Fixing defect 1 alone would leave this trap for the next unrecognized install layout.

Impact

Major degradation or frequent failure

Version or commit

origin/main @ 5661c6116 (apps/server 0.0.31). Confirmed present on current main — providerMaintenance.ts, CodexDriver.ts and providerMaintenanceRunner.ts are unchanged there.

Environment

  • macOS 26.6.0, aarch64
  • Codex CLI 0.146.1 via OpenAI standalone installer at ~/.local/bin/codex → ~/.codex/packages/standalone/current/bin/codex
  • @openai/codex@latest on npm at the time: 0.147.0
  • Codex provider binaryPath: default "codex"
  • A second, npm-global codex also present at /opt/homebrew/bin/codex — but note this reproduces with no npm copy at all, in which case npm install -g creates one the user never asked for.

Logs or stack traces

codex doctor reports the install method unambiguously, and could be a cheap detection signal:

✓ install      consistent
  managed by npm: yes · bun: no · pnpm: no · package root /opt/homebrew/lib/node_modules/@openai/codex
  npm update target   /opt/homebrew/lib/node_modules/@openai/codex

On a standalone install the same command reports an installer-managed root instead.

Workaround

Point the provider at an install T3 Code can classify — set binaryPath to the absolute npm shim path (e.g. /opt/homebrew/bin/codex), whose realPath lands under /lib/node_modules/ and so matches isNpmGlobalCommandPath. This keeps canUpdate: true and makes the provider immune to PATH shadowing. Removing the shadowing standalone symlink also works but leaves the underlying guess in place.

Prior report

#3550 describes the same user-visible symptom and was closed NOT_PLANNED. It appears never to have been triaged on merit — the sole comment is the reporter noting a bot had filed spurious issues, and this got swept up in that cleanup.

Its stated hypothesis was missing Bun support, and that hypothesis was almost certainly wrong: isBunGlobalCommandPath landed 2026-05-05 in #2312, roughly seven weeks before #3550 was filed. Bun was already handled. That reporter most likely hit this same :311 fallback and misattributed it — which is why I'm filing fresh rather than asking to reopen.

Suggested fix

Both parts are small:

// CodexDriver.ts — mirror ClaudeDriver
nativeUpdate: {
  executable: "codex",
  args: ["update"],
  lockKey: "codex-native",
  isCommandPath: (p) => {
    const n = normalizeCommandPath(p);
    return n.endsWith("/.local/bin/codex")
        || n.endsWith("/.local/bin/codex.exe")
        || n.includes("/.codex/packages/standalone/");
  },
},

realCommandPath is already threaded through resolveProviderMaintenanceCapabilitiesEffect (providerMaintenance.ts:366-375), so the /.codex/packages/standalone/ match fires through the symlink regardless of the link's name.

For defect 2, the :311 fallback should return makeManualOnlyProviderMaintenanceCapabilities once a command path was successfully resolved but matched nothing — reserving the npm assumption for the case where resolution failed entirely and there is genuinely nothing to go on. Surfacing "T3 Code can't auto-update this install method" is strictly better than silently npm install -g-ing a package the user may not have.

providerMaintenance.test.ts already covers the per-manager branches; these would slot in alongside.

Activity

  1. svarunid commented on Aug 19, 2026

    @svarunid

    On Windows standalone installs, codex update delegates to powershell.exe -ExecutionPolicy Bypass -c "$env:CODEX_NON_INTERACTIVE=1; irm .../install.ps1 | iex". Upstream Codex has an open Windows bug where powershell.exe inherits PowerShell 7 PSModulePath entries from its parent environment, causing module resolution (notably Get-FileHash) to fail.

    Relevant upstream issues:

    • openai/codex#27117 — identifies PSModulePath inheritance as the cause; removing/normalizing it before spawning Windows PowerShell fixes the reproduction.
    • openai/codex#30015 — reports the same failure from a real native Windows install during codex update.

    So if this issue is fixed by adding nativeUpdate: { executable: "codex", args: ["update"], ... }, I think the Windows path deserves a regression test / explicit consideration.

    There are two ownership options:

    1. Preferable: T3 invokes codex update and leaves PowerShell details to Codex. Once Windows standalone update from pwsh inherits PSModulePath into powershell.exe, causing Get-FileHash to fail openai/codex#27117 is fixed upstream, T3 automatically benefits.
    2. Defensive T3 workaround: if T3's environment passed to codex can contain a PowerShell-7 PSModulePath, sanitize that variable for the update subprocess on Windows. This avoids propagating an environment known to break Codex's subsequent powershell.exe child.

    I would avoid having T3 duplicate the install.ps1 invocation itself, since that hard-codes Codex's updater implementation into T3 and could drift from upstream.

    It may also be worth testing:

    • standalone Codex launched from a normal GUI/T3 environment;
    • T3 launched from Windows PowerShell 5.1;
    • T3 launched from PowerShell 7 with a PowerShell-7 PSModulePath;
    • npm-managed Codex, to ensure native-update detection doesn't steal that path;
    • an unknown/unclassified Codex install, which should become manual-only rather than falling back to npm.
  2. marcob896 commented on Aug 24, 2026

    @marcob896

    Confirmed on a current headless macOS Apple Silicon setup running T3 Code 0.0.34-nightly.20260824.1174.

    Setup:

    • Codex installed with OpenAI's official standalone installer
    • active executable: ~/.local/bin/codex -> ~/.codex/packages/standalone/releases/<version>-aarch64-apple-darwin/bin/codex
    • two T3 provider instances share that executable while using separate CODEX_HOME directories
    • both instances use an explicit absolute binaryPath

    When Codex 0.149.0 fell behind npm latest 0.149.1, both T3 snapshots reported:

    {
      "status": "behind_latest",
      "currentVersion": "0.149.0",
      "latestVersion": "0.149.1",
      "canUpdate": false,
      "updateCommand": null,
      "message": "Install the update now or review provider settings."
    }

    So T3 correctly avoided the unsafe npm fallback in this explicit-path variant, but the UI could only show the update advisory and offered no one-click update.

    The installed CLI exposes the supported native command:

    codex update    Update Codex to the latest version
    

    Running that command against the same standalone executable updated it successfully:

    Updating Codex CLI from 0.149.0 to 0.149.1
    Codex CLI 0.149.1 installed successfully.
    

    Both isolated profiles then resolved the shared executable as codex-cli 0.149.1 without changing their account homes.

    This is a real multi-instance confirmation that wiring nativeUpdate to the resolved standalone executable with args: ["update"] is the correct behavior. It also avoids creating or maintaining a second npm-owned Codex installation.

    Related implementation candidates already open: #5630 and #4065; the broader installation-aware work is in #6436.

  3. Dropje97 commented on Sep 1, 2026

    @Dropje97

    Confirming this on Linux, with a harder failure mode than the one in the report: the same root cause produces a non-zero exit instead of a silent no-op.

    Setup — Codex installed with OpenAI's standalone installer, exactly the layout described in the issue:

    ~/.local/bin/codex -> ~/.codex/packages/standalone/current/bin/codex
                       -> releases/0.151.0-x86_64-unknown-linux-musl
    
    $ codex --version
    codex-cli 0.151.0
    

    binaryPath left at the default bare "codex". npm's global prefix is ~/.local.

    What the UI shows: Provider update failed — Update command exited with code 1.

    Reproduced by hand, running the exact command the runner builds:

    $ npm install -g --allow-scripts=@openai/codex @openai/codex@latest
    npm error code EEXIST
    npm error path ~/.local/bin/codex
    npm error EEXIST: file already exists
    npm error File exists: ~/.local/bin/codex
    npm error Remove the existing file and try again, or run npm
    npm error with --force to overwrite files recklessly.
    

    Exit code 1. ~/.local/lib/node_modules/ contains no @openai/codex — npm never owned this install.

    Why this variant differs. The original report's machine had npm prefix /opt/homebrew, so npm install -g succeeded and quietly maintained a second, unused copy — hence the permanent "still needs an update" loop. Here npm's prefix is ~/.local, which is the same bin directory the standalone installer owns, so npm's bin-link step collides with the installer's symlink and aborts outright. Same defect, two symptoms, differing only in where npm's prefix happens to point:

    npm prefix collides with ~/.local/bin/codex? result
    elsewhere (e.g. /opt/homebrew) no exit 0, shadow install, permanent "still needs an update"
    ~/.local yes exit 1, EEXIST, "Update command exited with code 1"

    Neither is recoverable from the UI. codex update from a terminal works correctly in both cases, which is the fix this issue already proposes.

    Still present on current main: apps/server/src/provider/providerMaintenance.ts is unchanged, and apps/server/src/provider/Drivers/CodexDriver.ts:68 still passes nativeUpdate: null.

    Worth noting for anyone picking this up: #4065, which implements the nativeUpdate path, is currently CONFLICTING against main and has been idle since 2026-07-29.

  4. simonsteiner commented on Sep 1, 2026

    @simonsteiner

    Still reproduces on current main (c17d02cff); line refs have drifted since 5661c6116 — the bare-name fallback is now providerMaintenance.ts:321, and CodexDriver.ts:68 still passes nativeUpdate: null.

    #5646 also changed the emitted command, so what runs today is:

    npm install -g --allow-scripts=@openai/codex @openai/codex@latest
    

    (Adding only the drift check — the Linux repro and the codex update confirmation are already covered above by @Dropje97 and @marcob896.)

  5. toddmorey commented on Sep 1, 2026

    @toddmorey

    Another Linux variant, on Arch (CachyOS), where both providers are owned by the system package manager and npm's global prefix is root-owned. This fails harder than the cases above: no in-app action can ever succeed, because the command targets a directory the app's user cannot write.

    Setup

    $ pacman -Qo /usr/bin/claude
    /usr/bin/claude is owned by claude-code 2.1.251-1
    $ pacman -Qo /usr/bin/codex
    /usr/bin/codex is owned by openai-codex 0.151.0-1.1
    
    $ npm prefix -g
    /usr
    $ npm root -g
    /usr/lib/node_modules
    $ stat -c '%U:%G %a' /usr/lib/node_modules
    root:root 755

    binaryPath left at the default bare value for both providers.

    What the UI shows: Provider update failed — Update command exited with code 1.

    Interestingly this lands as two distinct defects, one per provider.

    codex — the bare-name npm assumption, exactly as reported

    There is no @openai scope in the global root at all:

    $ ls /usr/lib/node_modules/
    @anthropic-ai  node-gyp  nopt  npm  semver

    /usr/bin/codex is a 251 MB standalone binary shipped by the distro package. npm has never owned it. providerMaintenance.ts:321 classifies it as npm-managed purely because binaryPath has no path separator.

    claude — npm detection is right, but the target is unwritable, and the divergence in the issue title has already materialized

    There really is an npm copy:

    $ npm ls -g --depth=0
    /usr/lib
    ├── @anthropic-ai/claude-code@2.1.251
    ...
    
    $ pacman -Qo /usr/lib/node_modules/@anthropic-ai/claude-code
    error: No package owns /usr/lib/node_modules/@anthropic-ai/claude-code

    ...but it is not what PATH resolves to. /usr/bin/claude is a distro wrapper:

    #!/bin/sh
    export DISABLE_UPDATES=1
    export DISABLE_INSTALLATION_CHECKS=1
    exec /opt/claude-code/bin/claude "$@"

    So the probed install is /opt/claude-code/bin/claude (214 MB, pacman-owned), while the npm copy under /usr/lib/node_modules is an orphan nothing ever executes — the "silently maintained an npm copy I was never running" outcome from the original report, already present here before any update was attempted. An npm install -g would bump the orphan and leave the probe reading the untouched pacman install: the permanent loop in the title. It never gets that far, because the write fails first.

    Suggested check

    Worth noting that prefix containment will not separate these — /usr/bin/codex is inside npm prefix -g (/usr) while being entirely unrelated to npm. Two cheap checks would cover every variant reported on this issue:

    1. Before assuming npm ownership, confirm npm actually lists the package (npm ls -g --depth=0 --json, or that $(npm root -g)/<pkg> exists). That alone disqualifies codex here, and the standalone-installer cases above.
    2. Before setting canUpdate: true, confirm the process can write npm root -g and $(npm prefix -g)/bin. That covers this case and any root-owned or read-only prefix, where the right answer is an advisory plus the manual command rather than a button.

    Both are also the honest answer for a distro-managed install: T3 Code cannot drive pacman, and shouldn't try.

    Environment: T3 Code 0.0.38-1 (AUR t3code-bin), CachyOS (Arch), npm 12.0.2, Node 26.7.0.

  6. BearHuddleston commented on Sep 1, 2026

    @BearHuddleston
    Contributor

    Confirmed again on the current Linux nightly with the exact UI sequence shown in the report:

    • T3 Code 0.0.38-nightly.20260901.1250
    • Linux x86_64
    • Codex 0.152.0, installed by the official standalone installer
    • T3 offers Codex 0.152.1; clicking Update ends with Codex v0.152.1 update failed

    The active executable is:

    ~/.local/bin/codex -> ~/.codex/packages/standalone/current/bin/codex
                        -> ~/.codex/packages/standalone/releases/0.152.0-x86_64-unknown-linux-musl/bin/codex
    

    However, T3 advertises and runs:

    npm install -g --allow-scripts=@openai/codex @openai/codex@latest
    

    There is no npm-managed @openai/codex on this machine. Because npm's global prefix is ~/.local, its bin-link step collides with the standalone installer's existing symlink:

    npm error code EEXIST
    npm error path ~/.local/bin/codex
    npm error File exists: ~/.local/bin/codex
    

    After the failed action, the provider remains on 0.152.0 and continues advertising 0.152.1 with canUpdate: true and the npm command above. The installed CLI exposes the supported native command:

    codex update    Update Codex to the latest version
    

    This is the provider-maintenance parity gap with Claude: standalone Codex installs should use the resolved executable's native codex update path, as Claude's driver does for claude update. An unclassified install should be manual-only rather than assumed to be npm-owned.

  7. YoungPhlo commented on Sep 2, 2026

    @YoungPhlo

    Additional cross platform confirmation:

    • On Windows, clicking Update now errors.
    • On macOS, my active Codex installation is from OpenAI’s standalone installer. T3 nevertheless offered and ran npm install -g --allow-scripts=@openai/codex @openai/codex@latest.

    The npm command succeeded, but it installed a separate npm managed Codex instead of updating the standalone installer I actually use. This is undesirable even though it does not surface as an error on macOS: T3 mutates a second installation while leaving the active one untouched.

    For standalone installs, T3 should use codex update, or treat the installation as manual instead of falling back to npm.

    The attached screenshots show the npm command T3 offered and its execution after clicking Update now.

    Image Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions