Helper app is sandboxed (entitlement + runtime check), but `URLsForDirectory:` returns user home (`/Users//`) instead of container path — why?

Problem summary

I have a macOS helper app that is launched from a sandboxed main app. The helper:

  • has com.apple.security.app-sandbox = true and com.apple.security.inherit = true in its entitlements,
  • is signed and embedded inside the main app bundle (placed next to the main executable in Contents/MacOS),
  • reports entitlement_check = 1 (code signature contains sandbox entitlement, implemented via SecStaticCode… check),
  • sandbox_check(getpid(), NULL, 0) returns 1 (runtime sandbox enforcement present),
  • APP_SANDBOX_CONTAINER_ID environment variable is not present (0).

Despite that, Cocoa APIs return non-container home paths:

  • NSHomeDirectory() returns /Users/<me>/ (the real home).
  • [[NSFileManager defaultManager] URLsForDirectory:inDomains:] and URLForDirectory:inDomain:appropriateForURL:create:error: return paths rooted at /Users/<me>/ (not under ~/Library/Containers/<app_id>/Data/...) — i.e. they look like non-sandboxed locations.

However, one important exception: URLForDirectory:... for NSItemReplacementDirectory (temporary/replacement items) does return a path under the helper's container (example: ~/Library/Containers/<app_id>/Data/tmp/TemporaryItems/NSIRD_<helper_name>_hfc1bZ).

This proves the sandbox is active for some FileManager APIs, yet standard directory lookups (Application Support, Documents, Caches, and NSHomeDirectory()) are not being redirected to the container.

What I expect

The helper (which inherits the sandbox and is clearly sandboxed) should get container-scoped paths from Cocoa’s FileManager APIs (Application Support, Documents, Caches), i.e. paths under the helper’s container: /Users/<me>/Library/Containers/<app_id>/Data/....

What I tried / diagnostics already gathered

Entitlements & code signature

codesign -d --entitlements :- /path/to/Helper.app/Contents/MacOS/Helper
# shows com.apple.security.app-sandbox = true and com.apple.security.inherit = true

Runtime checks (Objective-C++ inside helper):

extern "C" int sandbox_check(pid_t pid, const char *op, int flags);
NSLog(@"entitlement_check = %d", entitlement_check()); // SecStaticCode check
NSLog(@"env_variable_check = %d", (getenv("APP_SANDBOX_CONTAINER_ID") != NULL));
NSLog(@"runtime_sandbox_check = %d", sandbox_check(getpid(), nullptr, 0));
NSLog(@"NSHomeDirectory = %s", NSHomeDirectory());
NSArray *urls = [[NSFileManager defaultManager]
    URLsForDirectory:NSApplicationSupportDirectory
    inDomains:NSUserDomainMask];
NSLog(@"URLsForDirectory: %@", urls);

Observed output:

entitlement_check = 1
env_variable_check = 0
runtime_sandbox_check = 1
NSHomeDirectory = /Users/<me>
URLsForDirectory: ( "file:///Users/<me>/Library/Application%20Support/..." )

Temporary/replacement directory (evidence sandbox active for some APIs):

NSURL *tmpReplacement = [[NSFileManager defaultManager]
    URLForDirectory:NSItemReplacementDirectory
    inDomain:NSUserDomainMask
    appropriateForURL:nil
    create:YES
    error:&err];
NSLog(@"NSItemReplacementDirectory: %@", tmpReplacement.path);

Observed output (example):

/Users/<me>/Library/Containers/<app_id>/Data/tmp/TemporaryItems/NSIRD_<helper_name>_hfc1bZ

Other facts

  • Calls to NSHomeDirectory() and URLsForDirectory: are made after main() to avoid "too early" initialization problems.
  • Helper is placed in Contents/MacOS (not Contents/Library/LoginItems).
  • Helper is a non-GUI helper binary launched by the main app (not an XPC service).
  • macOS version: Sequoia 15.6

Questions

  1. Why do NSHomeDirectory() and URLsForDirectory: return the real /Users/<me>/... paths in a helper process that is clearly sandboxed (entitlement + runtime enforcement), while NSItemReplacementDirectory returns a container-scoped temporary path?

  2. Is this behavior related to how the helper is packaged or launched (e.g., placement in Contents/MacOS vs Contents/Library/LoginItems, or whether it is launched with posix_spawn/fork+exec vs other APIs)?

  3. Are there additional entitlements or packaging rules required for a helper that inherits sandbox to have Cocoa directory APIs redirected to the container (for Application Support, Documents, Caches)?

*Thanks in advance — I can add any requested logs

How are you starting this helper process? Via NSTask (Process in Swift)? Or posix_spawn? Or something else?

Does the helper have its own bundle structure. So, something like this:

MyApp.app/
    Contents/
        Info.plist
        MacOS/
            MyApp
            MyHelper.app/
                Contents/
                    Info.plist
                    MacOS/
                        MyHelper

Or is it a standalone executable, like this:

MyApp.app/
    Contents/
        Info.plist
        MacOS/
            MyApp
            MyHelper

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for extra info.

Based on that I set up a quick test with Apple’s native tools:

  1. Using Xcode 16.4 on macOS 15.6.1, I created a new project from the macOS > App template.

  2. I modified the content view like this:

    struct ContentView: View {
        var body: some View {
            VStack {
                Text("Parent")
                Button("Test") {
                    do {
                        let u = Bundle.main.url(forAuxiliaryExecutable: "Test799357Child.app")!
                        let b = Bundle(url: u)!
                        let e = b.executableURL!
                        print("will spawn child, path: \(e.path)")
                        let p = Process()
                        p.executableURL = e
                        try p.run()
                        print("did spawn child")
                    } catch {
                        print("did not spawn child, error: \(error)")
                    }
                }
            }
            .padding()
        }
    }
    
  3. Within that project, I created a new target from macOS > App template.

  4. And modified its content view to look like this:

    struct ContentView: View {
        var body: some View {
            VStack {
                Text("Child")
            }
            .padding()
        }
    }
    

    That way I can tell them apart (-:

  5. I added com.apple.security.inherit to the entitlements file.

  6. And removed com.apple.security.files.user-selected.read-only.

  7. I arrange to embed the child app in the parent.

  8. In both the parent and child, I added code to log the home directory path:

    struct Test799357App: App {
        init() {
            print("parent home: \(FileManager.default.homeDirectoryForCurrentUser.path)")
        }
        …
    }
    
    struct Test799357ChildApp: App {
        init() {
            print("child home: \(FileManager.default.homeDirectoryForCurrentUser.path)")
        }
        …
    }
    

    I’m using homeDirectoryForCurrentUser but I see the same results with NSHomeDirectory.

  9. I ran the parent and clicked the Test button. In Xcode I see:

parent home: /Users/quinn/Library/Containers/com.example.apple-samplecode.Test799357/Data
will spawn child, path: …/Test799357.app/Contents/MacOS/Test799357Child.app/Contents/MacOS/Test799357Child
did spawn child
child home: /Users/quinn/Library/Containers/com.example.apple-samplecode.Test799357/Data

The parent and child both got the same home directory, that being the Data directory of the main app’s container container.

I’m not sure what’s going on in your case but I recommend that you:

  • First replicate my test, to verify that it’s not something weird going on with your Mac specifically.
  • Then try integrating bits of my test into your main app, to see where things go wrong.

ps It’s better to reply as a reply, rather than in the comments; see Quinn’s Top Ten DevForums Tips for this and other titbits.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Helper app is sandboxed (entitlement &#43; runtime check), but &#96;URLsForDirectory:&#96; returns user home (&#96;/Users//&#96;) instead of container path — why?
 
 
Q