Skip to content

[Bug]: macOS login-shell PATH capture times out at 5s and is recorded as Success, so every provider reports "not found on PATH" #10534

Open

Description

@willsheldon

Raised from downstream fleet tooling (new-trixie-box), found while debugging a Codex provider that would not spawn.

Area: apps/desktop

Summary

On macOS the bootstrap login-shell environment capture can time out at its 5000 ms bound against a perfectly ordinary zsh. The launchctl getenv PATH fallback is empty on a stock machine, so nothing is captured, and both parent spans still record exit: Success. The desktop process, the bundled server, and every provider spawned from it then run with launchd's default PATH (/usr/bin:/bin:/usr/sbin:/sbin) for the entire app run.

Nothing surfaces in the UI. The failure is reported to the user as a missing CLI instead.

Evidence

From ~/.t3/userdata/logs/desktop.trace.ndjson.* for a single app run (app started 15:18:40 local):

15:19:06.241  desktop.shellEnvironment.runCommandOutput          6105 ms
              DesktopShellEnvironmentCommandTimeoutError
              { "probe": "login-shell", "executable": "zsh", "argumentCount": 2, "timeoutMs": 5000 }
              "Desktop shell environment login-shell probe (zsh) timed out after 5000ms."   [WARN]
15:19:12.355  desktop.shellEnvironment.runCommandOutput           156 ms   (fallback probe, no value)
15:19:12.512  desktop.shellEnvironment.installPosixEnvironment   6270 ms   exit: Success
15:19:12.512  desktop.shellEnvironment.installIntoProcess        6270 ms   exit: Success

Effective PATH of every T3 process afterwards, via ps -wwEo command=:

/usr/bin:/bin:/usr/sbin:/sbin

The same capture run by hand on the same machine, warm, completes in about 1.1 s and returns the full PATH:

$ time /bin/zsh -ilc "printf '%s\n' '__T3CODE_ENV_PATH_START__'; printenv PATH || true; printf '%s\n' '__T3CODE_ENV_PATH_END__'"
...
1.127 total

So this is a timing failure at cold start, not a broken shell configuration. launchctl getenv PATH is empty here, and /var/db/com.apple.xpc.launchd/config/user.plist does not exist, which is the default state.

Impact

Anything spawned by a bare command name fails for the whole app run, and the app run is long: this one had been up 17 hours.

The user-visible message is misleading. The Codex provider reported:

Codex CLI (codex) was not found on PATH.

while codex 0.153.2 was installed at /opt/homebrew/bin/codex and worked in any terminal. The thread-level error was:

ProviderAdapterProcessError: Failed to spawn Codex App Server process for command: codex app-server

checkCodexProviderStatus produces that text from isCodexAppServerSpawnError, so a spawn failure caused by the app's own empty PATH is presented as a missing installation. Setting an absolute binaryPath in provider settings works around it per provider, and the probe then reports installed: true immediately.

Suggested changes

  1. Do not record a swallowed capture failure as exit: Success on installPosixEnvironment and installIntoProcess. The WARN event is only reachable by reading the trace file.
  2. Surface the failed capture in the UI, and distinguish it from a missing CLI. "The shell environment could not be captured" is actionable; "codex was not found on PATH" sends the user to reinstall a CLI that is already installed.
  3. Retry once before giving up, or raise the bound. 5000 ms is tight for a cold start where the login shell has not been paged in yet.
  4. Offer a way to re-run the capture without restarting the app.

Related, but not duplicates

Environment

  • T3 Code (Alpha) 0.0.99-gitcache.1
  • macOS 26.6.2 (25G83), Apple Silicon
  • Login shell zsh 5.9, SHELL=/bin/zsh
  • codex 0.153.2 at /opt/homebrew/bin/codex

Activity

  1. juliusmarminge commented on Sep 7, 2026

    @juliusmarminge
    Member

    Confirmed in source. This is a real macOS desktop bug, not a broken shell config, and it is not a duplicate of the nearby PATH tickets.

    What happens

    DesktopShellEnvironment.installIntoProcess runs once at desktop bootstrap (apps/desktop/src/app/DesktopApp.ts) and mutates process.env. The bundled server inherits that env (extendEnv: true in DesktopBackendConfiguration). Providers then spawn bare names against it.

    The login-shell probe is hard-capped at 5s:

    • LOGIN_SHELL_TIMEOUT = Duration.seconds(5) in apps/desktop/src/shell/DesktopShellEnvironment.ts
    • runCommandOutput logs DesktopShellEnvironmentCommandTimeoutError as WARN, then returns "" (error channel is never)
    • installPosixEnvironment / installIntoProcess therefore always complete, so both Effect spans record exit: Success

    That matches the posted traces (6105 ms timeout, then launchctl at 156 ms with no value, then both parents Success). On current macOS, launchctl getenv PATH is empty by default, so the merge keeps launchd’s /usr/bin:/bin:/usr/sbin:/sbin for the rest of the run.

    The server has a second chance (hydratePosixPath in apps/server/src/os-jank.ts via readEnvironmentFromLoginShell in packages/shared/src/shell.ts) with the same 5s execFileSync timeout. In this incident it also failed to repair PATH — every T3 process still showed the launchd PATH after 17 hours.

    There is no UI for a failed capture. “Rescan server environment” only re-runs discovery inside the already-hydrated server.

    Why Codex says “not found on PATH”

    checkCodexProviderStatus (apps/server/src/provider/Layers/CodexProvider.ts) maps any CodexAppServerSpawnError to installed: false and the exact string Codex CLI (\codex`) was not found on PATH.A spawn ENOENT caused by the app’s own PATH is indistinguishable from a missing install. An absolutebinaryPathbypasses PATH lookup, which is why that workaround reportsinstalled: true` immediately. Claude uses the same “not found on PATH” mapping.

    Related, not duplicates

    #1799 (closed #1787) added the login-shell + launchctl chain; the launchctl step is a no-op on this OS.

    Open PR #9232 is a useful last-resort mitigation (prepend known POSIX CLI dirs such as /opt/homebrew/bin when both probes return nothing). It would likely make Homebrew codex resolvable after this timeout. It does not cover Success-on-failure recording, a UI distinction from a missing CLI, a retry / timeout raise, or re-capture without restart. Do not treat #9232 as closing this issue.

    Suggested fix

    1. Stop recording a swallowed capture failure as Success on installPosixEnvironment / installIntoProcess.
    2. Retry once and/or raise the 5s bound — a warm zsh -ilc on the same machine is ~1.1s; cold start exceeded 5s.
    3. Surface a failed capture in the UI and distinguish it from a missing CLI (The shell environment could not be captured vs `codex` was not found on PATH).
    4. Offer a way to re-run capture without quitting the app (overlaps [Bug]: macOS desktop never refreshes the login-shell environment after launch — tools installed later (Homebrew, gh, opencode, nvm node) stay invisible to agents and VCS #9429; this ticket’s first need is a successful first capture).
  2. added
    via-triageFiled through npx t3 triage
    bugSomething is broken or behaving incorrectly.
    on Sep 7, 2026
  3. hinescreative commented on Oct 12, 2026

    @hinescreative
    Contributor

    Still reproduces on 0.0.46-nightly.20261012.3010 (macOS, Apple Silicon). Trigger here was a cold boot: T3 opened ~29 s after login while ~600 processes were starting. The login-shell probe took 6381 ms and hit DesktopShellEnvironmentCommandTimeoutError (timeoutMs 5000), the launchctl-path fallback returned nothing in 233 ms, and the server ran with PATH=/usr/bin:/bin:/usr/sbin:/sbin. Settings → Providers showed Codex, Claude, Grok and OpenCode as not found, and four Claude turns failed. The same shell starts in 0.5 s warm. Quitting and reopening T3 fixed it (probe 517 ms). LOGIN_SHELL_TIMEOUT is still seconds(5) with no retry in this build; one retry after startup settles would cover the boot case.

  4. stttkm commented on Oct 20, 2026

    @stttkm

    Note: This comment was written by Claude Code (Claude Opus 5.5), an AI agent, during a t3 triage session on the affected user's machine. The user reviewed it and approved posting it.

    Another repro on the desktop app 0.1.0 (T3 Code Beta), macOS 27.0 arm64. Unlike the other reports this was not a cold boot: it was an ordinary app relaunch while the machine was busy (load avg ~33-41, many agent sessions running).

    Timeline (local time, from desktop.trace.ndjson / server.trace.ndjson):

    • Previous launch: login-shell probe completed in 1983 ms, full PATH, Claude/gh worked all morning.
    • User relaunched the app. desktop.shellEnvironment.runCommandOutput hit DesktopShellEnvironmentCommandTimeoutError { probe: "login-shell", executable: "zsh", timeoutMs: 5000 } after 5555 ms.
    • The server process ran with PATH=/usr/bin:/bin:/usr/sbin:/sbin (checked with ps eww). First ProcessSpawnError: Failed to spawn process 'gh' appeared ~30 s later. UI: "Claude Agent CLI (claude) was not found on PATH." then "Provider turn failed." claude is at /opt/homebrew/bin/claude.
    • The user rebooted to try to fix it. Next launch timed out again (6018 ms), same bare PATH.
    • The same zsh -ilc from a clean env, measured afterwards: ~2.3 s warm. So a ~2 s shell config crosses the 5 s limit under ordinary load, not only at cold boot.

    Workaround: set an absolute Claude binary path in Settings → Providers. That only covers Claude, though: gh and other tools still fail for the rest of that app run, so the user has to relaunch anyway.

    Two things would help: a longer timeout or a retry for the probe, and a visible warning when the probe fails, because right now the only symptom is a misleading "not found on PATH" message.

    Authored by Claude Code (Claude Opus 5.5) via t3 triage.

  5. hieu-lee commented on Oct 20, 2026

    @hieu-lee

    I opened #22286 for the failed-capture cache and desktop SSH recovery path.

    A failed preferred-shell capture currently leaves preparation cached for the rest of the app run. The patch keeps failed/degraded POSIX captures retryable and prepares the environment before desktop SSH operations. Successful preferred-shell captures remain cached. A fallback shell or launchctl PATH stays usable without suppressing later recovery.

    The focused checks passed on macOS: 80 tests passed, 1 optional test skipped, plus desktop typecheck and changed-file lint/format checks. The new process regressions use temporary shell profiles and real OpenSSH to cover host resolution, ProxyCommand helper invocation, fallback recovery, success caching, and continued failure. They stop before remote login.

    This is partial coverage of this issue, not a complete resolution: it does not add failure UI, change provider error messages, or retry an already-running provider server. It is also separate from #9429's refresh-after-success case and complementary to #21913's noisy-shell probe fix. I left this issue open rather than adding a closing keyword to the PR.

    The PR includes a short explanation video and repeatable validation commands.

    Authored with GPT-6.1 Sol via Codex.

  6. shivasymbl commented on Oct 21, 2026

    @shivasymbl

    Note: Written by Claude Code (an AI agent) while debugging this on the affected user's machine. The user reviewed it and approved posting.

    Another repro on desktop app 0.1.0 (T3 Code Beta), macOS 27.0 arm64, plus a suggestion that would fix the first launch rather than only the retry path in #22286.

    Repro: login-shell probe hit DesktopShellEnvironmentCommandTimeoutError { executable: "zsh", timeoutMs: 5000 } after 6110 ms. Codex and Claude showed "not found on PATH", and gh failed to spawn. The CLIs live in ~/.local/bin, which .zprofile adds to PATH.

    Suggestion: fall back to a non-interactive login shell (-lc) before launchctl. -lc still reads .zshenv and .zprofile, where PATH is usually set, but skips .zshrc, where the slow setup lives (compinit, prompt frameworks, nvm/conda, completion evals). On this machine:

    probe env time result
    zsh -ilc (current) app launch 6110 ms timed out, launchd PATH
    zsh -ilc warm terminal ~3400 ms full PATH
    env -i HOME=… SHELL=/bin/zsh PATH=/usr/bin:/bin:/usr/sbin:/sbin zsh -lc clean, GUI-like 280 ms ~/.local/bin present; codex, claude, gh all resolve

    Here the slow part was three compinit calls in .zshrc, the last a full compinit from Docker Desktop's completions block. Collapsing them to one compinit -C got -ilc to about 1 s. Users can't be expected to find that, though, and -lc avoids .zshrc entirely.

    Rough shape in apps/desktop/src/shell/DesktopShellEnvironment.ts:

    const readLoginShellEnvironment = (shell, names, interactive = true) =>
      names.length === 0
        ? Effect.succeed({})
        : runCommandOutput({
            probe: interactive ? "login-shell" : "login-shell-noninteractive",
            command: shell,
            args: [interactive ? "-ilc" : "-lc", buildEnvironmentCaptureCommand(shell, names)],
            timeout: LOGIN_SHELL_TIMEOUT,
          }).pipe(Effect.map((output) => extractEnvironment(output, names)));
    
    for (const shell of listLoginShellCandidates(config.platform, config.env.SHELL)) {
      Object.assign(shellEnvironment, yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES));
      if (!shellEnvironment.PATH) {
        Object.assign(shellEnvironment, yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES, false));
      }
      if (shellEnvironment.PATH) break;
    }

    apps/server/src/os-jank.ts (readEnvironmentFromLoginShell) has the same -ilc-then-launchctl shape and would need the same fallback.

    Caveat: PATH entries that are only set in .zshrc (e.g. an npm global prefix) would be missing from the -lc result. That still beats the launchd default, and it stacks with #22286's retry, which can pick up the full -ilc PATH later. A one-line UI hint when the fallback is used ("shell PATH read timed out; using non-interactive login shell") would also make this much easier to diagnose than "not found on PATH".

    Untested as a patch in the repo; the timings above are measured, the code is a sketch.

  7. TheLoroXD commented on Oct 21, 2026

    @TheLoroXD

    Note: drafted with Claude Code on the affected machine, from its local logs.

    Another cold-boot repro on 0.1.2-nightly, plus two details that matter for the fix.

    Environment: T3 Code (Nightly) desktop 0.1.2-nightly.20261021.3486, macOS 26 (Darwin 25.6.0) arm64, zsh. The machine sits behind a TLS-inspecting corporate proxy, and the shell profile sets the CA bundle with NODE_EXTRA_CA_CERTS.

    Timeline (desktop.trace.ndjson, first launch after a reboot):

    11:02:31  desktop.shellEnvironment.runCommandOutput        5006 ms  DesktopShellEnvironmentCommandTimeoutError {probe: "login-shell", executable: "zsh", timeoutMs: 5000}
    11:02:36  desktop.shellEnvironment.runCommandOutput        1374 ms  launchctl getenv PATH, nothing returned
              desktop.shellEnvironment.installPosixEnvironment 6380 ms  exit: Success
    

    After that, every Claude turn failed in about 0.1 s because binaryPath: "claude" could not be spawned. Quitting and reopening fixed it; the next probe took 767 ms. On other launches the probe takes 0.6 to 1.4 s.

    1. On current main, the server's fallback never runs after a desktop timeout. An earlier comment says the server's hydratePosixPath also failed. In desktop mode it is skipped entirely (main @ e78f580):

    • installShellEnvironment returns installPosixEnvironment(config).pipe(Effect.as(true)) (apps/desktop/src/shell/DesktopShellEnvironment.ts:520), which is true even when the probe timed out.
    • That value reaches the backend as shellEnvironmentPrepared (apps/desktop/src/backend/DesktopBackendConfiguration.ts:824).
    • fixPath returns early when shellEnvironmentPrepared === true (apps/server/src/os-jank.ts:102).
    • The result is cached with Effect.cached for the whole app run (DesktopShellEnvironment.ts:531).

    #22286 changes the return value to preferredShellPrepared and adds a TTL to the cache. That covers this point at startup, but later provider spawns still do not trigger a new probe.

    2. PATH is not the only thing lost. The desktop probe also captures SSL_CERT_FILE, SSL_CERT_DIR and NODE_EXTRA_CA_CERTS (DesktopShellEnvironment.ts:96, :460). When the probe times out, the backend and every provider start without the user's CA bundle. Behind a TLS-inspecting proxy, that breaks providers even with an absolute binaryPath. The server-side fallback reads only PATH, __MISE_DIFF and JAVA_HOME (os-jank.ts:28), so the CA variables would not come back even once #22286 lets that fallback run.

    Suggestions:

    • Retry the capture after startup settles, or before the first provider spawn or provider refresh, and update the server's environment for later spawns.
    • Include the CA variables in whichever fallback runs.
    • Show a warning in provider status or Diagnostics when the capture timed out, so the user sees more than "not found on PATH".
  8. EduardoFazolo commented on Oct 24, 2026

    @EduardoFazolo

    Note: Written by Claude Code (Claude Opus 5.5) during a t3 triage session on the affected user's machine. The user reviewed it and approved posting.

    Another repro on the desktop app (T3 Code Beta), macOS 26 (Darwin 25.6.0) arm64, zsh. In this case the update itself caused it. The same shell was already close to the limit, and the first launches on the new version went over it.

    Timeline (local time, from desktop.trace.ndjson / server.trace.ndjson):

    time version login-shell probe providers
    16:23:25 0.0.45 4759 ms, success Claude + Codex OK
    16:29:35 — auto-update installed (0.0.45 → 0.1.2)
    16:30:34 0.1.2 5598 ms, DesktopShellEnvironmentCommandTimeoutError {probe: "login-shell", executable: "zsh", timeoutMs: 5000} spawn claude ENOENT, codex app-server spawn failed
    16:33:27 0.1.2 5407 ms, timeout same
    16:38:45 0.1.2 5633 ms, timeout
    16:41:50 0.1.2 5762 ms, timeout

    The UI showed "Claude — Unavailable. Claude Agent CLI (claude) was not found on PATH." Both CLIs were installed (~/.local/bin/claude, ~/.bun/bin/codex). As in the original report, installPosixEnvironment still recorded exit: Success.

    Where the time went: zsh -il took about 6.0–6.4s. A PS4 timestamp trace showed 5.5s of it in nvm.sh, almost all in the default nvm use at startup. Loading nvm with --no-use brings the whole login shell to about 0.46s. nvm with a default alias is common, so a lot of users are likely just under 5s and one slow launch away from this.

    Knock-on effect, worth knowing for #15854: the first Claude turn on 0.1.2 was also the first run after the v1→v2 legacy import. That turn failed to spawn claude, but the new native session ID had already been saved. Every later turn then failed with No conversation found with session ID until someone copied the transcript into that ID by hand. So this PATH failure can permanently break a thread, not just fail one turn.

    Workarounds that worked: absolute binaryPath for each provider instance in settings; launchctl setenv PATH … so the fallback finds a PATH (lost on logout or restart); making the shell faster.

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

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions