Repository navigation
[Bug]: agent-device wrapper doesn't set ELECTRON_RUN_AS_NODE, so Codex agent calls launch the desktop app and SIGABRT ("T3 Code quit unexpectedly") #18182
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/desktop
Summary
The device-tool wrapper ~/.t3/userdata/device/bin/agent-device runs the Electron app binary as a script interpreter but never sets ELECTRON_RUN_AS_NODE=1. The wrapper only gets Node mode if the variable is inherited from its caller.
When a Codex agent calls the wrapper through its shell tool, the variable is missing. Codex's shell_environment_policy (inherit = "core") strips it. The binary then starts up as a full Electron desktop app instead of Node:
- Sandboxed command:
NSApplicationinit can't reach WindowServer and callsabort(). The command exits with code 134 and macOS shows a "T3 Code (Nightly) quit unexpectedly" crash dialog. It looks like T3 crashed, but the main window is fine. - Escalated (unsandboxed) command: a second T3 desktop instance starts up ("app ready", "backend ready"). It then fails on
IndexedDB/…leveldb/LOCKbecause the real instance holds the lock.
The same problem exists one level down: agent-device-launcher.mjs spawns the app binary again with { ...process.env } and doesn't set ELECTRON_RUN_AS_NODE there either.
This gets more likely once #16456 is fixed: removing ELECTRON_RUN_AS_NODE from provider sessions would break every agent call to agent-device.
Generated wrapper (as written by T3)
~/.t3/userdata/device/bin/agent-device:
#!/bin/sh
exec '/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code (Nightly)' '~/.t3/userdata/device/bin/agent-device-launcher.mjs' "$@"~/.t3/userdata/device/bin/agent-device-launcher.mjs (excerpt):
const env = { ...process.env };
delete env.AGENT_DEVICE_DAEMON_BASE_URL;
delete env.AGENT_DEVICE_DAEMON_AUTH_TOKEN;
delete env.AGENT_DEVICE_CONFIG;
const child = spawn("/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code (Nightly)", [".../agent-device/0.21.23/node_modules/agent-device/bin/agent-device.mjs", ...args], { stdio: "inherit", env });Steps to reproduce
Deterministic, no agent needed (macOS, from any terminal):
W=~/.t3/userdata/device/bin/agent-device
P='(version 1)(allow default)(deny mach-lookup (global-name "com.apple.windowserver.active") (global-name "com.apple.CoreServices.coreservicesd") (global-name-prefix "com.apple.coreservices.launchservicesd"))'
# A) variable present: works
ELECTRON_RUN_AS_NODE=1 sandbox-exec -p "$P" "$W" help; echo "exit=$?"
# -> agent-device <command> [args] [--json] ... exit=0
# B) variable absent (what a Codex shell gets): SIGABRT + crash report
env -u ELECTRON_RUN_AS_NODE sandbox-exec -p "$P" "$W" help; echo "exit=$?"
# -> exit=134, new "T3 Code (Nightly)-<ts>.ips" in ~/Library/Logs/DiagnosticReports
# Also shows the binary isn't in Node mode: --version prints the T3 app version, not agent-device's
env -u ELECTRON_RUN_AS_NODE "$W" --version # -> 0.0.46-nightly.20261011.2955 (expected 0.21.23)
ELECTRON_RUN_AS_NODE=1 "$W" --version # -> 0.21.23Real-world trigger: in a Codex (gpt-6.1-sol) thread with device access, the agent runs any agent-device … command from its shell tool (for example agent-device help or agent-device snapshot -i --platform ios --udid … --config … --session …).
Expected behavior
agent-device always runs as Node, whatever environment the caller passes, and never starts the desktop app or triggers a macOS crash dialog.
Actual behavior
Every agent-initiated agent-device call from a Codex shell exits with code 134. Each one creates a crash report and a "quit unexpectedly" dialog, which makes it look like T3 is crashing repeatedly. Escalated calls start a second desktop instance instead.
Evidence from one night. Every crash report matches a Codex CommandExecution with exit_code: 134 to the second:
| Crash report (local, EDT) | Codex command (UTC) | Command |
|---|---|---|
| 00:09:43 | 04:09:43Z → 134 | agent-device open --help --platform ios --udid … --config … --session … |
| 00:12:42 | 04:12:42Z → 134 | agent-device install com.example.app …/Debug-iphonesimulator/App.app --platform ios --udid … |
| 05:03:38 | 09:03:38Z → 134 | agent-device --help |
| 05:55:29 | 09:55:29Z → 134 | agent-device help |
| 06:05:11 | 10:05:09Z → 134 | agent-device snapshot -i --platform ios --udid … |
| 06:05:58 | (manual repro B above) → 134 | agent-device help |
All of these crash reports have parentProc: codex (codex app-server), except the manual repro, whose parent is zsh. Each process lived about 150 ms.
Escalated run of the same command (codex log, 05:55:58 local), showing a second desktop instance starting up:
[05:55:58.956] INFO (#4): runtime logging configured scope: desktop component: desktop-startup
[05:55:58.958] INFO (#4): app ready scope: desktop component: desktop-startup
...
ERROR:content/browser/indexed_db/instance/leveldb/backing_store.cc:237] Failed to open LevelDB database from ~/Library/Application Support/t3code-v2/IndexedDB/t3code_app_0.indexeddb.leveldb, IO error: .../LOCK: No further ...
Suggested fix
Don't depend on the caller's environment:
- In the generated
agent-deviceshell wrapper:ELECTRON_RUN_AS_NODE=1 exec '<app binary>' '<launcher.mjs>' "$@"
- In
agent-device-launcher.mjs, setenv.ELECTRON_RUN_AS_NODE = "1"on the spawned child. - Check any other generated shims that use the app binary as an interpreter (for example the expo-device-hub client) for the same issue.
Verified that adding (1) alone fixes the repro in the same sandbox:
env -u ELECTRON_RUN_AS_NODE sandbox-exec -p "$P" sh -c 'ELECTRON_RUN_AS_NODE=1 exec "/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code (Nightly)" ~/.t3/userdata/device/bin/agent-device-launcher.mjs --version'
# -> 0.21.23, exit 0Impact
Major degradation or frequent failure
Version or commit
T3 Code (Nightly) 0.0.46-nightly.20261011.2955; also seen on 0.0.46-nightly.20261010.2948. agent-device 0.21.23.
Environment
macOS 27.0 (26A428), Apple Silicon (arm64). Provider: Codex app-server (gpt-6.1-sol), ~/.codex/config.toml with [shell_environment_policy] inherit = "core". Claude Code threads are unaffected because their shell inherits ELECTRON_RUN_AS_NODE=1.
Logs or stack traces
Faulting thread (identical in every report, including the manual repro):
EXC_CRASH (SIGABRT) asi: libsystem_c.dylib: "abort() called"
0 libsystem_kernel.dylib __pthread_kill
1 libsystem_pthread.dylib pthread_kill
2 libsystem_c.dylib abort
3 HIServices ___RegisterApplication_block_invoke
4 libdispatch.dylib _dispatch_client_callout
5 libdispatch.dylib _dispatch_once_callout
6 HIServices _RegisterApplication
7 HIServices GetCurrentProcess
8 AppKit -[NSMenuBarPresentationInstance _getAggregateUIMode:withOptions:]
9 AppKit _NSGetAggregateUIMode
10 AppKit -[NSMenuBarPresentationInstance _isVisible]
11 AppKit +[NSMenuBarPresentationInstance _isMenuBarVisible]
12 AppKit _NSInitializeAppContext
13 AppKit -[NSApplication init]
14 AppKit +[NSApplication sharedApplication]
15 Electron Framework node::PrincipalRealm::inspector_enable_network_tracking() const
16 Electron Framework ...
17 Electron Framework ElectronInitializeICUandStartNode
Workaround
Add ELECTRON_RUN_AS_NODE = "1" under [shell_environment_policy.set] in ~/.codex/config.toml (untested; it should restore the variable for Codex shells). Or call the wrapper as ELECTRON_RUN_AS_NODE=1 agent-device …. Note that T3 regenerates the wrapper files, so editing them by hand doesn't stick.
Related: #16456 (the opposite direction: ELECTRON_RUN_AS_NODE leaking into provider sessions on Linux).
Activity
gitkamaal commentedon Oct 11, 2026
Confirmed workaround: adding
ELECTRON_RUN_AS_NODE = "1"under[shell_environment_policy.set]in~/.codex/config.tomlfixes it. Inside a sandboxedcodex execshell,agent-device --versionnow prints0.21.23and exits 0, with no new crash report. Twocodesign_util.cc:79 task_name_for_piderrors still print to stderr, but they are harmless. Already-runningcodex app-serverprocesses only pick up the change after a restart.juliusmarminge commentedon Oct 11, 2026
Note
Grok responding on behalf of Julius.
Confirmed on current main. Same hole as #12926 (closed completed on 2026-10-02 without a fix landing; #13093 and #12241 stayed unmerged). This issue is the live tracker.
On desktop Electron,
resolveNodeExecutable(packages/shared/src/nodeRuntime.ts) returnsprocess.execPathwhen the host is not a Node SEA — intentionally the app binary. Other desktop spawners already force Node mode (DesktopCliShim, backend child env, snapshot helpers).ensureAgentDeviceShim(apps/server/src/device/AgentDeviceShim.ts) does not: it still writesand the launcher re-spawns that binary with
{ ...process.env }only. NoELECTRON_RUN_AS_NODE=1in the POSIX wrapper, the Windows.cmd, or the launcher child env.When a Codex shell runs the shim,
shell_environment_policyinherit = "core"strips any inherited flag. Sandboxed calls hitNSApplicationinit →abort()(exit 134, "T3 Code quit unexpectedly"). Escalated calls start a second desktop instance and fight the IndexedDB lock. Claude Code threads look fine only because they keep the inherited flag.#16456 / #16679 (stop leaking
ELECTRON_RUN_AS_NODEinto provider sessions) make this worse if they land first — every agent call toagent-devicewould then depend on the shim setting the flag itself.Fix: set
ELECTRON_RUN_AS_NODE=1in the generated POSIX/cmdwrappers and inagent-device-launcher.mjs's spawn env (the #13093 / #12241 shim half). Add a regression that runs the generated shim through a fake desktop executable that refuses to start without Node mode. Temporary workaround:ELECTRON_RUN_AS_NODE = "1"under[shell_environment_policy.set]in~/.codex/config.toml, or prefix the command; hand-editing the regenerated wrapper does not stick.WesleyYJiang commentedon Oct 13, 2026
Another consequence of this one, worse than the crash: the stray desktop instance can re-point the machine's T3 Connect managed tunnel.
What we saw (macOS, Nightly 0.0.46-nightly.20261012):
~/.t3/userdata/device/bin/agent-device install …from azsh -lcshell. The env had noELECTRON_RUN_AS_NODE, because Codex'sshell_environment_policyinherit = "core"drops it.environment.cloud.activateManagedTunnelwith originhttp://127.0.0.1:3774, gotbefore-quitabout 35 s later and exited.127.0.0.1:3774(dial tcp 127.0.0.1:3774: connect: connection refused, relay host returned 502). Every remote client (desktop peers, iOS) saw the environment as unreachable for hours, while the main app looked healthy.recoverManagedCloudTunnelwith the main server's own origin and the relay host returns 200 again.Workaround on our side:
[shell_environment_policy.set] ELECTRON_RUN_AS_NODE = "1"in~/.codex/config.toml.Suggested fixes: have the generated wrapper set the variable itself (
exec env ELECTRON_RUN_AS_NODE=1 …). Separately, it may be worth makingactivateManagedTunnelrefuse, or at least warn, when another live instance already owns the confirmed origin on the same userdata.