~/.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:
- The policy your app requests on a run.
- 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 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.