Soba Docs

Concepts

Security model

One invariant: a run gets the overlap between what the machine's owner allows and what your app asked for, decided on the machine, so it holds even if the control plane is hostile.

resolved permissions  =  machine grant  ∩  app request

An app can only ever ask for less than the machine already allows. It cannot escape a root, widen a tool allowlist, raise a timeout, or upgrade a permission mode.

Why it lives on the machine#

Soba is the component this architecture asks you to trust least. It is a server somebody else operates, and the whole proposition is that you can point your machine at one. If the check lived there, the guarantee would be "this holds unless the control plane is compromised", which is not a guarantee.

Deciding it on the machine makes it hold even when the control plane is hostile. A request that asks for more than the grant allows is refused before anything spawns.

The default grant is nearly nothing#

With no ~/.soba/policy.json, a machine grants read-only tools inside ~/.soba/workspace and nothing else. Not $HOME, not the launch directory. See The machine grant.

The same shape, applied six times#

The intersection is not one check. The same "narrow only" rule governs every dimension a run can vary along:

Dimension Machine says The app asks Result
Tools allowTools / askTools / denyTools the tools this run wants intersected; the machine's denylist is always unioned in
Directory roots, defaultCwd a working directory refused unless it resolves inside a granted root
Time maxTimeoutMs a timeout clamped down
Permission mode permissionMode a permission mode the less permissive of the two
Cost class what the machine can serve route.allow intersected, and enforced on both sides
Network the machine's own reachability; there is no grant field allowNetwork a restriction only: it narrows what a run may reach and can never grant access the machine did not already have

Anything unrecognised narrows#

A machine and a control plane will not always be running the same version, so the rule is that anything unrecognised makes the resolved permissions smaller, never larger. The case that matters most is the cost class: a ceiling an app sent stays a ceiling even when the machine does not recognise what is in it, rather than degrading into "anything goes" and spending money an app explicitly said it would not.

Sensitive tools are never enabled by a request alone#

bash, write, edit, notebookedit

These are never turned on by a request from your app, on any machine. A machine owner can still grant them explicitly in their local policy. Tool names are compared case-folded throughout, so the lowercase list above covers the Bash and Write a policy file actually writes. The point is that a remote party cannot reach them by asking.

Containment is layered, not a flag#

For an agent runtime the worker does not simply pass a list of permitted tools. It also keeps ungranted built-ins out of the model's view entirely, confines file tools to the working directory, refuses any attempt to bypass the permission layer, and ignores the machine owner's own user, project and local settings.

That last clause is load-bearing. A run arriving over the network must not inherit the machine owner's CLAUDE.md, hooks, skills or plugins, or a remote prompt could silently trigger local configuration.

Widening is asked for, never taken#

There is exactly one request that can make a machine more permissive, and it cannot complete without a person at that machine:

a broker may ask to widen; only a person at the machine may say yes.

An app may send a folder request. It comes in two shapes and neither reads the disk: choose one carries no path at all and the machine answers by opening the system's own folder chooser, so the only path that ever leaves is the one somebody clicked; this one names a folder a person typed and the machine asks about it by name. On a literal yes the worker adds the root to ~/.soba/policy.json and makes Write, Edit and Bash askable there; on anything else it writes nothing.

Nothing lists a directory. An earlier version of this let an app ask for the names of the directories inside one, so a dashboard could draw a picker. It is gone: a paired app enumerating somebody's home directory to fill in a form is a read of their disk that nobody agreed to.

approver: "remote" — the channel that asks the app — can never answer one. An app approving its own request for more permission is the escalation this model exists to prevent, so a machine set to remote refuses folder requests and says so. "grants": "off" refuses them whatever the approver is.

What a yes buys is that writing may be asked about. It never becomes automatic: every Write, Edit and Bash still stops for its own approval.

There is no bypass mode#

There is deliberately no "skip all permission checks" mode, anywhere. It is not a setting an app can send, and it is not a flag a support conversation can talk someone into. A thing that cannot be expressed cannot be asked for by mistake.

What is still trusted#

Honesty about the boundary:

  • With an agent runtime, the resolved policy is advisory: the CLI is given the permitted tool set and is trusted to honour it. With a model runtime the gate is in-process and nothing has to be trusted. See Agents and models.
  • The machine's owner is trusted about their own machine. Their local configuration can declare a cost class because the only money reachable from that file is theirs.
  • Soba is trusted to meter and bill honestly. Nothing here prevents a billing dispute; it prevents a control plane escalating on a machine.
© 2026 Soba resolved = machine grant ∩ broker request