Soba Docs

Connect a machine

The machine grant

~/.soba/policy.json, the file where the machine's owner says what any run may do. With no file, a machine grants read-only tools in one scratch directory and nothing else.

There are two policies in Soba and they are not the same object:

  1. The policy your app requests on a run.
  2. The policy the machine's owner grants: this file.

The worker resolves them by intersection and the local grant always wins. See Security model.

The default, with no file at all#

JSON
{
  "roots": ["~/.soba/workspace"],
  "defaultCwd": "~/.soba/workspace",
  "allowTools": ["Read", "Glob", "Grep", "WebSearch", "WebFetch"],
  "askTools": [],
  "denyTools": [],
  "maxConcurrency": 2,
  "maxTimeoutMs": 600000,
  "permissionMode": "deny",
  "allowRuntimes": [],
  "allowAppTools": true,
  "allowMeteredKeys": false,
  "approver": "local",
  "grants": "ask"
}

Read-only tools inside ~/.soba/workspace. Not $HOME, not the launch directory. Nothing is askable, so the approval path is inert until the owner opts into it. A machine becomes more useful only when its owner says so, in a file they control.

A malformed policy file is a hard failure

It is not a silent fallback to the defaults. Quietly ignoring a policy someone wrote is the kind of bug that ends up looking like a breach.

A realistic grant#

JSON
{
  "roots": ["~/Projects"],
  "allowTools": ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Edit"],
  "denyTools": ["Bash"],
  "maxConcurrency": 2,
  "askTools": ["Write"],
  "maxTimeoutMs": 600000,
  "permissionMode": "ask",
  "approver": "local",
  "allowAppTools": true,
  "allowMeteredKeys": false
}

Every field#

Field Meaning
roots Directories a run may execute inside. Everything else is refused. ~/ is expanded
defaultCwd Where a run lands when none is requested. Must be inside roots
allowTools The maximum tool set any run may use with no questions asked
askTools Tools that may be attempted, each gated by a live yes/no. This is the approval ceiling
denyTools Never permitted, whatever is requested. Always unioned in
maxConcurrency How many runs this machine will take at once
maxTimeoutMs Hard wall-clock cap. A requested timeout is clamped down to it
permissionMode The most permissive mode this machine will operate in: ask, deny or auto
allowRuntimes Runtime ids this machine will run. Empty means all detected
allowAppTools Whether app-defined tools may be tunnelled through this machine
allowMeteredKeys Whether a metered provider key in this machine's environment may be spent
approver local, remote, desktop or none; see Approvals
grants Whether an app may ask a person AT this machine for a folder: ask or off. The only request that can widen this file, and only a local yes can complete it

allowTools versus askTools#

allowTools runs with no questions asked. askTools is the approval ceiling: tools the model may attempt, each one gated by a live yes/no.

A yes can only ever select from that set. So even an approver that says yes to everything, including a compromised one, cannot widen the machine past what its owner pre-authorised. That property is what makes "approver": "remote" safe to offer at all.

allowMeteredKeys#

Default false, and the default is the interesting part.

The worker inherits the owner's shell, and that shell may hold ANTHROPIC_API_KEY. Passing it through means a run advertised as user-subscription quietly bills their API account while Soba, reading a free cost class, charges nothing. Both sides lose and neither is told.

So a runtime whose key is set is withheld unless the owner sets this flag — advertised to nobody, and reported to the owner naming the variable. Setting the flag serves those runtimes instead, re-classified as frontier, so the meter and the wallet agree again.

The key itself is never removed from the environment. Which of their own credentials a person's CLI may use is not ours to decide, and Anthropic's terms for running Claude Code inside another product say so in as many words.

Grant-only, by construction

There is no way for an app to ask for allowMeteredKeys. Nothing off this machine may decide to spend the owner's money.

grants#

Default "ask". This is the one field here that decides whether the machine may be asked to become more permissive — and the wording is the whole design:

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

An app can send a folder request. The worker does not act on it: it puts the question to whoever is standing at that machine, and on a literal yes it runs exactly what soba-worker allow <dir> --write runs, into exactly this file.

A request comes in one of two shapes, and neither of them reads the disk:

Shape What the machine does
Choose one (no path in the request) The Soba app opens the system's own folder chooser. The only path that leaves the machine is the one somebody clicked
This one (a path somebody typed) The app, or the terminal the worker was started in, asks about that folder by name
Value
"ask" A request becomes a question for a person at this machine. The default
"off" Refused, without anybody being asked

Three things it cannot do, whatever a broker sends:

  • approver: "remote" can never answer one. That channel asks the app, and an app approving its own request for more permission is the escalation this whole model exists to prevent. A machine set to remote refuses folder requests outright and says so.
  • It cannot make writing automatic. A yes adds the root and makes Write, Edit and Bash askable there. Every one of them still stops for its own yes.
  • It cannot ask about a folder that is not there. A directory that does not exist is refused before a dialog is drawn, so a request cannot be used to probe what is on the disk. Nothing anywhere lists a directory: an earlier version answered "what folders are in here" so a dashboard could draw them, and it was removed — enumerating somebody's folders to fill in a form is a read of their disk that nobody asked them about.

Every decision, yes or no, is appended to ~/.soba/audit.log.

Nothing has to be restarted

A grant applies the moment it is written — by this request, by soba-worker allow, or by the folder picker in the app. The worker watches the file.

Relocating the file#

SOBA_POLICY_FILE points the worker at a different grant; SOBA_HOME relocates every piece of worker state at once. Both are useful in containers. See Files and environment.

© 2026 Soba resolved = machine grant ∩ broker request