Fenzi

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.

File: fenzi.yamlSchema version 1

Where the policy comes from

PlanPolicy used
FreeThe built-in developer-default, plus fenzi.yaml in the project root if there is one.
TeamThe 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.

developer-default
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: person

The 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.

fenzi.yaml
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.

Wall: paths need a new sandbox

Changing paths or workspace takes effect the next time a sandbox is created. Changing hosts can apply to a running sandbox.

Fields

FieldValuesNotes
version1Required.
nameTextShown in the log and on Team. Only in an org document; a project fenzi.yaml that sets it is refused.
agentsAgent namesWhich agents may run under this policy. Only in an org document; a project fenzi.yaml that sets it is refused. See Agents.
workspaceread-write, read-onlyAccess to the project copy. Default read-write.
paths.read_onlyAbsolute pathsExtra paths inside the sandbox the agent may read. Paths under a home directory are rejected.
paths.read_writeAbsolute pathsExtra writable paths. / is rejected.
hosts[].hostExact host nameWildcards are not accepted.
hosts[].portNumberDefault 443.
hosts[].accessread-only, read-writeSee Host access.
hosts[].allowMETHOD /pathRequired with read-write. Each line is one allowed request shape.
approvalspersonThe 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/node are requested with an encoded slash in the path. The read-only level already allows it. npm audit sends a POST, which read-only blocks, so agent images turn audit off. Run npm audit outside the sandbox.
  • pip. Packages download from files.pythonhosted.org, not only pypi.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 compile
Compiled OpenShell policy, excerpt
version: 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 catalog

A few choices are worth knowing:

  • hard_requirement means 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 pip or npm, 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
Example output, failing
! 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/hooks

To 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.

ResultMeaningStarts?
within_boundaryNothing the policy allows is outside the boundary.Yes
exceeds_boundaryThe prover found an action outside it, and shows one.No
unsupportedThe policy has a rule type the prover does not model.No
inconclusiveThe prover could not decide.No
errorA 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.