[Bug]: macOS service pins Homebrew Node Cellar path, making Node upgrades unsafe
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
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/server
Steps to reproduce
-
On an Apple Silicon Mac, install Node with Homebrew and invoke Node through Homebrew's stable path:
command -v node node -p 'process.execPath'On my machine this prints:
/opt/homebrew/bin/node /opt/homebrew/Cellar/node/26.8.1/bin/node/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:
npx t3@0.0.41-nightly.20260909.1439 service update -
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:
/opt/homebrew/Cellar/node/26.8.1/bin/node -
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 node command 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 on main at d29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4.
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
$ ls -l /opt/homebrew/bin/node
/opt/homebrew/bin/node -> ../Cellar/node/26.8.1/bin/node
$ node -p 'process.execPath'
/opt/homebrew/Cellar/node/26.8.1/bin/node
$ plutil -extract ProgramArguments.0 raw \
"$HOME/Library/LaunchAgents/com.t3tools.t3code.service.plist"
/opt/homebrew/Cellar/node/26.8.1/bin/node
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, ...).- These two observations are consistent with the generated plist and process tree on my host.
Relevant source on the inspected commit:
- https://github.com/pingdotgg/t3code/blob/d29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4/apps/server/src/cloud/bootService.ts#L570-L572
- https://github.com/pingdotgg/t3code/blob/d29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4/apps/server/src/serviceLauncher.ts#L397-L408
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with apps/server/src/cloud/bootService.ts, especially the service plan's nodePath, then inspect the macOS renderer that writes ProgramArguments and apps/server/src/serviceLauncher.ts. Reproduce the generated plist with the listed npx and plutil commands, and trace both launchd startup and managed-runtime handoff. Done means the service remains startable after a normal Homebrew Node upgrade without relying on a removed version-specific executable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, node.js, typescript
- Domain
- backend, devops, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100