macOS Security-Scoped Bookmarks

macos_security_scoped_bookmarks is a Flutter plugin for creating, refreshing, and safely accessing app-scoped macOS security bookmarks.

The package owns every native resolution and exposes it only as a callback- scoped resource. Access, refresh, and cleanup therefore use the exact same native NSURL, even when separate bookmarks resolve to the same filesystem path. File and directory kinds are detected by macOS rather than supplied by the caller.

How does this compare with macos_secure_bookmarks?

This package is a modern, safety-focused alternative to macos_secure_bookmarks. The comparison below targets version 0.2.1, the latest published version at the time of writing.

macos_security_scoped_bookmarks macos_secure_bookmarks 0.2.1
Access lifetime Automatically bounded by withBookmarkAccess Callers must manually resolve, start, and stop access
Native resource identity A unique token retains each exact resolved NSURL Resolved URLs are indexed by filesystem path
Concurrent same-path resolutions Kept independent Can overwrite one another in the native path map
Permissions Read-only by default; read/write is explicit Creates bookmarks without the read-only restriction
Stale bookmarks Reports staleness and can refresh the exact resolved URL Detects staleness internally but cannot expose or refresh it
File or directory Detected by macOS Supplied by the caller through isDirectory
Cleanup failures Automatic cleanup; preserves operation and cleanup errors Cleanup and error preservation are the caller's responsibility
Persisted value Versioned value containing bookmark data and its access mode Raw base64 native bookmark data
Privacy Does not log filesystem paths Native implementation prints resolved paths
Toolchain Dart 3.7+, Flutter 3.29+, macOS 10.14+ Dart 2.12–2.x, Flutter 1.17+, macOS 10.11+
Native integration CocoaPods and Swift Package Manager CocoaPods

For new applications, we recommend macos_security_scoped_bookmarks. Its callback API makes the safe lifecycle the default: protected work finishes before access is released, cleanup also runs after exceptions, and concurrent bookmarks do not collide merely because they resolve to the same path. It also supports least-privilege read-only bookmarks and the stale-bookmark refresh flow required by long-lived applications.

macos_secure_bookmarks may still be relevant when an application must support macOS 10.11–10.13 or an older pre-Dart-3 Flutter toolchain. Its manual API may also be easier to adopt temporarily in an existing application already built around it, but the application must balance every successful start with a stop and handle stale bookmarks itself.

The two packages are not drop-in API replacements. In particular, an existing macos_secure_bookmarks value is raw native bookmark data, while this package persists a versioned value that also records whether access is read-only or read/write. Plan an explicit data migration and choose the appropriate access mode before switching an application with stored bookmarks.

Usage

Create bookmark data immediately after an authorized user action, such as an operating-system file picker or drag and drop. Bookmarks are read-only by default; request read/write access explicitly only when it is needed:

import 'dart:io';

import 'package:macos_security_scoped_bookmarks/macos_security_scoped_bookmarks.dart';

final bookmark = await createBookmark(
  file,
  access: MacOSSecurityScopedBookmarkAccess.readOnly,
);

// Persist this complete opaque value, not native bookmark data by itself.
await storage.write(bookmark.encode());

Restore the complete bookmark value, then perform filesystem work inside withBookmarkAccess. The callback receives the resolved file or directory only while its native security scope is active:

final bookmark = MacOSSecurityScopedBookmark.decode(await storage.read());

final byteCount = await withBookmarkAccess(
  bookmark,
  (resource) async {
    if (resource.isStale) {
      final refreshedBookmark = await resource.refreshBookmark();
      // Atomically replace the stored bookmark.
      await storage.write(refreshedBookmark.encode());
    }

    return (resource.entity as File).length();
  },
);

Do not retain the resource or its entity after the callback completes. withBookmarkAccess always attempts to stop native access and discard the resolution. It also preserves both errors if the operation and cleanup fail. The encoded MacOSSecurityScopedBookmark keeps native bookmark data and its access mode together, and refresh automatically preserves that mode.

All protected filesystem work must finish inside the callback. Return only detached results such as bytes, strings, paths, counts, or application models. A future-returning operation such as readAsBytes is safe because withBookmarkAccess waits for that future:

final bytes = await withBookmarkAccess(
  bookmark,
  (resource) => (resource.entity as File).readAsBytes(),
);

Do not return the resource, its entity, a stream, or an open filesystem handle. Creating a stream does not consume it, so this releases access too early:

// Incorrect: the security scope ends before this stream is consumed.
final stream = await withBookmarkAccess(
  bookmark,
  (resource) => (resource.entity as File).openRead(),
);

Consume lazy operations completely inside the callback instead:

final chunks = await withBookmarkAccess(
  bookmark,
  (resource) => (resource.entity as File).openRead().toList(),
);

If starting access fails at the channel boundary or receives a malformed response, the plugin cannot know whether native access started. It therefore attempts to stop access before rethrowing the original error. If cleanup also fails, MacOSSecurityScopedBookmarkCleanupException retains both failures.

Isolates

The default service is stateless and can be used from multiple isolates. Before calling plugins in a spawned background isolate, initialize its BackgroundIsolateBinaryMessenger with the root isolate token. Transfer an encoded MacOSSecurityScopedBookmark between isolates, decode it in the target isolate, and complete withBookmarkAccess before that isolate exits. Never transfer or retain the callback-scoped resource.

macOS entitlements

Sandboxed applications need app-scoped bookmark access:

<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>

For read-only bookmarks and picker access, add:

<key>com.apple.security.files.user-selected.read-only</key>
<true/>

For read/write bookmarks, use this entitlement instead:

<key>com.apple.security.files.user-selected.read-write</key>
<true/>

The example persists the complete bookmark value, restores it across launches, and contains complete Debug/Profile and Release entitlement files. Bookmark data reveals access to a user-selected location; choose storage appropriate for your application's privacy and threat model.

Errors and privacy

The plugin does not log filesystem paths. Native creation, resolution, and refresh errors are returned as PlatformExceptions whose details contain the native error domain and code when available. withBookmarkAccess throws MacOSSecurityScopedBookmarkAccessException when macOS refuses to activate the security scope. If both an operation and automatic cleanup fail, it throws MacOSSecurityScopedBookmarkCleanupException, which retains both errors and their stack traces so callers can diagnose the operation.