Repository navigation
bug: post-update harness refresh fails with ENOENT after brew upgrade #63
Description
What happened?
contextbridge update's post-update harness refresh (added in #52) silently no-ops for every Homebrew-installed user because the spawn target path was deleted by brew before the spawn fires.
runUpdate in packages/cli/src/commands/update.ts calls commandRunner.run(process.execPath, ['install', '<harness-id>', '--scope', '<scope>']) to re-run the new binary's per-harness installer. But process.execPath is captured at process start — for brew-installed binaries this is the cellar path of the pre-upgrade version (e.g., /opt/homebrew/Caskroom/cli@alpha/0.3.0-alpha.1/contextbridge). Brew's --cask upgrade flow then runs Purging files for version 0.3.0-alpha.1, deleting that whole cellar directory. By the time runUpdate reaches the spawn, the path is gone — Node fails with ENOENT.
The refresh-failure logging from #52 correctly downgrades this to a warning so update itself still exits 0 with ✓ update complete., but the actual per-harness re-install never runs. The post-update refresh feature — the whole reason #52 was filed — is effectively dead for brew users.
Discovered live during alpha.1 → alpha.2 upgrade testing of #62.
contextbridge version
0.3.0-alpha.2 (and 0.3.0-alpha.1; the failure is in the upgrade transition itself, observable on every brew-driven version bump)
Operating system
macOS
Steps to reproduce
- Install contextbridge via the brew tap (e.g.,
brew install contextbridge/tap/cli@alpha). - Run
contextbridge install claude --yesso a managed harness exists for the refresh to act on. - Wait for (or simulate) a newer release on the same channel.
- Run
LOG_LEVEL=trace contextbridge update. - Observe
ENOENTwarnings for each managed harness while update exits 0.
What did you expect to happen?
After the binary swap, runUpdate re-runs the new binary's install <harness> for each managed scope. Trace should show the per-harness install shellouts firing against the post-upgrade binary path.
What actually happened?
Brew swaps the binary and purges the old cellar directory. runUpdate then tries to posix_spawn against the captured-at-startup process.execPath — which is the now-deleted old path. Each managed harness logs post-update harness refresh failed with ENOENT. Update reports success and exits 0, but no refresh happens.
Relevant logs or stack trace
$ LOG_LEVEL=trace contextbridge update
A new version is available: v0.3.0-alpha.2 (you're on v0.3.0-alpha.1).
...
==> Upgrading cli@alpha
0.3.0-alpha.1 -> 0.3.0-alpha.2
==> Unlinking Binary '/opt/homebrew/bin/contextbridge'
==> Linking Binary 'contextbridge' to '/opt/homebrew/bin/contextbridge'
==> Purging files for version 0.3.0-alpha.1 of Cask cli@alpha
🍺 cli@alpha was successfully upgraded!
{"level":40,"msg":"post-update harness refresh failed","err":{"type":"Error","message":"ENOENT: no such file or directory, posix_spawn '/opt/homebrew/Caskroom/cli@alpha/0.3.0-alpha.1/contextbridge'","code":"ENOENT","path":"/opt/homebrew/Caskroom/cli@alpha/0.3.0-alpha.1/contextbridge","syscall":"posix_spawn","errno":-2},"harness":"claude","scope":"user"}
{"level":40,"msg":"post-update harness refresh failed","err":{"type":"Error","message":"ENOENT: no such file or directory, posix_spawn '/opt/homebrew/Caskroom/cli@alpha/0.3.0-alpha.1/contextbridge'","code":"ENOENT","path":"/opt/homebrew/Caskroom/cli@alpha/0.3.0-alpha.1/contextbridge","syscall":"posix_spawn","errno":-2},"harness":"codex","scope":"user"}
✓ update complete.which contextbridge immediately after returns /opt/homebrew/bin/contextbridge (the stable symlink), which readlink resolves to …/0.3.0-alpha.2/contextbridge. The new binary is in place and would have worked if it had been the spawn target.
Anything else
- Discovered while testing fix: refresh Claude marketplace cache during install #62 (the marketplace-cache-refresh fix). fix: refresh Claude marketplace cache during install #62 itself is unaffected by this bug — they're separate code paths. Linking for context.
- Introduced in feat!: rename Claude plugin to planbridge@contextbridge; refresh plugins on update #52 (
refresh plugins on update) —runUpdatewalksALL_INSTALLERSand spawnsprocess.execPath install <harness-id>for each managed scope. - Why the unit tests in
packages/cli/src/commands/update.test.tsdon't catch this:FakeCommandRunnerhappily resolves stubs registered againstprocess.execPath, soexecuted → refresh fires → install spawnslooks fine in tests. The cellar-purge happens outside the test process. - Affected scope: every brew-installed user (likely the majority of macOS users).
- Install-script users may be unaffected if the install script writes the binary to a stable path that survives upgrades — worth verifying, but the brew path is definitely broken.
Activity
test: drop redundant tests added in previous commit
review: drop issue refs from comments, reorder binaryPath arg, rename…
fix: resolve contextbridge via PATH for post-update refresh
fix: resolve contextbridge via PATH for post-update refresh (#64)