Daeseon Yoo
Back to project
·Tech retro·6 min

The six ghost apps: shipping the first signed build, then losing a day to macOS TCC

Landed and verified a parallel-agent integration, shipped the first code-signed build (v0.2.28), then spent the evening fighting a macOS permission popup that would not stop — because six renamed copies of the app all shared one bundle identifier. Plus the real lesson: a fresh chat session forgot how to deploy, and almost rebuilt a pipeline that already existed.

A long day that produced one clean release and one ugly bug, and a process lesson sitting underneath both.

TL;DR

What shipped

The morning was the good part. A parallel agent had written a Rust adapter layer that reads Codex and Antigravity transcripts and normalizes them into the same shape the app already used for Claude, plus a "Workbench" panel — a seven-zone board for dispatching work to per-zone agents, with a read-only git snapshot underneath.

Instead of trusting the worklog's "tests pass" claim, I re-ran everything:

Only then did it land (8c507b8), with the blueprints in a separate commit (b684c4d). Two of the three "failures" in the tree — Biome lint (179 diagnostics) and rustfmt drift — turned out to be pre-existing repo-wide debt, not anything this work introduced. Worth saying out loud: "the worklog says the tests pass" is not verification. Re-running it is.

The redeploy I had forgotten

Then: "redeploy it, I want to use the feature."

A fresh session has no memory of how this app actually ships. I looked at the standard secrets file, did not find a signing key, and concluded out loud that there was no redeploy pipeline. That was wrong, and the founder rightly pushed back: you used to redeploy this yourself.

The fix was not to guess harder — it was to read the action logs. Every Bash command this project's agents run gets logged to logs/actions.jsonl. Grepping it for tauri build surfaced the exact runbook I had "forgotten," verbatim:

pnpm -C apps/desktop tauri build        # signs with a minisign key kept outside the repo
cp -R .../bundle/macos/Talkak.app /Applications/    # local install
cp the .dmg + .app.tar.gz into landing/, regenerate latest.json
deploy landing/ to Vercel  ->  talkak.daeseon.ai/latest.json  ->  the updater serves it

The updater config (createUpdaterArtifacts: true, a public key, an endpoint at talkak.daeseon.ai) had been right there in tauri.conf.json. The signing key was a minisign key stored outside the repo, read into an env var at build time and never printed. None of it was missing. The session just did not know where to look.

That is the real bug of the day, and it is not in any source file.

The first signed build

The founder had also just become a registered Apple Developer and wanted the permission popups to stop. Important distinction that is easy to get wrong: code signing does not remove macOS TCC prompts. What it does is make the grant stick. An unsigned (ad-hoc) build gets a different code identity on every rebuild, so macOS treats each version as a new app and re-asks for Documents access every single time. Sign with a stable identity and the grant persists across updates.

So v0.2.28 became the first signed build, using the Apple Development certificate already in the keychain (stable TeamIdentifier). The bundle came out signed and valid (satisfies its Designated Requirement), the updater artifacts minisign-signed, latest.json regenerated. Installed to /Applications/Talkak.app, committed as a29e07e.

That should have been the end of it.

The six ghost apps

It was not. The Documents prompt kept firing. Then the screenshot arrived, and one detail broke the whole thing open: the prompt said "DalkkakAI.app" — but the app I had built, signed, and installed was "Talkak.app". The product had been renamed at some point; the bundle on disk was Talkak, but TCC was naming something else.

The reason: macOS TCC remembers permission grants by CFBundleIdentifier, not by app name. A quick audit with mdfind found the problem — there were six copies of the old-named app bundle scattered around:

/Applications/DalkkakAI 2.app
~/Desktop/Talkak/DalkkakAI/DalkkakAI.app
~/Desktop/Talkak/DalkkakAI 1/DalkkakAI.app
~/Desktop/Talkak/DalkkakAI 2/DalkkakAI.app
~/Desktop/Talkak/DalkkakAI 3/DalkkakAI.app
apps/desktop/src-tauri/target/release/bundle/macos/DalkkakAI.app  (stale build output)

Every one of them — and the new Talkak.app — carried the same identifier ai.ddalkkak.desktop. So macOS had several different code identities all claiming one bundle id. The grant could not pin to any of them, the system kept showing a stale name, and "Allow" never stuck. Clicking Allow on one identity did nothing for the next request from a different one.

The fix was filesystem hygiene, not code:

  1. Moved all six DalkkakAI.app copies to the Trash, leaving exactly one app with that bundle id: /Applications/Talkak.app.
  2. tccutil reset All ai.ddalkkak.desktop to clear the confused grant state.
  3. Relaunched the single signed Talkak.app so the next grant pins to one stable identity.

A self-inflicted aftershock: reset All clears every permission, so the app then re-asks for each one (Documents, then "access data from other apps") one time. Those are answered once and they stop — but in the moment they look like the bug continuing.

A subtle trap also showed up mid-cleanup: moving a .app bundle to the Trash does not kill a process already running from it. The bundle moves; the live process keeps requesting permissions under the old name until it is actually killed.

Lessons

The release itself was clean. The day got long because the knowledge of how to ship it did not survive the start of a new conversation — which is the whole argument for writing this down.