Skip to content

Stop dev builds from shadowing the installed app in LaunchServices - #154

Merged
MaxGhenis merged 3 commits into
mainfrom
claude/laughing-lamarr-4ccb46
Aug 19, 2026
Merged

Stop dev builds from shadowing the installed app in LaunchServices#154
MaxGhenis merged 3 commits into
mainfrom
claude/laughing-lamarr-4ccb46

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Problem

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. Most recently 2026-07-25: a build predating the Google self-heal OSID fix (#148) launched instead of /Applications/OpenMessage.app and latched Google Messages in needs_repair from 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.0 against the real app's 0.2.9.

Fix

Make it structurally impossible for a build output to win that resolution:

bundle id DMG
./macos/build.sh com.openmessage.app.dev OpenMessage-dev.dmg
RELEASE=1 ./macos/build.sh com.openmessage.app OpenMessage.dmg

The 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.swift hardcodes ~/Library/Application Support/OpenMessage — so a dev build still reads the same store. What is id-scoped: UserDefaults (the defaults write com.openmessage.app V2Primary R8 lever), the notification grant, and the sandbox container. So a dev build's output refuses to suggest installing, and both CLAUDE.md and the deploy runbook now require RELEASE=1.

CI would have shipped a dev-id release — also fixed

release.yml ran bash macos/build.sh with no RELEASE=1. Under this change that would have produced a com.openmessage.app.dev app and an OpenMessage-dev.dmg, breaking every downstream step that references macos/build/OpenMessage.dmg (Gatekeeper assessment, artifact upload, release upload).

  • release.yml now sets RELEASE=1 and asserts the built app carries com.openmessage.app and that OpenMessage.dmg exists, before notarization.
  • test.yml asserts both directions: the default build must claim com.openmessage.app.dev, and a RELEASE=1 build must restore com.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.
  • Renaming Foo.appFoo.app.disabled does nothing. LaunchServices registers on bundle structure, not the .app extension; 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.plistContents/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 forced lsregister -R -f). Filter the lsregister -dump by identifier: instead; the audit command is in the runbook.
  • Building from a nested .claude/worktrees/* checkout needs GOWORK=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:

  • fresh build registers as com.openmessage.app.dev
  • /Applications/OpenMessage.app remains the sole claimant of com.openmessage.app
  • Finder resolves com.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

@MaxGhenis

Copy link
Copy Markdown
Owner Author

Follow-ups from this work filed as issues, so nothing here is blocking:

Note this branch is now ~2 weeks behind main (2 commits), so the green checks above are stale and would need a re-run before merge.

MaxGhenis and others added 2 commits August 12, 2026 06:26
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
MaxGhenis force-pushed the claude/laughing-lamarr-4ccb46 branch from 59feab9 to 5335874 Compare August 12, 2026 10:26
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>
@MaxGhenis
MaxGhenis merged commit 12398ce into main Aug 19, 2026
5 checks passed
@MaxGhenis
MaxGhenis deleted the claude/laughing-lamarr-4ccb46 branch August 22, 2026 15:20
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)
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