[BUG] macOS: iOS Simulator sidecar aborts on every call — sandbox blocks its per-bundle-id Metal shader cache dir

Status Closed — duplicate
Maintainer reply None cached
Activity 1 comment · opened Aug 14, 2026 · closed Aug 15, 2026

iOS Simulator helper crashes on every call: missing per-bundle-id Metal cache directory

Component: Claude desktop app — iOS Simulator tool (claude-ios-sim sidecar)
Severity: Tool is 100% unusable. 33/33 calls failed over two days; no successful call after onset.
Symptom: Every attach / screenshot / tap returns
Claude Code iOS Simulator is restarting after a crash. Try again in 1s.
The sidecar dies with exit code=134 (SIGABRT) about one second after spawn.

---

Summary

The sidecar's seatbelt profile grants no write access under the Darwin user cache
directory. Because the sidecar is a bundled app, Metal keys its shader cache by bundle
id, so the directory it needs is:

$(getconf DARWIN_USER_CACHE_DIR)/com.anthropic.claude.ios-sim/com.apple.metal

Nothing else on a Mac ever writes to that path — the bundle id belongs to this helper alone
— and the helper can never create it, because packaged builds always sandbox it. On any
machine where that directory does not already exist, MTLGetShaderCachePath() fails and
caches nil behind its dispatch_once. Apple's -[_MTLDevice recordBinaryArchiveUsage:]
then reads that cached nil with no nil check and aborts the process.

The one-line fix is at the bottom.

Environment

| | |
|---|---|
| macOS | 27.0 beta, build 26A5406e |
| Hardware | Apple M4 |
| Claude | 1.30096.1 |
| Xcode | Xcode-beta only, at /Applications/Xcode-beta.app (xcode-select -p points there) |
| Sidecar | Claude iOS Sim.app/Contents/MacOS/claude-ios-sim, bundle id com.anthropic.claude.ios-sim, arm64, not translated |
| Profile | Claude.app/Contents/Resources/claude-ios-sim.sb |

Timeline

  • First crash: 2026-08-13 12:54:31
  • Crashes before that date: 0
  • Total: 33, spanning 2026-08-13 12:54 to 2026-08-14 11:27
  • Claude.app mtime: 2026-08-13 21:34

The tool worked before Aug 13 and has never worked since. See "What I could not verify"
for how much of the cause of that date boundary is inference.

Crash

Exception: EXC_CRASH (SIGABRT), Termination: Abort trap: 6, exit 134
NSInvalidArgumentException:
  *** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[0]

Triggered thread (com.apple.root.default-qos.cooperative), inner frames first:

CoreFoundation   -[__NSPlaceholderArray initWithObjects:count:]
CoreFoundation   +[NSArray arrayWithObjects:count:]
Metal            __39-[_MTLDevice recordBinaryArchiveUsage:]_block_invoke
libdispatch      _dispatch_once_callout
Metal            -[_MTLDevice recordBinaryArchiveUsage:]
Metal            -[_MTLBinaryArchive loadFromURL:error:]
Metal            -[_MTLBinaryArchive initWithOptions:device:url:error:]
CoreImage        +[CIKernelLibrary(Internal) internalBinaryArchiveWithName:device:]
CoreImage        CI::PrecompiledUberFunctions::PrecompiledUberFunctions(CI::MetalContext const*)
CoreImage        CI::new_uber_functions(CI::MetalContext const*)
CoreImage        CI::MetalContext::init(id<MTLCommandQueue>, char const*)
CoreImage        +[CIContext(Internal) internalContextWithMTLCommandQueue:options:]
CoreImage        -[CIContext initWithOptions:]
FBSimulatorControl  FBSurfaceImageGenerator.image()

App-side log:

[Simulator] sidecar sandbox=enabled profile=/Applications/Claude.app/Contents/Resources/claude-ios-sim.sb
[Simulator] sidecar spawn: .../Claude iOS Sim.app/Contents/MacOS/claude-ios-sim
[Simulator] sidecar exited code=134 sig=null
[Simulator] sidecar crash — detaching 1 iOS device(s)
[Simulator] failed to open frame stream: Error: [Simulator] sidecar exited (code=134 sig=null)
[Simulator] attach failed (describe_failed): ... restarting after a crash

Root cause

1. What objects[0] is. Disassembling the crashing block, the array is
@[<global>, @"archiveUsage.db"], passed to a path-building call:

__39-[_MTLDevice recordBinaryArchiveUsage:]_block_invoke:
  +68:  ldr  x8, [x8, #0xc8]      ; MTLGetShaderCachePath::onceToken
  +76:  cmn  x8, #0x1             ; compare with -1 (dispatch_once DONE)
  +80:  b.ne <+316>               ; token != DONE  -> skip entirely
  +88:  ldr  x8, [x8, #0xa0]      ; MTLGetShaderCachePath::result   <-- objects[0]
  +104: add  x9, x9, #0x80        ; @"archiveUsage.db"
  +108: stp  x8, x9, [sp, #0x18]
  +128: bl   +[NSArray arrayWithObjects:count:]   ; count = 2

lldb resolves both globals by name:

Metal`MTLGetShaderCachePath::result      (Metal.__DATA.__bss + 40)
Metal`MTLGetShaderCachePath::onceToken   (Metal.__DATA.__bss + 80)

So the block proceeds only when the once has completed, then reads the cached result
without checking it for nil. If MTLGetShaderCachePath() ran and failed, the token is
DONE and the result is nil — and this code dereferences it. That missing nil check is
Apple's bug; the rest of this report is about why we hand them a failed path.

2. Why the path resolution fails. MTLGetShaderCachePath reaches
getCacheMainFolder(char*, unsigned long, char const*, bool), which builds the directory
and calls mkdir(path, 0777) tolerating EEXIST(17) — the constants 511 (0777) and
17 are both visible in its disassembly. If the directory is absent and mkdir returns
anything else — EPERM from seatbelt — resolution fails and nil is cached.

3. The part that makes this easy to misdiagnose. The cache path depends on whether the
process is bundled:

| Process | MTLGetShaderCachePath() returns |
|---|---|
| bare command-line tool | .../C/com.apple.metal |
| app bundle with a bundle id | .../C/<bundle-id>/com.apple.metal |

The sidecar is bundled, so it needs .../C/com.anthropic.claude.ios-sim/com.apple.metal.
Every probe run as a plain binary resolves a valid path and works fine, including under the
real profile — which is what makes this look like it is not the sandbox at all.

4. The profile gap. claude-ios-sim.sb allows writes to DARWIN_TMP
(_CS_DARWIN_USER_TEMP_DIR, .../T/) but nothing under _CS_DARWIN_USER_CACHE_DIR
(.../C/). Those are different directories. Running a probe under the real profile:

cache base: /var/folders/<x>/<y>/C/
  mkdir  OK(exists)   com.apple.metal
  mkdir  DENIED(1 Operation not permitted)  probe.testdir
  write  DENIED(1 Operation not permitted)  com.apple.gpuarchiver/archiveUsage.db
  write  DENIED(1 Operation not permitted)  probe.testfile

Metal also emits, on stderr under the profile:

sandbox_extension_issue_file failed for /var/folders/<x>/<y>/C//com.apple.metalfe: 1 (Operation not permitted)
sandbox_extension_issue_file failed for /var/folders/<x>/<y>/C//com.apple.gpuarchiver: 1 (Operation not permitted)

Why this passes on a developer machine

sidecarSandbox.ts gates the sandbox as:

app.isPackaged ? true : process.env.CLAUDE_SIM_SANDBOX !== '0'

Unpackaged dev builds can disable it, and the pre-sandbox build ran unconfined. Either one
creates .../C/com.anthropic.claude.ios-sim/ once, permanently, as a side effect. After
that the sandboxed sidecar's mkdir returns EEXIST and everything works — so a machine
that has ever run the helper unsandboxed cannot reproduce this.

The profile's own header notes the rules were derived by bisecting against a real booted
device until screenshot, tap, and video all passed. On an already-warmed machine that
bisect converges on a profile with this hole still in it, and it also warns that "a missing
rule does NOT fail loudly."

Confirmation

Creating the directory from an unsandboxed process carrying the same bundle id — nothing
else changed, profile untouched and byte-identical to shipped — fixed it completely:
screenshot, tap, and a live h264 frame stream all work, and the sidecar has stayed up
across dozens of calls. Restoring the missing directory is sufficient and necessary.

Suggested fix

Primary — grant the sidecar its own Metal cache namespace. Pass the per-bundle cache path
as a parameter the way DARWIN_TMP is already passed:

// sidecarSandbox.ts, alongside DARWIN_TMP
METAL_CACHE: path.join(confstr(_CS_DARWIN_USER_CACHE_DIR), 'com.anthropic.claude.ios-sim')
;; Metal keys its shader/binary-archive cache by bundle id under the Darwin user CACHE
;; dir, which is NOT DARWIN_TMP. Without write access getCacheMainFolder()'s mkdir gets
;; EPERM, MTLGetShaderCachePath() caches nil, and -[_MTLDevice recordBinaryArchiveUsage:]
;; dereferences that nil with no check -> SIGABRT on the first frame.
(allow file-write* (subpath (param "METAL_CACHE")))

This stays scoped to the helper's own cache namespace; the rest of C/ remains denied.

A parent-side mkdir -p of that directory before spawn is worth adding as belt and
braces, but it is not sufficient on its own: a GPU driver or macOS bump makes Metal
create a new versioned subdirectory inside, which the sandbox would still deny. The write
rule is the durable fix.

Worth re-running the profile's validation recipe on a machine that has never run the
helper unsandboxed — otherwise this class of gap stays invisible.

What I could not verify

  • Other machines. Everything above is measured on one Mac. The argument that it

generalizes is reasoning from the bundle id being exclusive to this helper and the
sandbox being unconditional in packaged builds — not an observation of other users.

  • Why Aug 13 specifically. That the sandbox landed then is inference from the crash

timeline plus the profile's contents. I did not confirm it against release history. An
equally consistent story is that the directory existed earlier and was pruned.

  • Whether a stable Xcode changes anything. Only Xcode-beta is installed here, so the

SimulatorKit loaded is the beta's. I have no reason to think it matters — the crash is
in CoreImage and Metal, not SimulatorKit — but I did not test the other configuration.

Reproducing on a clean machine

  1. Confirm $(getconf DARWIN_USER_CACHE_DIR)/com.anthropic.claude.ios-sim does not exist.
  2. Boot any simulator, call the iOS Simulator tool's screenshot.
  3. Sidecar exits 134; a claude-ios-sim-*.ips report lands in

~/Library/Logs/DiagnosticReports/ with the stack above.

To flip it back and forth, create or remove that directory between runs. Note that a
minimal CoreImage repro must use CIContext(options:) — the initializer
FBSurfaceImageGenerator.image() uses — because CIContext(mtlDevice:) and
CIContext(mtlCommandQueue:) do not load the precompiled binary archive and will not
reproduce it.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗