Skip to content
macOSDeep Dive Published Updated 6 min readViews unavailable

macOS Security-Scoped Bookmarks: Persistent Access and Balanced Resource Lifetimes

Persist user-selected macOS file access with security-scoped bookmarks, stale-data renewal, balanced access lifetimes, and precise failure diagnosis.

A sandboxed macOS editor can remember a folder the user selected without acquiring unrestricted filesystem access. A security-scoped bookmark stores the information needed to resolve that resource later and request access again. The bookmark is an opaque capability associated with a particular access context, not simply a path string and not a guarantee that every subsequent I/O operation will succeed.

The difficult part is lifetime management. A URL can resolve successfully while access has not started, a bookmark can resolve while being stale, and an asynchronous worker can outlive the access granted by its caller. A reliable implementation treats bookmark persistence, resource access, and file operation completion as separate transitions.

Start with an intentional selection

For a sandboxed macOS target, configure the User Selected File capability in Xcode as read-only or read/write according to the feature. Use a standard open or save interaction so the user selects the actual resource. Do not replace that consent flow by constructing a URL from an arbitrary pathname and assuming it carries the same authorization.

Apple distinguishes implicit scope supplied by system interactions from explicit scope restored from persisted bookmark data. Follow the ownership contract of the interaction that delivered the URL. The examples here operate on an explicit bookmark resolved from storage; they do not add another unconditional access increment to a panel URL that the system has already activated.

A selected folder can authorize access to its descendants, including nested folders, but other controls can still reject individual items. The user should therefore understand the scope of choosing a root folder. If the workflow only reads one document, request that document or read-only access rather than encouraging selection of an entire home directory.

Store bookmark data rather than a reconstructed path

Create persistent app-relative bookmark data while the app has access to the selected resource. A nil relative URL means this example does not use document-relative scope. The read-only option narrows the restored capability for an importer:

import Foundation

func importerBookmark(for selectedURL: URL) throws -> Data {
    try selectedURL.bookmarkData(
        options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess],
        includingResourceValuesForKeys: nil,
        relativeTo: nil
    )
}

Store the resulting Data in the app’s durable state using a write strategy appropriate to that state. Associate it with a human-readable label and an internal identifier, but do not attempt to parse or edit its bytes. A display path may be useful in the interface; it is not a substitute for the bookmark.

Bookmarks may survive supported moves and renames, whereas a saved path can point at a different item later. Resolution is still fallible: the volume can be absent, the file can be deleted, or the signing/access context can change. Treat a bookmark as a recoverable reference, not a permanent entitlement to a pathname.

Resolve, renew, acquire, operate, release

When restoring access, resolve with .withSecurityScope, inspect the stale flag, and renew stored data when required. Then explicitly acquire access before touching the resource. Apple requires balancing successful access starts with stops; forgetting to stop leaks kernel resources and can eventually prevent the app from acquiring additional sandbox access until relaunch.

This synchronous helper is intentionally restricted to persisted, explicitly scoped bookmarks. The supplied operation must finish before returning. The replacement bookmark is returned for the caller to persist atomically with its own metadata.

enum BookmarkAccessError: Error {
    case denied
}

func withImporterBookmark<T>(
    _ data: Data,
    operation: (URL) throws -> T
) throws -> (value: T, renewedBookmark: Data?) {
    var stale = false
    let url = try URL(
        resolvingBookmarkData: data,
        options: [.withSecurityScope],
        relativeTo: nil,
        bookmarkDataIsStale: &stale
    )

    guard url.startAccessingSecurityScopedResource() else {
        throw BookmarkAccessError.denied
    }
    defer { url.stopAccessingSecurityScopedResource() }

    let renewed = stale ? try importerBookmark(for: url) : nil
    let result = try operation(url)
    return (result, renewed)
}

For example, operation can synchronously read and validate a bounded configuration file. It should not create a background task and immediately return. If it does, the defer revokes the helper’s access before that task finishes. An asynchronous implementation needs an access owner whose lifetime includes awaited I/O, cancellation cleanup, and every child operation that still uses the URL.

A false access result is not universally equivalent to unreadable data: a URL already available inside the app container may need no security scope. This helper deliberately fails closed because its contract is external access through an explicit importer bookmark. Keep that contract separate from a general-purpose URL reader that supports container URLs.

Stale does not mean unusable

The stale flag means the stored bookmark should be recreated and updated, not that the resolved resource must automatically be rejected. Renew against the resolved URL, retain the intended read-only policy, and persist the replacement safely. If renewal fails, surface the error and retain enough state to retry or request a new selection.

Do not erase the old bookmark first and then attempt to generate the replacement. A crash or write failure would unnecessarily lose the remembered selection. Keep the previous record until the new data is durably committed. If the operation itself fails after renewal, decide whether the refreshed reference should still be saved; the example returns only on success, so a production owner may separate those results when that distinction matters.

Access is not a lock or an I/O guarantee

Security scope changes sandbox reachability. It does not serialize writers, make the file immutable, download all provider content, or guarantee disk space. Use file coordination where appropriate for shared documents and still handle ordinary read, write, and provider errors.

POSIX mode bits, ACLs, System Integrity Protection, data protection, and other macOS controls can deny an operation even when the sandbox scope is valid. Diagnose the actual error domain and code. A permission error is not automatically a stale bookmark, and Full Disk Access is not a blanket workaround for a missing lifetime balance.

Keep bookmark resolution errors separate from access acquisition and file-operation errors in logs. Record an internal resource ID, whether renewal was needed, the stage that failed, and the error domain/code. Avoid logging opaque bookmark blobs or sensitive paths by default.

Validate real lifecycle boundaries

Test first selection, successful access after app relaunch, movement or renaming, an unmounted external volume, deletion, a changed ACL, and attempted writes through a read-only bookmark. Exercise cancellation while a reader is active and repeated imports over a long session to catch leaked access starts.

Also test the actual signed distribution build. App-scoped bookmarks have code-signing identity requirements; a different debug build, helper, or extension should not be assumed to consume them successfully. Apple documents different mechanisms for transient cross-process sharing and document-relative references, so choose those deliberately when the feature requires them.

Recovery should preserve the user’s work and explain the missing resource. Offer retry for an unavailable volume and a fresh selection for a lost reference. A correct bookmark manager remembers consent, renews its reference when necessary, and releases every access lifetime after the associated work is complete.

Related:

Sources:

Comments