Run the daemon from inside the app bundle so the icon keeps its place - #2
Merged
Conversation
The LaunchAgent named /opt/homebrew/bin/yap, Homebrew's symlink into yap.app. CFBundle derives Bundle.main from the path a process was exec'd with rather than the path it resolves to, so the daemon's main bundle was /opt/homebrew/bin — not a bundle — and LaunchServices published no bundle identifier for it: lsappinfo answers CFBundleIdentifier=[ NULL ] while still reporting LSBundlePath=/Applications/yap.app. Menu bar managers name each item <owner bundle id>:<autosave name> through NSRunningApplication and fall back to the process name when there is none, so the daemon appeared as yap:Item-0 while the same build launched from /Applications appeared as com.terrifiedbug.yap:Item-0. Two identities for one app, each with its own remembered section, and the relaunch tracking those managers key on bundle identifier only recognises the bundled one. Measured in Thaw's own defaults, where both keys existed and both sat in the hidden section. So resolve the symlink when writing the plist, and re-exec through the bundle at startup for the plists already out there. Bundle.main's own identifier is no use as the test: the binary carries a __TEXT,__info_plist so it answers com.terrifiedbug.yap either way. Verified: identity flips NULL -> com.terrifiedbug.yap in one pid, the event tap still comes up (TCC keys the grants on the signature, not the path), and the icon survives daemon restarts. Build, smoke and lint match main.
This was referenced Aug 14, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The menu bar icon kept vanishing under a menu bar manager (Thaw, the maintained Ice fork). It was an identity bug, not a UI bug.
What was wrong
The LaunchAgent named
/opt/homebrew/bin/yap— Homebrew's symlink intoyap.app. CFBundle derivesBundle.mainfrom the path a process was exec'd with, not the path it resolves to, so the daemon's main bundle was/opt/homebrew/bin, which is not a bundle, and LaunchServices published no bundle identifier for it:Menu bar managers name each item
<owner bundle id>:<status item autosave name>, read throughNSRunningApplication, and fall back to the process name when there is no identifier. So the daemon showed up asyap:Item-0while the same build launched from /Applications showed up ascom.terrifiedbug.yap:Item-0— one app, two identities, each with its own remembered section, and the relaunch tracking those managers key on bundle identifier only ever recognises the bundled one. Both keys were present in Thaw's own defaults, both filed underhidden.The comment on
resolveBinaryPathclaimedBundle.main.executablePathis symlink-resolved. It is absolute but not resolved — measured — which is how the shim path got into the plist in the first place.The fix
Installresolves the symlink, so launchd execs the bundle's own executable.Runre-execs through the bundle at startup, which covers the plists already installed and a bareyap runtyped at a shell.execvkeeps the pid, so launchd sees one process.Bundle.main.bundleIdentifieris useless as the test here and the comment says so: the binary carries its own__TEXT,__info_plist, so in-process it answerscom.terrifiedbug.yapeither way. The honest question is whether we were exec'd from inside a bundle, so the guard reads the main bundle's path.Verification
Scratch bundle with a distinct identifier, launched through a symlink and directly:
[ NULL ]→com.terrifiedbug.yapexecprobecom.terrifiedbug.yapexecprobeOne pid in both cases, so the exec happens in place and does not loop; the real-path launch does not exec at all.
On the live daemon, with the plist pointed at the bundled executable:
CFBundleIdentifier=com.terrifiedbug.yap, thelistening on fn holdbanner still appears — so the Accessibility grant survives the path change, TCC keying the grants on the code signature and the embedded identifier rather than on a path (the microphone grant is recorded againstcom.terrifiedbug.yap,client_type=0) — and the icon is present in the menu bar across two daemon restarts and a manager relaunch, holding its saved position.Build, the CI smoke steps and the embedded-plist check all pass; swiftlint reports 75 violations on this branch and 75 on main, so nothing new.
README gains three lines telling anyone running a menu bar manager to look in its hidden section once.