Repository navigation
[Bug]: macOS service pins Homebrew Node Cellar path, making Node upgrades unsafe #11054
Description
Activity
Triage
Confirmed. This is a real boot-service bug, not a local plist misconfiguration.
t3 service install/service updatepersistprocess.execPathas the service Node:apps/server/src/cloud/bootService.tssetsnodePath: host.execPathand writes that into LaunchAgentProgramArguments[0](and systemdExecStart).HostProcessExecutablePathinpackages/shared/src/hostProcess.tsisprocess.execPath.- Node realpaths that value, so Homebrew becomes
/opt/homebrew/Cellar/node/<ver>/bin/nodeinstead of/opt/homebrew/bin/node. apps/server/src/serviceLauncher.tsthenspawn(process.execPath, …)for managed runtimes, so a later handoff can also miss after the keg is removed.
The renderer already refuses to depend on
PATH(absolute Node is correct). The defect is pinning the keg realpath, whose lifetime is one Homebrew version. Tests even fixture macOS with/opt/homebrew/bin/node, which is the stable path install does not currently write.brew upgrade nodecan delete the old keg. The running process can survive on the old inode; the next launchd start (logout/reboot) or launcher handoff fails even thoughnodeon the prefix symlink still works. That matches the report. Forcing keg removal on a live host is not required — the generated plist is the repro.Not a duplicate of #11055
#11055 is the LaunchAgent inheriting launchd’s 256
maxfilesand hittingEMFILEon userdata watchers. #11056 only addsSoftResourceLimits.NumberOfFiles. Neither changesnodePath. Keep these separate.No open PR fixes the Cellar pin. #8173 only put
/opt/homebrew/binon the agentPATHfor provider CLIs.The same
execPathpin is on Linux systemd. Distro Node is usually stable; linuxbrew/nvm can hit the same class of failure.Suggested fix
Prefer a durable absolute Node when building the plan (Homebrew
$prefix/bin/nodewhenexecPathis a Cellar keg;argv[0]when it is already a non-Cellar absolute). Do not switch to barenode. Also stop the launcher from spawning children only viaprocess.execPath— launchd following a symlink does not change the running process’sexecPath, so live handoff still breaks after a keg removal.homebrewOwnershipFromCommandPathinapps/server/src/provider/providerMaintenance.tsalready knows Cellar layouts.Until then: point the LaunchAgent at the prefix
nodeand re-check aftert3 service update(that command rewrites the plist). After a Node upgrade,service updatefrom a working Node also rewrites the pin. Full Disk Access may still key off the real binary and need a re-grant.Labels:
bug. Removingneeds-triage.- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 10, 2026 Interested in a small fix here: prefer the stable Homebrew shim (
dirnameofprocess.execPathwhen under Cellar, orcommand -v node//opt/homebrew/bin/nodewhen that is what launched us) when writing LaunchAgent/systemdnodePath, instead of pinningprocess.execPathto a keg path.Happy to open a tightly scoped PR against
bootService.ts/HostProcessExecutablePathif that direction matches what you want.Thanks for taking the time to report this and provide the details. We revisited it during the orchestrator V2 cleanup.
The boot service now launches a pinned standalone T3 executable, removing the reported dependency on a Homebrew Cellar Node executable that disappears on upgrade.
I’m closing this based on the current source and the evidence in this thread.
Existing installations may need service reinstallation to replace a previously generated unit; this assessment concerns newly generated current services.
If you still hit this on a current build, please reply with the app/server versions and the steps that reproduce it. We can reopen this if the original problem is still there.
Before submitting
Area
apps/server
Steps to reproduce
On an Apple Silicon Mac, install Node with Homebrew and invoke Node through Homebrew's stable path:
On my machine this prints:
/opt/homebrew/bin/nodeis a symlink managed by Homebrew.process.execPathis the resolved, version-specific executable.Install or update the T3 background service from that environment. I reproduced the generated configuration after:
Inspect the executable stored in the LaunchAgent:
plutil -extract ProgramArguments.0 raw \ "$HOME/Library/LaunchAgents/com.t3tools.t3code.service.plist"The plist contains the resolved Cellar path:
A normal Homebrew Node upgrade can repoint
/opt/homebrew/bin/nodeand eventually remove the old keg. At that point the LaunchAgent still references the old version-specific path.I did not intentionally remove the currently running Node keg just to force an outage on this host. The deterministic repro here is the generated service definition retaining a path whose lifetime is tied to one Homebrew keg.
Expected behavior
A T3 background service installed with Homebrew-managed Node should remain startable across a routine Node upgrade, or the service lifecycle should otherwise make this dependency and the required maintenance explicit before the old executable disappears.
Actual behavior
The generated LaunchAgent stores the resolved Cellar path. The service works while that keg exists, but a later restart can fail once Homebrew removes it, even though the normal
nodecommand continues to work through/opt/homebrew/bin/node.There is a related runtime concern: the stable service launcher also starts managed T3 runtimes through its own
process.execPath. If the launcher remains alive while Homebrew replaces and removes its Node keg, a later managed-runtime handoff may also try to use the removed executable.Impact
Major degradation or frequent failure
Version or commit
Observed in
0.0.41-nightly.20260909.1439; the relevant behavior is also present onmainatd29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4.Environment
macOS 26.6.2 (Apple Silicon), Homebrew Node 26.8.1, npm 11.19.0, T3 background service managed by launchd
Logs or stack traces
Investigation notes
I inspected the installed package and current source to understand whether this was only a local plist change.
bootService.tsbuilds the service plan withnodePath: host.execPath, and the macOS renderer writesplan.nodePathintoProgramArguments[0].serviceLauncher.tsstarts a managed runtime withspawn(process.execPath, ...).Relevant source on the inspected commit:
t3code/apps/server/src/cloud/bootService.ts
Lines 570 to 572 in d29c56a
t3code/apps/server/src/serviceLauncher.ts
Lines 397 to 408 in d29c56a
I may be missing an intended assumption about how Node is provisioned for the background service. I am reporting the lifecycle mismatch rather than proposing a specific implementation. Preserving an upgrade-stable executable reference, resolving the current runtime at launch, or explicitly coordinating service maintenance with Node upgrades may have different trade-offs that are clearer to the maintainers.
Workaround
I currently keep a locally managed LaunchAgent configuration and check it after
t3 service update. This avoids relying on the generated Cellar path, but I do not consider the local configuration a recommendation for the upstream design.