Here's a breakdown of the different attempts made in case it's helpful:
Without the Endpoint Security entitlement, nono uses Seatbelt (sandbox_init()) for macOS sandboxing. Seatbelt is a static, apply-once sandbox — once set, permissions cannot be expanded. This creates fundamental challenges for interactive permission management. Here's what we've attempted:
Goal: Use sandbox_extension_issue_file() / sandbox_extension_consume() to dynamically grant file access to sandboxed processes.
Result: Extension tokens do not survive exec(). The kernel resets the grant table on exec, so tokens consumed before exec are wiped, and tokens consumed after exec return success but are not honored. Since nono must exec() the target program (Claude Code, shell commands, etc.), this approach is unusable.
Fallback: SCM_RIGHTS fd passing — the unsandboxed supervisor opens files and passes fds over a Unix socket. This works but only for programs that integrate the nono SDK and explicitly request fds. Unmodified programs (cat, git, ssh) can't use it.
Goal: Intercept shell commands spawned by the AI agent and route them through nono-shell for per-command sandboxing with interactive retry prompts on denial.
Architecture:
- Supervisor listens on a Unix socket
SHELL=nono-shellset in agent environmentDYLD_INSERT_LIBRARIESinjects a C interposition library that hooksposix_spawn,posix_spawnp, andexecveto redirect/bin/bash,/bin/zsh, etc. tonono-shellnono-shellconnects to the supervisor, which forks a per-command sandbox, monitors for denials, and offers interactive retry
Result: Two independent failures:
-
DYLD stripped by hardened runtime. Claude Code's binary is signed with hardened runtime (
flags=0x10000(runtime)), which causes macOS to stripDYLD_INSERT_LIBRARIESat exec time. The interposition library never loads. (Note: Seatbelt itself does NOT strip DYLD — this was verified empirically. It's specifically the hardened runtime on the target binary.) -
Claude Code ignores
$SHELL. Claude Code readsprocess.env.SHELLbut only uses it as the shell interpreter when the path contains"bash"or"zsh". Otherwise it falls back to searching/bin/bash,/bin/zshin hardcoded paths. SinceSHELL=nono-shelldoesn't contain those strings, Claude Code bypasses nono-shell entirely and spawns/bin/bashdirectly.
Partial workaround in progress: Create a symlink proxy_dir/bash → nono-shell so the SHELL path contains "bash". This is fragile — it depends on Claude Code's internal shell detection heuristics not changing.
Goal: Use Seatbelt's rule ordering (last-match-wins at equal specificity) to allow specific files within broadly denied directories.
How it works: Policy groups like deny_credentials generate (deny file-read-data (subpath "~/.ssh")). Profile entries like read_file: ["~/.ssh/config"] generate (allow file-read-data (literal "~/.ssh/config")) as a platform rule placed after the deny, overriding it via last-match-wins.
Limitations:
- All permissions must be declared upfront — no dynamic expansion
- No interactive retry (denials are final within the running sandbox)
- Complex rule ordering semantics that are undocumented and empirically discovered
- Initial implementation had a bug where allow rules appeared before deny rules in the generated profile, requiring a separate
apply_file_deny_exceptions()pass
Goal: Detect Seatbelt denials after a command fails and offer to retry with expanded permissions.
How it works:
DenialMonitorspawnslog streamto capture Seatbelt denial events from macOS unified log- Stderr fallback parses "Operation not permitted" / "Permission denied" patterns
TtyGuardstops the agent process group, claims terminal foreground, shows retry prompt- If approved, adds capabilities and reruns the command
Limitations:
- Only works in shell proxy mode where the supervisor controls command execution
- Since DYLD injection and SHELL override both fail (see #2), commands run directly under the agent sandbox, not through per-command sandboxes — so the supervisor never sees them
log streamis asynchronous; denial events can arrive late or not at all- Requires a real TTY; doesn't work in piped/CI environments
Every approach hits the same wall: Seatbelt is static and Endpoint Security is the only supported API for dynamic, per-operation authorization on macOS. Without it, we're left with:
- A static sandbox that can't be expanded after apply (Seatbelt)
- Extension tokens that don't survive exec
- DYLD injection that's stripped by hardened runtime
- Shell interposition that depends on the target program's internal shell detection
The Endpoint Security framework solves all of these by intercepting operations at the kernel level, making allow/deny decisions in real-time from a separate supervisor process, with no requirements on the monitored process.