Skip to content

Fix custom output directory not persisting in non-sandboxed builds#497

Open
igorlenskiii-web wants to merge 1 commit into
pasrom:mainfrom
igorlenskiii-web:fix-output-dir-bookmark
Open

Fix custom output directory not persisting in non-sandboxed builds#497
igorlenskiii-web wants to merge 1 commit into
pasrom:mainfrom
igorlenskiii-web:fix-output-dir-bookmark

Conversation

@igorlenskiii-web

Copy link
Copy Markdown

Symptom

Settings → Output → choosing a custom output folder appears to work (the UI shows the selected path), but nothing persists: every new recording writes to the default ~/Downloads/MeetingTranscriber/ again, and the folder is silently recreated there. Observed on the Homebrew/direct build v0.6.0-rc6, macOS 26.5.

Root cause

AppSettings.setCustomOutputDir stores the folder as a security-scoped bookmark:

guard let data = try? url.bookmarkData(options: .withSecurityScope, ...) else { return }

Security-scoped bookmarks require the App Sandbox. The direct (non-App-Store) build is not sandboxed (it can't be — CATap capture), so bookmarkData(options: .withSecurityScope) throws, the guard try? swallows the error, and the setting is never written. effectiveOutputDir then falls back to AppPaths.downloadsProtocolsDir forever, while the Settings UI keeps displaying the user's chosen path.

Fix

  • Extract bookmark creation/resolution into makeOutputDirBookmark / resolveOutputDirBookmark helpers.
  • Creation: try .withSecurityScope first (APPSTORE build keeps its behavior), fall back to a regular bookmark for the non-sandboxed build, and log an error instead of failing silently.
  • Resolution: mirror the fallback (.withSecurityScope → plain), so both bookmark kinds resolve regardless of which build wrote them.
  • Stale-bookmark re-creation goes through the same helper.

AppSettings+RPC.rpcOutputDirPath needs no change — it already resolves with [.withoutUI, .withoutMounting], which works for both bookmark kinds.

Testing

Reproduced the symptom on macOS 26.5 with the rc6 direct build (chosen folder shows in UI, files keep landing in ~/Downloads/MeetingTranscriber). The patch was reviewed against the existing style but not compiled locally — relying on CI (unit tests + E2E) to exercise it; happy to adjust if anything trips.

🤖 Generated with Claude Code

Security-scoped bookmarks require the App Sandbox. The direct
(non-App-Store) build is not sandboxed, so
bookmarkData(options: .withSecurityScope) throws there; the
`guard try?` in setCustomOutputDir swallowed that error, the chosen
folder silently never persisted, and output always fell back to
~/Downloads/MeetingTranscriber while the Settings UI kept showing the
selected path.

Fix: try the scoped bookmark first (APPSTORE build), fall back to a
regular bookmark, mirror the same fallback when resolving, and log a
creation failure instead of silently returning.
@pasrom

pasrom commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Thanks for digging into this and for the clean, well-documented change. The symptom you describe (custom output folder shows in the UI but recordings keep landing in ~/Downloads/MeetingTranscriber) is real and worth fixing, so I want to get the root cause pinned down before merging, because I could not reproduce the specific mechanism this PR targets.

I could not reproduce the scoped-bookmark throw

The stated root cause is that bookmarkData(options: .withSecurityScope) throws in the non-sandboxed direct build, so guard try? swallows it and the folder never persists. I tried to reproduce that throw and could not, in any configuration I tested on the direct (non-App-Store) build:

  • a bare non-sandboxed process,
  • a real signed .app bundle with the exact Homebrew entitlements (mic only, no com.apple.security.app-sandbox), both ad-hoc and Developer-ID signed,
  • create in one process / resolve in a relaunched process,
  • TCC-protected targets (Documents / Desktop / Downloads).

In all of them url.bookmarkData(options: .withSecurityScope) succeeds and URL(resolvingBookmarkData:options: .withSecurityScope) resolves it back; startAccessingSecurityScopedResource() even returns true. Apple's docs say scoped bookmarks are "used in sandboxed apps", but a non-sandboxed app creating and resolving them does not appear to throw. If you have a build/OS where the scoped bookmarkData call actually throws, a captured error (the NSError domain/code) would be very helpful, because that is the crux.

Why that matters for this diff

Because the scoped-create branch succeeds on the direct build, makeOutputDirBookmark returns from its first if let and the new options: [] create fallback is never reached there. So in the non-sandboxed build the bytes stored by setCustomOutputDir are the same scoped bookmark as before, and customOutputDir resolves them the same way as before. That means this change would not alter the stored-or-resolved behavior on the affected build, which is what makes me think the persistence failure is coming from somewhere other than bookmark creation.

A likelier cause to check

customOutputDir feeds effectiveOutputDir, and the pipeline captures effectiveOutputDir by value when the processing queue is built; that queue is only rebuilt at an idle watch-start. So if you pick a new folder while watching is active or a job is in flight, the Settings UI updates immediately (it reads effectiveOutputDir live) but the running queue keeps writing to the previously captured directory. That reproduces your exact symptom with the bookmark working correctly. Could you check:

  • Does the chosen folder stick after a full quit + relaunch, or after toggling watching off and back on while idle? If it starts working then, this is the queue-capture path, not bookmarks.
  • After picking a folder, is customOutputDirBookmark actually present in the app's defaults?

What I would happily take from this PR regardless

  • The resolve-side tolerance (accepting a plain bookmark as well as a scoped one) is a genuine robustness win: resolving a plain bookmark with .withSecurityScope does throw Cocoa 259, so once any non-scoped bookmark exists, the fallback is the right call.
  • The logger.error on creation failure is a good idea too.

The one thing that needs to change before this could merge is the doc comment on makeOutputDirBookmark: it states the ".withSecurityScope throws in the non-sandboxed build" mechanism as established fact, and I have not been able to reproduce that, so it should not be baked into the source as the rationale.

Appreciate the contribution; let's confirm the repro and I think we land the useful parts of this quickly.

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.

2 participants