Policy and confinement
Each paired app has a capability ceiling OpenCompanion enforces on every run - a permission mode and a network setting it can only clamp down, never raise - plus the work folder its dispatched runs are pinned to and the folders you grant a terminal session.
Each paired app has a capability ceiling that OpenCompanion enforces on every run it dispatches. The ceiling has two fields:
- Permission mode -
read-only,auto-edit, orfull. Caps what a run may do to files. - Network -
onoroff. Caps whether a run may reach the network.
The ceiling is a maximum, not a target. An app can request a run at or below the ceiling; ask for more and OpenCompanion clamps the run down to the ceiling. An app can never raise it.
Running in local mode? Pass --local in place of --url on any policy command below to manage the single local scope; everything else works the same.
Show the whole policy
opencompanion policy show
opencompanion policy show --url https://your-app.example/apiPer app, this prints everything that decides what its runs may do on this machine:
| Line | What it says |
|---|---|
permission ceiling | read-only, auto-edit, or full - the most a run may do to files. |
network | on or off - whether a run may reach the network. |
scheduled runs | allowed or refused - whether this machine runs the app's schedules. |
app-dispatched runs | allowed or refused - whether this machine runs work the app's own code dispatches. |
chat runs | Always allowed. A run you start by typing is never refused here. |
work root | The folder this app's dispatched runs are pinned to. |
granted folders | The folders of YOUR own a terminal session may run in (none by default). |
Under those lines it prints the invariants it can hold you to: ceilings only clamp down, dispatched runs stay in the work root shown, a terminal session starts there or in a folder you granted, app-pushed MCP servers are dropped (the only local ones are yours, from mcp add), and all of it is enforced by the daemon on your machine rather than trusted to any app. A terminal is your own CLI, so the footer is equally plain about what its folder is not - see the warning below. The last thing it prints is the honest limit on the two origin denies, so you never read them as more than they are.
Change the ceiling
opencompanion policy set --url https://your-app.example/api --permission-mode read-only --network offPass at least one of --permission-mode or --network; an omitted field keeps its current value. The change is recorded to the local audit log as a policy-change event, and a running daemon picks up the new ceiling on its next run with no restart. To lock an app down to reads with no network, set --permission-mode read-only --network off.
Refuse work you did not start
The ceiling caps what a run may do; these two switches decide whether a run happens at all, by where it came from:
opencompanion policy set --url https://your-app.example/api --schedule deny --dispatch deny--schedule allow|deny- the app's scheduled runs.--dispatch allow|deny- runs the app's own product code dispatches to your machine.
Both default to allow and are recorded to the audit log like any other policy change. Chat is never deniable: a run you start by typing is you, asking.
This deny defends against an honest app. An app that simply omits the origin on a dispatched run is indistinguishable, on the wire, from a chat turn - so treat these as a preference the app respects, not a wall. The real gate for app-dispatched work is the per-device grant in the app itself, which is off by default.
Work-folder confinement
Every app's dispatched runs are pinned to a single work/<backendKey>/<productId>/ folder under OpenCompanion's per-user data directory. That folder is the only part of your machine such a run may WRITE to: an app names no path (it cannot ask for one), and any MCP server it tries to push into a run is dropped. opencompanion policy show prints the exact work root for each app so you can see precisely where its runs are confined.
Confinement and MCP-stripping are enforced by the daemon on your machine, not trusted to the app. The only MCP servers a run ever gets besides the app's own tools are the ones you added locally with mcp add.
The work folder bounds what a run CHANGES, not what it can SEE. Confinement is the folder a run is driven in plus the CLI's own sandbox - not a wall around your machine. Under auto-edit (the default ceiling) a dispatched run's WRITES are held to its work folder, but the CLI is started with READ access to the rest of your filesystem, just as if you had run it there yourself - so the rest of your home directory is readable, not off-limits. Under read-only it writes nowhere; reads are still not confined. Under full neither axis is confined. Pair a companion only with an app you would let read your disk.
The one read that IS denied: OpenCompanion's own secrets. A dispatched run is unattended and prompt-injectable, so the daemon denies it read access to its own secrets/ folder - the master key and your encrypted device credential - even though the rest of your disk stays readable. The deny is applied through each CLI's own read-deny mechanism and is OS-enforced on macOS (seatbelt) and on Linux with bubblewrap installed. On a Linux host without bubblewrap it falls back to the CLI's tool-permission rules: those still stop the file-read tools from reaching the folder, but they are not OS-enforced, so a shell command whose read the static recognizer does not catch (an obfuscated path, or a reader like python or dd it does not classify) is not blocked there. A terminal session is your own CLI and is never confined this way.
Granted folders (terminal sessions)
A terminal session (opencompanion terminal) exists to work on your own project, so it is the one thing that may leave the work folder - and only into a folder you granted, by name, on this machine:
opencompanion policy grant-folder add ~/code/my-project --url https://your-app.example/api
opencompanion policy grant-folder list
opencompanion policy grant-folder remove ~/code/my-project --url https://your-app.example/apiopencompanion terminal --cwd ~/code/my-project then opens there; a --cwd anywhere else is refused. The check resolves symlinks and is segment-relative, so .., a link out of a granted folder, and a sibling that merely starts with the same name are all refused. An app can never add, name, or widen a grant: a terminal reports no path to any app, and the session an app composes cannot carry one. Each grant and revoke is recorded to the audit log as a policy-change event.
A folder is where a terminal STARTS, not a sandbox around it. A terminal session is your own CLI, running on your own machine with your own terminal attached - what keeps it in that folder is the CLI's own approval prompts, and the ceiling decides whether those stay on. Under full the CLI is started with its prompts bypassed, so nothing then holds it there. Under auto-edit or read-only OpenCompanion withholds that bypass and the CLI keeps asking you before it acts - note that read-only withholds the bypass flag, it does not hand the CLI a read-only mode. A dispatched run is different: it is driven headlessly and stays confined regardless.
A network: off ceiling refuses a terminal session outright: the interactive CLIs have no flag that blocks egress, and a policy that promises the network is off must not quietly open an online terminal.
Enforcement caveat (Hermes). Hermes runs its own tools under its own hooks, so the read-only ceiling and network-off posture are not enforced for a Hermes run. Where enforcement matters - unattended read-only or air-gapped work - prefer Claude Code or Codex (Codex is the one CLI that OS-enforces network-off).
Terminal sessions
Open your own coding CLI already wired to an app you paired - the app's tools on its MCP surface, the app's instructions in its context - plus your own local MCP servers, in the work folder or a folder you grant.
The audit log
OpenCompanion keeps a local, append-only log of everything it does. Every run is written before it executes and fails closed if it cannot be, so no app can cause a run that leaves no trace.