Skip to content

Run the daemon from inside the app bundle so the icon keeps its place - #2

Merged
TerrifiedBug merged 1 commit into
mainfrom
fix/menubar-bundle-identity
Aug 14, 2026
Merged

Run the daemon from inside the app bundle so the icon keeps its place#2
TerrifiedBug merged 1 commit into
mainfrom
fix/menubar-bundle-identity

Conversation

@TerrifiedBug

Copy link
Copy Markdown
Owner

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 into yap.app. CFBundle derives Bundle.main from 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:

$ lsappinfo info -only bundleid,bundlepath,executablepath <daemon pid>
"CFBundleIdentifier"=[ NULL ]
"LSBundlePath"="/Applications/yap.app"
"CFBundleExecutablePath"="/opt/homebrew/bin/yap"

Menu bar managers name each item <owner bundle id>:<status item autosave name>, read through NSRunningApplication, and fall back to the process name when there is no identifier. So the daemon showed up as yap:Item-0 while the same build launched from /Applications showed up as com.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 under hidden.

The comment on resolveBinaryPath claimed Bundle.main.executablePath is 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

  • Install resolves the symlink, so launchd execs the bundle's own executable.
  • Run re-execs through the bundle at startup, which covers the plists already installed and a bare yap run typed at a shell. execv keeps the pid, so launchd sees one process.

Bundle.main.bundleIdentifier is useless as the test here and the comment says so: the binary carries its own __TEXT,__info_plist, so in-process it answers com.terrifiedbug.yap either 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:

launched via LaunchServices identity pids
symlink [ NULL ]com.terrifiedbug.yapexecprobe 1
real path com.terrifiedbug.yapexecprobe 1

One 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, the listening on fn hold banner 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 against com.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.

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.
@TerrifiedBug
TerrifiedBug merged commit b65471b into main Aug 14, 2026
1 check passed
@TerrifiedBug
TerrifiedBug deleted the fix/menubar-bundle-identity branch August 14, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant