
The App Sandbox: Learning to Live in a Cage
You cannot open a file without asking permission. Every time.
The Mac App Store requires sandboxing. Submit without the sandbox entitlement and the app is rejected. The sandbox is Apple’s security model for distributed Mac software, and for most productivity apps it is a moderate inconvenience. For a disk analyzer, it is a fundamental constraint.
Renala’s job is to read the filesystem. The sandbox says: not without permission.
What the sandbox does
Think of it as a container, similar in spirit to Docker but enforced at the OS level. A sandboxed app gets its own filesystem slice under ~/Library/Containers/<bundle-id>/Data/. Preferences, caches, application support files: all live there, not in the real home directory.
By default, a sandboxed app cannot read arbitrary files. It cannot open /Users/you/Documents/ without the user explicitly granting access. It cannot access volumes other than the startup disk. It cannot read certain system directories at all.
The security model is sound. A sandboxed app that gets compromised cannot read your SSH keys, cannot access your email, cannot touch other apps’ data. The cost is that legitimate apps, including disk analyzers, have to work harder for capabilities that non-sandboxed apps get for free. A disk analyzer that cannot read the disk is a philosophical statement, not a product.
Security-scoped bookmarks
When the user selects a directory through NSOpenPanel, the app receives a URL with access for the current session. Close the app and reopen it: the URL is just a path string. Access is gone.
To reopen the same directory after a restart without re-prompting, you need a security-scoped bookmark. When the user grants access, you call url.bookmarkData(options: .withSecurityScope, ...) and store the resulting blob. On next launch, URL(resolvingBookmarkData:) reconstructs the URL, and url.startAccessingSecurityScopedResource() activates the scope.
The access is not free. You must call stopAccessingSecurityScopedResource() when done, or the security scope leaks. The system limits how many scopes can be open simultaneously.
When it works, the bookmark system is invisible. The user grants access once and never thinks about it again. Don’t Make Me Think applies to security UX as much as navigation. A permission prompt that appears unexpectedly reads as a bug, even when it is technically correct behavior.
flowchart TD
A["User picks folder<br/>in NSOpenPanel"] -->|"grants access"| B["URL + security scope<br/>active for session"]
B -->|"url.bookmarkData(...)"| C["Bookmark blob<br/>persisted to disk"]
B -->|"startAccessingSecurityScopedResource"| D["Access files"]
D -->|"stopAccessingSecurityScopedResource"| E["Scope released"]
C -->|"next launch"| F["URL(resolvingBookmarkData:)"]
F -->|"startAccessingSecurityScopedResource"| D
F -->|"stale or invalid"| G["Re-authorization<br/>permission dialog"]
The NSHomeDirectory trap
Early in development, the cloud drive detection code checked whether a path started with the user’s home directory. The logic used NSHomeDirectory() to get the home path. Reasonable approach, wrong environment.
Inside a sandboxed app, NSHomeDirectory() returns the container path: ~/Library/Containers/com.renala.app/Data/. Not /Users/you/. The real home directory is invisible to this API.
Every cloud drive path check silently failed. iCloud paths start with /Users/you/Library/Mobile Documents/. Google Drive paths start with /Users/you/Library/CloudStorage/. Neither starts with the container path. The cloud detection code was a no-op for every user. No crash, no error, no log. Just silently, confidently wrong. The code passed every test I wrote, because I wrote the tests in the same wrong environment.
The fix: do not use NSHomeDirectory() at all. Check if the path contains "/Library/CloudStorage/" or "/Library/Mobile Documents/". String containment, not prefix matching. The real path will contain those substrings regardless of what the home directory prefix is.
The read-write entitlement
The sandbox entitlement for file access has two modes: read-only and read-write. A disk analyzer only reads files. Read-only is the obvious choice. Smaller footprint, cleaner security posture.
Then came Move to Trash.
NSWorkspace.recycle(_:completionHandler:) moves files to the Trash. It requires write access. A read-only entitlement blocks the call silently. Nothing moves, no error.
The trade-off: request the read-write entitlement. Broader than strictly necessary for scanning, but a disk analyzer that can only show you the problem without letting you fix it is a diagnostic tool with no cure. Nobody wants that.
graph TD
A["App Sandbox entitlement<br/>com.apple.security.app-sandbox"] --> B["File read-write entitlement<br/>com.apple.security.files.user-selected.read-write<br/>(required for Move to Trash)"]
A --> C["User-selected files entitlement<br/>com.apple.security.files.user-selected.read-only<br/>(temporary session access)"]
B --> D["Bookmarks entitlement<br/>com.apple.security.files.bookmarks.app-scope<br/>(persistent access across launches)"]
C --> D
External volumes
Scanning an external SSD, a USB drive, or a Time Machine volume requires access beyond the startup disk. The sandbox does not grant this by default.
The entitlement com.apple.security.files.all-volumes extends access to all mounted volumes. Without it, NSOpenPanel can still be used to grant access to a specific external volume on a per-session basis, but the experience is fragmented and the permissions do not persist cleanly across launches.
For a tool whose primary use case is “help me understand what is on all my drives,” blocking external volumes by default would be a significant limitation.
A friend’s feedback
A friend, a graphic designer and UX specialist, tested the app on his Intel Mac. His written feedback included this: “I get the impression it doesn’t analyze hidden files. Maybe an option should be added? Besides, you can’t even target a hidden folder directly to analyze it.”
He was right on both counts. The scanner skips hidden files by default (a common pattern in macOS file enumeration), so .Trash, .config, and similar directories are excluded from the results. The sandbox compounds this: the standard file picker does not show hidden items, so even a user who wants to scan a hidden folder has no way to navigate to it through the UI.
Adding a “show hidden files” toggle to the scanner is straightforward. Letting users target a hidden folder through the file picker requires configuring the picker explicitly, and most users would not expect to need it. These remain known limitations. The feedback confirmed they are not invisible: a designer noticed within the first session.
Living with the constraints
For most apps, the sandbox is minor overhead. For a disk analyzer, it requires explicit attention to permissions, bookmark lifecycle, and path handling.
The bugs it introduces are the worst kind: silent. NSHomeDirectory() returning the container path will not crash. It will not log an error. It will just make cloud detection fail for every user, and the code will look correct the entire time.
The container path distinction, the bookmark lifecycle, the read-write requirement for trash: each of these is documented somewhere in Apple’s developer documentation. Spread across different sections, none flagged as gotchas. You find them the way you find all platform landmines: by stepping on them.
The App Store requires the sandbox. The implementation details are left as an exercise for the developer.
References
- Apple: App Sandbox overview: Concepts, container layout, and entitlement reference.
- Apple: startAccessingSecurityScopedResource: Security-scoped bookmark activation and scope lifecycle.
- WWDC 2012: The OS X App Sandbox: Original deep dive into sandbox design, containers, and entitlements.
- Apple: Entitlements reference: Full list of available entitlement keys and their effects.