The Endpoint Security framework is Apple's supported API for intercepting system operations in real-time. It replaced the deprecated kernel extensions (kexts) starting in macOS 10.15.
A privileged ES client process (the supervisor) subscribes to event types. When a monitored process performs an operation, the kernel holds the syscall and delivers an AUTH event to the ES client. The client inspects the event and responds with ALLOW or DENY. Only then does the kernel proceed.
Child process (sandboxed) Kernel ES Client (nono supervisor)
│ │ │
├── open("/home/.aws/config") ──►│ │
│ ├── ES_EVENT_TYPE_AUTH_OPEN ──►│
│ │ (blocked in kernel) │
│ │ ├── Check policy
│ │ ├── Prompt user?
│ │◄── es_respond_auth_result ───┤
│◄── ALLOW or EPERM ─────────────│ │
| Event | What it intercepts |
|---|---|
ES_EVENT_TYPE_AUTH_OPEN |
File open — can inspect path AND downgrade flags (e.g., allow read-only but deny write) via es_respond_flags_result |
ES_EVENT_TYPE_AUTH_EXEC |
Process execution — can block specific binaries |
ES_EVENT_TYPE_AUTH_CREATE |
File/directory creation |
ES_EVENT_TYPE_AUTH_UNLINK |
File deletion |
ES_EVENT_TYPE_AUTH_RENAME |
File rename/move |
ES_EVENT_TYPE_AUTH_LINK |
Hard link creation |
ES_EVENT_TYPE_AUTH_CLONE |
File cloning (copyfile) |
1. No exec() limitation. The ES client runs as a separate process. The monitored child can fork + exec freely — the kernel delivers events based on the process being monitored, regardless of how many times it execs. This completely sidesteps the extension token problem.
2. No DYLD hacks. The interception happens in the kernel, not in userspace. Hardened runtime, SIP, code signing — none of it matters. The child process doesn't need any special setup, env vars, or libraries.
3. Per-operation granularity. Instead of declaring all permissions upfront in a static Seatbelt profile, the supervisor makes allow/deny decisions on each open() call. This enables:
- Interactive prompting ("Claude wants to read ~/.aws/config — allow?")
- Dynamic permission accumulation (approve once, allow for the session)
- Fine-grained flags —
AUTH_OPENcan downgrade read-write to read-only
4. Works with unmodified programs. cat, git, ssh, Claude Code — anything. The child doesn't need to integrate an SDK or be aware it's monitored.
5. Survives the full process tree. You can monitor a process and all its descendants. When Claude Code spawns bash, which spawns git, which spawns ssh — all of them are covered.
The com.apple.developer.endpoint-security.client entitlement is restricted:
- Requires an Apple Developer account
- Must be requested via Apple's System Extensions request form
- App must be structured as an Application Bundle with a provisioning profile
- Apple can grant development access but deny distribution access
- The app also needs user approval via System Settings → Privacy & Security → Full Disk Access (or the ES-specific prompt)
This is the same entitlement used by security products like Santa (Google), LuLu, and commercial EDR tools.
nono (ES client, entitled, runs as supervisor)
│
├── es_new_client() — subscribe to AUTH_OPEN, AUTH_EXEC, etc.
├── es_mute_process() — mute self + system processes for performance
│
└── fork + exec child (Claude Code, no sandbox_init needed)
│
└── Every file open/exec from the child tree
→ kernel delivers AUTH event to nono
→ nono checks policy (deny_credentials, never_grant, etc.)
→ nono prompts user if needed
→ nono responds ALLOW or DENY
No Seatbelt profiles. No DYLD interposition. No extension tokens. No shell naming hacks. Just a clean kernel-level interception with real-time policy decisions.
AUTH events have a deadline (~30-60 seconds). If the ES client doesn't respond in time, the system kills the client. For interactive prompts, nono would need to either respond quickly or use a strategy like deny-then-retry: deny the operation immediately, prompt the user in the background, and when approved, the child's next attempt succeeds.