Use
Policy file
A short file says what the agent may touch. Fenzi compiles it into the sandbox's own policy format and checks the result before anything starts.
Where the policy comes from
| Plan | Policy used |
|---|---|
| Free | The built-in developer-default, plus fenzi.yaml in the project root if there is one. |
| Team | The policy your security lead published, delivered as a signed file. A project file is not used. See Publish and revisions. |
You do not need to learn the sandbox's policy language. You also cannot write something in this file that the sandbox cannot enforce. Fields that would need that are left out.
The default policy
This is developer-default. The project folder is writable. Three package registries and the GitHub API are readable. Everything else is closed. The full picture, including what each agent adds, is on Default access.
version: 1
name: developer-default
agents: [claude, codex, grok, opencode]
workspace: read-write
hosts:
- host: api.github.com
access: read-only
- host: registry.npmjs.org
access: read-only
- host: pypi.org
access: read-only
- host: files.pythonhosted.org
access: read-only
approvals: personThe model API is not listed. Each agent's key comes with the endpoints it is allowed to reach, so the key and its destination stay together. See Keys.
Adding hosts for a project
Run fenzi init to create a fenzi.yaml in the project root, then add what this project needs.
version: 1
hosts:
- host: api.stripe.com
access: read-only
- host: api.github.com
access: read-write
allow:
- GET /repos/acme/web/**
- POST /repos/acme/web/pulls
paths:
read_only: [/opt/fixtures]Hosts merge with the default. A host listed in both uses the access level from the project file. Commit the file so everyone on the project runs with the same rules.
The agent can edit this file inside the sandbox, like any other file in the project. Nothing flags that edit: the diff shows it like any other file, and apply writes it like any other file. Read it there before you apply, because on Free the next run in that folder uses it. See Apply.
Changing paths or workspace takes effect the next time a sandbox is created. Changing hosts can apply to a running sandbox.
Fields
| Field | Values | Notes |
|---|---|---|
version | 1 | Required. |
name | Text | Shown in the log and on Team. Only in an org document; a project fenzi.yaml that sets it is refused. |
agents | Agent names | Which agents may run under this policy. Only in an org document; a project fenzi.yaml that sets it is refused. See Agents. |
workspace | read-write, read-only | Access to the project copy. Default read-write. |
paths.read_only | Absolute paths | Extra paths inside the sandbox the agent may read. Paths under a home directory are rejected. |
paths.read_write | Absolute paths | Extra writable paths. / is rejected. |
hosts[].host | Exact host name | Wildcards are not accepted. |
hosts[].port | Number | Default 443. |
hosts[].access | read-only, read-write | See Host access. |
hosts[].allow | METHOD /path | Required with read-write. Each line is one allowed request shape. |
approvals | person | The only value. There is no setting for automatic approval. |
Host access
read-only allows GET, HEAD, and OPTIONS, and blocks everything else. It is a rule about HTTP methods. It does not promise that a server's GET has no side effects.
read-write allows only the requests you list under allow. In a path, * matches within one segment and ** matches across segments. So /repos/acme/web/** covers everything under the repository, but not /repos/acme/web itself.
The file has no level that allows every method on a host. If you need more than reads, list the requests. The one way a host gets every method is an approved request that names no method or path, and that lasts only for the sandbox that asked. See Approving on Free.
Every rule is enforced. The sandbox's own default for request rules is to log violations and let them through. Fenzi always sets it to block.
Package registries
- npm. Scoped packages such as
@types/nodeare requested with an encoded slash in the path. Theread-onlylevel already allows it.npm auditsends aPOST, whichread-onlyblocks, so agent images turn audit off. Runnpm auditoutside the sandbox. - pip. Packages download from
files.pythonhosted.org, not onlypypi.org. The default lists both.
What it compiles to
See the compiled policy without starting anything. On a machine signed in to a team, it shows the team's signed revision, the same one fenzi run uses.
fenzi policy compileversion: 1
filesystem_policy:
include_workdir: false
read_only: [/bin, /usr, /lib, /etc, /app, /var/log, /proc, /dev/urandom]
read_write: [/sandbox, /tmp, /dev/null] # /sandbox is the working folder
landlock:
compatibility: hard_requirement
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies:
pypi_org:
name: pypi.org
endpoints:
- host: pypi.org
port: 443
protocol: rest
enforcement: enforce
access: read-only
binaries:
- path: <agent binary> # real path, from the agent catalogA few choices are worth knowing:
hard_requirementmeans the sandbox does not start if the kernel cannot enforce the filesystem rules. The upstream default would start it anyway and log a warning.- The working folder is listed by path instead of with
include_workdir, so the check below can reason about it. - Each host gets its own rule, and the rule names the real path of the agent. Programs the agent starts, such as
pipornpm, use the agent's rules. - The user setting applies on Docker and Podman. Under the MicroVM driver, the sandbox runs as the user the driver configures.
The check before start
Before a sandbox starts, Fenzi runs OpenShell's policy prover. It compares the compiled policy, including access that agent keys add, against a boundary. If any action is possible that the boundary does not allow, the prover names it and nothing starts.
fenzi policy check! Policy check exceeds_boundary
Checked developer-default (built in) + fenzi.yaml for Claude Code.
Example claude can POST to api.github.com:443 /repos/acme/infra/hooksTo fix it, narrow the allow lines for that host, or ask whoever owns the boundary to include it. The command exits with code 2 until the result is within_boundary.
| Result | Meaning | Starts? |
|---|---|---|
within_boundary | Nothing the policy allows is outside the boundary. | Yes |
exceeds_boundary | The prover found an action outside it, and shows one. | No |
unsupported | The policy has a rule type the prover does not model. | No |
inconclusive | The prover could not decide. | No |
error | A file could not be read or parsed. | No |
On Free, the boundary is compiled from the same file. The check then catches access that keys and their profiles add on top of what you wrote. So on Free a pass proves the sandbox gets what the file says, not that the file is safe: a host you list is inside the boundary because you listed it. On Team, an admin can save a boundary for the whole team, and a pass then means the policy stays inside it. See Team boundary.
After the sandbox is created, Fenzi runs the check a second time on the policy the sandbox actually holds. If that fails, the sandbox is deleted before the agent starts.
Adding a host from a request
On Team, an approval in the console is a one-off by default: it opens the host for the sandbox that asked, and the team policy does not change. Other sandboxes do not get the host.
An admin can choose Approve and add to policy instead. The sandbox that asked gets the same approval, and the Policy page opens with a draft of the next revision that adds the host, at read-only or read-write with the request's method and path. Nothing changes for other sandboxes until the draft is published. Then every running sandbox of the team loads the host at its machine's next forwarder pass, because a host is a network change, and new sandboxes start with it. See Approve, or add to the policy.
Outside this file
The prover returns unsupported for GraphQL, MCP, WebSocket, and JSON-RPC rules. Raw TCP and TLS passthrough cannot be checked at the request level. The policy file has no fields for them.
An agent cannot use GitHub's GraphQL API, an MCP server over the network, or a database connection inside the sandbox through this file. Outbound MCP on the sandbox's network path is still inspected. A rule for the MCP payload is not.
The file takes host names, never IP addresses.