Stop dev builds from shadowing the installed app in LaunchServices - #154
Merged
Conversation
This was referenced Aug 7, 2026
Owner
Author
|
Follow-ups from this work filed as issues, so nothing here is blocking:
Note this branch is now ~2 weeks behind |
Every OpenMessage .app on a dev machine declared the same CFBundleIdentifier (com.openmessage.app), so LaunchServices could resolve "OpenMessage" (Spotlight, Dock, `open -a OpenMessage`, notification clicks) to any of them. This caused two live outages; on 2026-07-25 a build predating the Google self-heal OSID fix (#148) launched instead of /Applications and latched Google Messages in needs_repair for ~10.5h (06:54 to ~17:20). Make it structurally impossible for a build output to win that resolution: plain `./macos/build.sh` now stamps com.openmessage.app.dev and emits OpenMessage-dev.dmg, while shippable builds opt in with RELEASE=1. The build also unregisters its own output from LaunchServices, though that alone is not durable (see below) — the dev bundle id is what actually closes the hole. The data dir is deliberately unaffected: BackendManager hardcodes ~/Library/Application Support/OpenMessage, so a dev build still reads the same store. What IS bundle-id-scoped is UserDefaults (the `defaults write com.openmessage.app V2Primary` lever), the notification grant, and the sandbox container — so the dev-build output refuses to suggest installing without RELEASE=1. Runbook gains a "Bundle-id shadowing" section recording two fixes that were verified NOT to work, so they don't get retried: - `lsregister -u <path>` is not durable. Any LaunchServices rescan re-registers the bundle; a forced rescan brought all 14 back at once. - Renaming Foo.app to Foo.app.disabled does nothing. LaunchServices registers on bundle structure, not the .app extension, and re-registered every renamed bundle at its new path. What works for bundles kept on disk is renaming Contents/Info.plist to Contents/Info.plist.disabled — lossless, reversible, survives forced rescans. Also documents that mdfind is an unreliable audit here (stale metadata, skips dot-directories) and that nested .claude/worktrees/* builds need GOWORK=off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dev bundle id change would otherwise have shipped a broken release: release.yml ran `bash macos/build.sh` with no RELEASE=1, so it would have produced a com.openmessage.app.dev app and an OpenMessage-dev.dmg, and every downstream step referencing macos/build/OpenMessage.dmg (Gatekeeper assessment, artifact upload, release upload) would have failed or shipped a dev-id build. Set RELEASE=1 in release.yml and assert, before notarization, that the built app carries com.openmessage.app and that OpenMessage.dmg exists — so a future regression fails the release rather than shipping an app that can't read the V2Primary lever or its notification grant. test.yml now asserts both directions: the default build must claim com.openmessage.app.dev (the property that stops stale builds winning LaunchServices resolution), and a RELEASE=1 build must restore com.openmessage.app plus OpenMessage.dmg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
claude/laughing-lamarr-4ccb46
branch
from
August 12, 2026 10:26
59feab9 to
5335874
Compare
Cross-family review (sol) caught a hole in the dev-bundle-id scheme: LaunchServices resolves `open -a OpenMessage` and Spotlight launches by the registered *name* (CFBundleName/CFBundleDisplayName), not the bundle id — and the name comes from the plist, not the .app filename (a bundle renamed on disk still registered as "OpenMessage" from its plist). With only CFBundleIdentifier stamped, a dev build kept CFBundleName "OpenMessage" and could still win the exact `open -a OpenMessage` used in the runbook's own deploy recipe. Dev builds now stamp CFBundleName and CFBundleDisplayName to "OpenMessage (dev)" alongside the dev id. Verified against a real build: the LaunchServices record registers as name "OpenMessage (dev)" / id com.openmessage.app.dev, and the only record named "OpenMessage" or claiming com.openmessage.app is /Applications/OpenMessage.app. Also from the review: - build.sh now clears BOTH DMG variants before hdiutil, so a dev build can't leave last week's OpenMessage.dmg sitting in build/ looking shippable (and vice versa). test.yml asserts the dev DMG is gone after the RELEASE=1 rebuild — which exercises the cleanup for real, since that step runs right after a dev build. - test.yml/release.yml assert CFBundleName in both modes, not just the id, so the name half of the fix can't silently regress. - Runbook: the "structurally cannot win resolution" claim now spells out both mechanisms (id-based vs name-based launches), and documents a real sharp edge of distinct dev ids: macOS no longer dedupes a dev build against the installed app, and a dev GUI that adopts the daemon on port 7007 SIGTERMs it on quit (BackendManager's adopted-PID stop path) — killing the live backend. Tracked with the other id-scoped traps in #165. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mwhobrey
pushed a commit
to mwhobrey/om-tui
that referenced
this pull request
Aug 24, 2026
…axGhenis#154) Every OpenMessage .app declared CFBundleIdentifier com.openmessage.app, so LaunchServices could resolve "OpenMessage" to any stale build output — twice causing live outages (most recently 2026-07-25: a build predating the MaxGhenis#148 OSID fix latched Google Messages in needs_repair for ~10.5h). Plain ./macos/build.sh now stamps a full dev identity — id com.openmessage.app.dev, name "OpenMessage (dev)" — and emits OpenMessage-dev.dmg, so a dev build can win neither id-based resolution (notification clicks, open -b) nor name-based resolution (open -a OpenMessage, Spotlight). RELEASE=1 restores the shippable identity; release.yml sets it and asserts the built id+name before notarization, and test.yml asserts both directions plus that stale cross-mode DMGs get cleaned. Runbook documents the shadowing mechanics, the audit command, the fixes that verifiably do NOT work (lsregister -u, .app renames), and the dev-GUI sharp edge (tracked in MaxGhenis#165). Cross-family reviewed: sol's initial DO-NOT-MERGE (name-based resolution hole, stale-DMG footgun) drove the CFBundleName stamp; re-review returned MERGE with no findings. Verified on a real build: the dev bundle's LS record registers as "OpenMessage (dev)"/com.openmessage.app.dev, and the only record named "OpenMessage" or claiming com.openmessage.app is /Applications/OpenMessage.app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (cherry picked from commit 12398ce)
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.
Problem
Every OpenMessage
.appon a dev machine declared the sameCFBundleIdentifier(com.openmessage.app), so LaunchServices could resolve "OpenMessage" — Spotlight, Dock,open -a OpenMessage, notification clicks — to any of them.This caused two live outages. Most recently 2026-07-25: a build predating the Google self-heal OSID fix (#148) launched instead of
/Applications/OpenMessage.appand latched Google Messages inneeds_repairfrom 06:54 to ~17:20 (~10.5h dead) before it was diagnosed.An audit of the machine found 19 bundles claiming the id — build outputs, rollback backups, dated app-backups, and Xcode archives. 15 of them declared version
1.0.0against the real app's0.2.9.Fix
Make it structurally impossible for a build output to win that resolution:
./macos/build.shcom.openmessage.app.devOpenMessage-dev.dmgRELEASE=1 ./macos/build.shcom.openmessage.appOpenMessage.dmgThe build also unregisters its own output from LaunchServices, but that is belt-and-braces only — see below. The dev bundle id is what actually closes the hole.
Blast radius, checked before choosing this. The data dir is not bundle-id-scoped —
BackendManager.swifthardcodes~/Library/Application Support/OpenMessage— so a dev build still reads the same store. What is id-scoped:UserDefaults(thedefaults write com.openmessage.app V2PrimaryR8 lever), the notification grant, and the sandbox container. So a dev build's output refuses to suggest installing, and bothCLAUDE.mdand the deploy runbook now requireRELEASE=1.CI would have shipped a dev-id release — also fixed
release.ymlranbash macos/build.shwith noRELEASE=1. Under this change that would have produced acom.openmessage.app.devapp and anOpenMessage-dev.dmg, breaking every downstream step that referencesmacos/build/OpenMessage.dmg(Gatekeeper assessment, artifact upload, release upload).release.ymlnow setsRELEASE=1and asserts the built app carriescom.openmessage.appand thatOpenMessage.dmgexists, before notarization.test.ymlasserts both directions: the default build must claimcom.openmessage.app.dev, and aRELEASE=1build must restorecom.openmessage.app+OpenMessage.dmg.Two fixes that do NOT work (verified, and recorded so they aren't retried)
lsregister -u <path>is not durable. Unregistered all 14 stale bundles, forced a rescan — every one came straight back.Foo.app→Foo.app.disableddoes nothing. LaunchServices registers on bundle structure, not the.appextension; it re-registered every renamed bundle at its new path. This is why an earlier rename of the r8 rollback never stuck.What works for bundles kept on disk: rename
Contents/Info.plist→Contents/Info.plist.disabled. Lossless, reversible, survives forced rescans.Also documented
mdfind "kMDItemCFBundleIdentifier == ..."is an unreliable audit — stale metadata, and it skips dot-directories (two hidden rollback bundles were found only via a forcedlsregister -R -f). Filter thelsregister -dumpbyidentifier:instead; the audit command is in the runbook..claude/worktrees/*checkout needsGOWORK=off— Go walks up, finds~/openmessage/go.work, and resolves the main module to the parent.Verification
Ran a real end-to-end build locally, then forced a LaunchServices rescan of the output:
com.openmessage.app.dev/Applications/OpenMessage.appremains the sole claimant ofcom.openmessage.appcom.openmessage.app→/Applications/OpenMessage.app/Machine cleanup was done separately (not in this diff): 219 MB of regenerable build outputs deleted, all 21 remaining bundles neutralized losslessly, live app confirmed healthy with all three transports connected.
🤖 Generated with Claude Code