Security

Capability-Based Permissions: A Safer Model Than Full Disk Access, Yes or No

Akshay Sarode 7 min read
Direct answer

A capability token is a scoped grant, one of workspace_rw, network, subprocess, screen_capture, accessibility, or full_disk_access, issued to a single agent for a single run and enforced by the OS sandbox itself. That's a different shape of permission than macOS's Full Disk Access prompt, which is one all-or-nothing switch per app, checked once and then forgotten. Bernato issues capability tokens per agent, enforces them at the sandbox level (macOS Seatbelt today), and logs every grant, so you can see exactly what an agent could touch instead of just whether you clicked yes at some point in the past.

You paste a command into a terminal, an AI coding agent starts working through your repo, and macOS pops up a dialog: "Terminal would like to access files in your Documents folder." You click Allow, because you always click Allow, because the alternative is stopping mid-task to figure out which of the forty files the agent is touching actually needs that folder. A few weeks later you've forgotten you granted it. The agent still has it. So does every other script that ever runs in that same terminal session, forever, until you go dig through System Settings and revoke it by hand.

That's the trap of a binary permission model: yes or no, once, for everything. It's not a security control so much as a formality you click through on the way to getting work done. Capability tokens are a different idea entirely, and the difference matters more now that the thing asking for access is an autonomous agent that can act faster than you can read its output.

The "yes or no" trap

macOS tracks app permissions in a system called TCC, short for Transparency, Consent, and Control. Every grant, Full Disk Access, Screen Recording, Accessibility, Camera, and so on, lives as a row keyed to an app's bundle identifier. The model is coarse by design: an app either has Full Disk Access or it doesn't. There's no "read-only," no "just this folder," no expiration. Once granted, the permission covers every file operation that app or its child processes will ever perform, across every project, forever, until a human finds the toggle and turns it off.

Most AI coding agent setups inherit this same shape, sometimes with a flag rather than a system dialog. A "yolo mode" or "skip permissions" switch turns off per-action prompting entirely, which is the same yes-or-no decision as clicking Allow on a TCC dialog, just made once at launch instead of once per app. Either way, the person granting access is being asked to accept "everything, forever" or stop and manually gate every single file read, network call, and subprocess the agent wants to make. Nobody does the second thing for long. Coarse permission systems don't prevent overreach, they just move the overreach earlier, to the one moment someone was willing to click through it.

What a capability token actually grants

Bernato issues capability tokens per agent, per run. Instead of one switch covering everything an agent might ever do, there are six, and you grant only the ones the job in front of you actually needs. They're enforced at the OS sandbox level, macOS Seatbelt today, which means the check happens in the kernel when the agent tries to do something, not in application code that the agent itself could route around.

CapabilityScopeTypical job that needs it
workspace_rwRead and write inside this agent's project folder onlyEditing code, running a formatter, writing test output
networkOutbound network requestsInstalling dependencies, calling an API during development
subprocessSpawning child processesRunning the test suite, invoking a build script
screen_captureReading pixels off the screenA visual regression agent comparing rendered output
accessibilityDriving other apps through the accessibility APIAn agent that operates a GUI app on your behalf
full_disk_accessRead and write anywhere on diskRare: an agent that genuinely needs something outside the workspace, like a shared config in your home directory

Notice that full_disk_access is still on the list. Capability tokens aren't about pretending broad access never has a legitimate use, some agents really do need to read a file outside their project. The point is that granting it becomes a deliberate, visible, per-agent decision instead of an ambient default an agent inherits just because it happened to be launched from a terminal that had it. Most agent runs, in my experience, only ever need workspace_rw, sometimes with network or subprocess added for a specific task. The other three stay off unless something concrete calls for them.

Full disk access vs. workspace_rw, drawn to scale

The gap between these two isn't a nuance, it's an order of magnitude. Full disk access covers your SSH keys, browser profiles, other users' home directories, every app's private data, and the project you're actually working on. Workspace_rw covers the project. That's it.

full_disk_access everything on the machine is in scope ~/.ssh ~/Documents ~/Photos other users' data browser profiles /Library ~/project workspace_rw only this folder is in scope
full_disk_access covers everything inside the dashed outline. workspace_rw scopes the same agent down to one project folder, the rest of the disk stays out of reach.

Try it: build a grant

Say you're kicking off an agent to fix a failing test. It needs to read and write files inside the repo, and it needs to run the test command itself. It has no reason to see your screen, drive another app, or touch anything outside the repo. Toggle the switches below and watch the risk summary change as the grant gets broader or narrower.

Try it: build a grant
workspace_rw
Read and write inside this agent's project folder
network
Make outbound network requests
subprocess
Spawn child processes, e.g. run the test suite
screen_capture
Read pixels off the screen
accessibility
Drive other apps through the accessibility API
full_disk_access
Read and write anywhere on disk
Risk summary

This agent can currently read and write inside its own project workspace folder.

The token configures the sandbox, it doesn't replace it

It's worth being precise about what a capability token is and isn't. The token is policy: a list of what this particular agent, for this particular run, is allowed to do. The sandbox, Seatbelt on macOS, is the enforcement mechanism, the same kernel-level confinement Apple uses for its own App Sandbox on the Mac App Store. The token doesn't turn the sandbox off or on. It configures what the sandbox will permit for that one process.

That distinction matters because it means the token can't be your only line of defense even if you wanted it to be. An agent running with all six capabilities granted is still a normal, unprivileged user process. It's not root, it doesn't get to touch other users' processes, and it doesn't get to disable the sandbox itself. The token widens or narrows the door inside a boundary that's already there. On the CLI side, the same idea shows up in how a run gets built: sandboxed exec confines the process, and a separate verification harness produces signed provenance for what actually happened, so the record of a run doesn't depend on the agent's own account of itself.

What happens when you revoke a capability mid-run

Because enforcement lives in the sandbox rather than in the agent's own code, revocation doesn't depend on the agent agreeing to stop. Kernel-level sandboxes like Seatbelt check restricted operations at the syscall boundary, every time a process asks to open a file, make a connection, or spawn a child, the kernel consults the current policy before allowing it through. Revoke a capability, and the next restricted syscall the agent attempts gets denied at that boundary. An operation already past that check, a write() call the kernel has already accepted, for instance, isn't retroactively unwound. But the agent doesn't get to keep going as if nothing changed. Its very next attempt to use the revoked capability comes back as a permission error, the same kind of error any process gets when it tries something the sandbox doesn't allow, and the agent's code has to handle that like any other IO failure.

Practically, this is the difference between revocation as a suggestion and revocation as a wall. If you're watching an agent's live PTY from the browser or your phone (part of what the runtime dashboard is for) and you see it about to do something you didn't intend to allow, pulling the network or subprocess capability mid-run stops the next attempt cold. It's not asking the agent nicely to wrap up.

Every grant is a row in a log

A TCC.db entry tells you the current state: this app has Full Disk Access, or it doesn't. It doesn't tell you when that grant was made, for which task, or whether it was ever actually used. Bernato keeps a local append-only activity log of what each agent run was granted and did, and the design target is a hash-chained audit log in Firestore with an hourly verifier, plus a small open-source verifier library (Apache-2.0, under 200 lines) so the chain can be checked independently rather than taken on trust. The point of a hash chain is that a grant, once logged, can't be quietly edited out later without breaking the chain in a way the verifier would catch.

Capability tokens aren't the only signal in that log. Agents also declare advisory intents at spawn time, filesystem, network, subprocess, mail, memory, spend, mobile, which describe what the agent expects to need. Intents aren't enforced the way capabilities are; an agent can declare a network intent and still get denied if it wasn't granted the network capability. But the gap between declared intent and granted capability is itself useful signal: an agent that says it needs mail but was only ever granted workspace_rw is either misconfigured or asking for something it shouldn't. The same one-time-token idea, rather than a standing credential, shows up elsewhere in the product too. Pairing a new machine into your fleet uses a one-time capability token issued from the tray, not an SSH key sitting in ~/.ssh indefinitely. Bernato's audit events are also designed to eventually flow into Ujex, its sibling governance and audit product, for teams that want a single place to review activity across more than just agent runs.

FAQ

How does this differ from macOS's own permission system (TCC)?

TCC grants are per-app and mostly binary: an app either has Full Disk Access, Screen Recording, or Accessibility, or it doesn't, and the grant applies to every file, project, and task that app ever touches until someone manually revokes it in System Settings. Capability tokens are per-agent and per-run: a token grants exactly the capabilities that run needs (often just workspace_rw) and expires with the run rather than persisting as a standing grant. Bernato's tokens are enforced at the same OS sandbox layer TCC and Seatbelt already use, they're a finer-grained policy on top of that layer, not a replacement for it.

What happens to in-flight file operations when a capability is revoked mid-run?

Operations the kernel has already accepted (a write() call already past its permission check) complete normally, they aren't unwound retroactively. The next restricted operation the agent attempts after revocation is denied at the syscall boundary, the same way any sandboxed process gets denied when it tries something outside its policy. The agent gets a permission error back and has to handle it like any other IO failure. It cannot simply continue as if the capability were still granted.

Do capability tokens slow an agent down?

Not meaningfully. The enforcement happens at the same kernel-level syscall checks that macOS's Seatbelt sandbox already performs for any sandboxed process, including ordinary App Store apps. Checking a permission table entry at a syscall boundary is not a perceptible cost next to the actual work a coding agent does: reading files, running builds, making network calls. Scoping which capabilities are granted is a policy decision, not a performance one.

Does this work on Linux or Windows too?

The OS-level enforcement described here, macOS Seatbelt, is macOS-specific today. The rest of the runtime, the bernatod daemon, the web dashboard, and the mobile app, works across platforms for supervising and approving agent runs, but the sandbox mechanism that actually enforces a capability token at the kernel level is currently a macOS feature. We're not claiming equivalent kernel-level sandbox enforcement on Linux or Windows at this time.

What's the difference between a capability token and an advisory intent?

A capability is enforced: the sandbox will not let the agent exceed it, full stop. An intent is declared: the agent states at spawn time what it expects to need (filesystem, network, subprocess, mail, memory, spend, mobile) but nothing forces that declaration to be accurate. The two are logged together so a reviewer can spot the gap between what an agent said it would do and what it was actually allowed to do.