fix(macos): restore CloudKit pull sync and stop CLI SIGABRT on exit#17
Merged
fix(macos): restore CloudKit pull sync and stop CLI SIGABRT on exit#17
Conversation
Two related production bugs that surface as one user-visible problem: the CLI is unusable across machines. 1. Cross-machine data divergence. release.yml stripped both aps-environment keys from the macOS app's signed entitlements before codesigning, with a comment claiming "we don't use APNs on macOS." That's wrong on a load-bearing point: NSPersistentCloudKitContainer needs aps-environment to receive CloudKit silent push notifications. Without it, the OS never wakes the app for "remote changes happened" pushes, so the local SwiftData store never pulls from CloudKit. Each Mac became a write-only island whose store reflected only what that machine itself wrote, which is exactly why the Mac mini and MacBook showed completely different sets of tosses. The Developer ID provisioning profile already authorizes aps-environment: production, so substitute the value (development → production) instead of stripping the keys. 2. CLI SIGABRT on process exit. `toss ls` consistently crashed after printing results with a PushKit assertion raised inside NSCloudKitMirroringDelegate.dealloc → NSPersistentStore.dealloc when SwiftData tore down its CloudKit-backed model container at process exit. PushKit can't resolve a bundle identifier for a sub-bundled `.app` helper invoked via the brew cask symlink path. The CLI doesn't actually need CloudKit — it does one read or write against the local SQLite store and exits; sync is the main app's job. Add a CloudKitMode enum to TossPersistenceStack.makeContainer with .automatic (default, current behavior) and .disabled (uses ModelConfiguration.CloudKitDatabase.none). The CLI's TossEnvironment now passes .disabled, which prevents NSPersistentCloudKitContainer from being instantiated at all and removes the PushKit init from the dealloc path. The main app and share extension are unaffected — they keep the default .automatic mode and continue to own CloudKit sync. Same store file, same data; only the transport layer for the CLI changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pseudobun
added a commit
that referenced
this pull request
Apr 12, 2026
🤖 I have created a release *beep* *boop* --- ## [1.3.1](v1.3.0...v1.3.1) (2026-04-12) ### Bug Fixes * **ci:** pin iOS Release to manual signing, stop minting dev certs ([#16](#16)) ([cd4df60](cd4df60)) * **macos:** restore CloudKit pull sync and stop CLI SIGABRT on exit ([#17](#17)) ([2e447ef](2e447ef)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
7 tasks
pseudobun
added a commit
that referenced
this pull request
Apr 12, 2026
…19) ## Summary v1.3.1 is dead on arrival: every launch SIGKILLs with \`Termination Reason: Namespace CODESIGNING, Code 1, Taskgated Invalid Signature\`. Diagnosed via side-by-side of the shipped binary's signed entitlements vs. its embedded provisioning profile: | Signed in v1.3.1 binary | Authorized by profile | |---|---| | \`aps-environment: production\` | **NOT PRESENT** | | \`com.apple.developer.aps-environment: production\` | ✅ present | The bare \`aps-environment\` key is the iOS convention. macOS uses only the \`com.apple.developer.*\` namespace for developer entitlements, and the Developer ID provisioning profile authorizes \`com.apple.developer.aps-environment\` but not the bare iOS-shaped key. amfi enforces "every restricted entitlement must be profile-backed" at load time, so the bare key — which #17 started adding alongside the correct one — causes amfi to SIGKILL the binary on exec. \`codesign --verify --strict\` in CI didn't catch this because it only validates structural integrity, not profile subset. That's amfi's job and it only runs at load time on the end-user's machine. ## Changes **\`.github/workflows/release.yml\`** — in the \`Prepare sanitized entitlements\` step, **delete** the bare \`aps-environment\` key on macOS releases instead of substituting it. Keep the \`Set :com.apple.developer.aps-environment production\` line — that's the macOS runtime key for CloudKit silent push delivery, and it IS in the profile's authorized list. ## Test plan - [ ] Merge this PR and let release-please open the v1.3.2 PR - [ ] Merge v1.3.2 release PR - [ ] Re-verify the \`Prepare sanitized entitlements\` log in the v1.3.2 release workflow — the dumped plist should show \`com.apple.developer.aps-environment: production\` and NO bare \`aps-environment\` key - [ ] After release ships: \`brew upgrade --cask tossinger\` on both Mac mini and MacBook - [ ] Launch the app — should no longer crash with SIGKILL - [ ] Add a toss on one machine, wait ~30s, verify it appears on the other (cross-machine CloudKit push sync should work since \`com.apple.developer.aps-environment\` is present) - [ ] Run \`toss ls\` — should exit cleanly (CLI opt-out from #17 still applies) ## Notes - This PR reverses **only the iOS-shaped half** of the APNs entitlement work from #17. The CLI CloudKit opt-out and the iOS cert-spam fix from #16 are untouched. - #17's \`toss/toss.entitlements\` source file still declares both keys with value \`development\` — that's fine for local Debug Xcode builds (Xcode automatic signing handles the iOS/macOS key split per-platform). The release workflow is the only place we need to strip the iOS shape for macOS distribution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Summary
Two related production bugs surfacing as one user-visible problem: the CLI is unusable across machines.
Bug 1: Cross-machine data divergence
The Mac mini and MacBook show completely different sets of tosses despite both being signed in to the same iCloud account, both running the same release, and both pointing at the same `iCloud.lutra-labs.toss` container.
`release.yml:173-177` strips both `aps-environment` and `com.apple.developer.aps-environment` from the macOS app's signed entitlements before codesigning, with a comment claiming "we don't use APNs on macOS." That's wrong on a load-bearing point: `NSPersistentCloudKitContainer` (the engine SwiftData uses for `cloudKitDatabase: .private(...)` mode) needs `aps-environment` to receive CloudKit silent push notifications. Without it the OS never wakes the app for "remote changes happened" pushes, so the local SwiftData store never pulls from CloudKit. Each Mac becomes a write-only island whose store reflects only what that machine itself wrote.
I verified the parent app's embedded provisioning profile (`/Applications/Tossinger.app/Contents/embedded.provisionprofile`) authorizes `aps-environment: production`, so the entitlement can be present — the workflow just deletes it. The fix substitutes the value (development → production) instead of stripping the keys.
Bug 2: CLI SIGABRTs at process exit
`toss ls` consistently crashes after printing results with:
```
NSInternalInconsistencyException 'Invalid parameter not satisfying: bundleIdentifier...'
-[PKUserNotificationsRemoteNotificationServiceConnection initWithBundleIdentifier:]
-[PKPushRegistry _registerForPushType:]
-[NSCloudKitMirroringDelegate dealloc]
-[NSPersistentStore dealloc]
TossKit Container.deinit
ListCommand.run
```
Read bottom-up: the command finishes, prints results, exits → SwiftData tears down → `NSPersistentCloudKitContainer` deallocs → CloudKit cleanup tries to register for push types via `PKPushRegistry` → PushKit looks up the bundle identifier through some path that doesn't work for a sub-bundled `.app` helper invoked via `/opt/homebrew/bin/toss → /Applications/Tossinger.app/Contents/Helpers/toss.app/Contents/MacOS/toss` → `bundleIdentifier` resolves to nil → assertion fails → SIGABRT.
The CLI doesn't actually need CloudKit. It opens the same SQLite store as the main app via the app group, performs a single read or write, and exits. CloudKit sync is the main app's job.
Changes
`.github/workflows/release.yml` (`Prepare sanitized entitlements` step) — replace the two `Delete :aps-environment` PlistBuddy lines with `Set ... production`. Updated the misleading comment to explain why APNs is actually load-bearing for CloudKit silent push.
`Packages/TossKit/Sources/TossKit/Persistence/PersistenceStack.swift` — added a public `CloudKitMode` enum with two cases:
`makeContainer` now takes `cloudKit: CloudKitMode = .automatic`, so the app and share extension are bytewise-equivalent at runtime through the default.
`tossinger/Support/TossEnvironment.swift` — passes `cloudKit: .disabled`. Doc comment explains the rationale (CLI doesn't await async push delivery anyway, and instantiating the CloudKit container in a sub-bundled helper SIGABRTs).
Same store file, same data; only the transport layer for the CLI changes.
Test plan
Notes
🤖 Generated with Claude Code