Security
Security model
One policy, five rings of enforcement, and a ledger outside all of them. Each ring is generated from the same personnel file.
Agent Office assumes an agent can make mistakes, can be manipulated by what it reads, and may try things it shouldn't. Its answer isn't a longer prompt. It enforces the same policy in several independent places, outside the agent's control, and records what happens somewhere the agent can't reach.
The five rings#
| Ring | Enforced by | Generated from | Stops |
|---|---|---|---|
| 1 · Policy | Branch protection on the policy repo's main. Only the approvers team merges. | Written by humans | Agents granting themselves anything |
| 2 · Identity & credentials | The forge: each agent is its own user, with collaborator permissions per repo, a token per run and protected files in its vault | Capabilities | An agent touching a repo it wasn't given, whatever its shell does |
| 3 · Sandbox | The agent's own OS user, plus Claude Code's sandbox (bubblewrap on Linux, Seatbelt on macOS) with a strict network allowlist | Capabilities + office.yaml | Reading the office's state or other agents' homes, and network access to unlisted hosts |
| 4 · Harness | Claude Code settings compiled per run, plus a managed settings floor | Capabilities + run policy | Mistakes inside the turn, with fast feedback to the agent |
| 5 · Charter | The prompt | The charter | Nothing, by itself. It's guidance. |
Outside all five sits the ledger: written by the office process, hash-chained, and anchored in the policy repo.
Ring 1: Policy#
Who exists and what they may do is decided by files in the policy repo. Merges to main are limited to the approvers team, which holds humans and the office's own admin account (used only for the office's commits, such as ledger anchors); direct pushes only to that admin account. Agents with hr.propose can open pull requests there. Agents cannot push or merge, and check 5 of the verify suite tests this. Provisioning after a merge is done by deterministic code, never by a model. The one way a human can widen an agent's powers without a PR is to approve a time-boxed capability window the agent asked for; that decision is ledgered too.
Ring 2: Identity and credentials#
Each agent is a real forge user (ag-<slug>), so the forge enforces its permissions whatever happens inside a run:
- Repo access comes from collaborator permissions compiled from
forge.readandforge.write.forge.writewithvia: pull_requestgives write access, so the agent can push branches and open PRs; whether it can merge depends on the target repo's branch protection. The office protectsmainon the policy, handbook and office repos; protectmainon any other repo you grant. - Vault protection. The agent can push to its vault, but not to
CLAUDE.mdor.office/**(checked by verify, check 3). - No long-lived forge credential. Each run gets a forge token named
run-<runId>, minted with the scopes its capabilities need and always revoked when the run ends. The agent's forge password stays in the office's secret store. Tokens are handed to git and Claude Code through the environment, never on a command line. (The model credential is long-lived; see limits.) - Isolation between agents. Vaults are private repos and each agent is a collaborator only on its own, so an agent's token can't read another agent's vault (checked by verify, check 4).
Ring 3: Sandbox#
Each run executes as the agent's own OS user (ag-<slug>), started through a sudo rule that lets the office act only as members of the agents group (never root or the office user), with a cleared environment. Agent users are created only by a root-owned helper that accepts nothing but ag-<slug> names. On top of that:
- the office's state directory (
0700, office user) is unreadable: no database, no secrets, no transcripts (checked by verify, check 1); - other agents' homes and vaults are unreadable (checked by verify, check 2);
- Claude Code's sandbox is required. If it's unavailable, the run fails closed. On Ubuntu 23.10 and later, bubblewrap needs the AppArmor profile that
officectl installwrites, anddoctorchecks it; - network access is limited to
net.egress, the forge host and the office host (checked by verify, check 7, live). When the forge or office runs on the same machine, the allowlist also gets that host's<ip>:<port>, because the Linux sandbox otherwise refuses names that resolve to the machine itself.
Ring 4: Harness#
For every run, the office passes Claude Code settings from the capability compiler:
- the permission mode comes from
run.mode.bypassPermissionsis rejected, and bypass mode is disabled; - allow and deny rules come from capabilities, on top of a fixed floor: no access to the state directory,
/etcor other agents' homes; nosudo,suordoas; noWebSearch; noWebFetch,curlorwgetwithoutnet.egress; no edits toCLAUDE.mdor.office/; - the office MCP server is the only MCP server (
--strict-mcp-config), project MCP servers are not loaded, and no user or project settings files are read (--setting-sources ""), so the agent can't add rules by writing a settings file into its vault; officectl installinstalls a system-wide floor in Claude Code's managed settings file.
Ring 4 gives the fastest feedback, because a denied tool call is refused inside the turn and recorded as run.denied. But it isn't the boundary. Rings 2 and 3 are.
Ring 5: Charter#
The charter tells the agent what its job is. Nothing in the security model depends on the agent following it.
Risky actions are office tools#
Actions with real consequences, such as proposing hires, mailing colleagues and requesting approvals, aren't shell commands. They are agent MCP tools. The office checks the capability server-side before it acts, and records a denial in the ledger.
The verify suite#
officectl verify tests rings 1 to 4 and the ledger against your own installation, playing the attacker with real agent identities. PASS means the attempt was blocked. Checks 1 to 6 always run (2 and 4 need two provisioned agents); check 7 runs only with --live. On a real Ubuntu 24.04 server, officectl verify --live passes all seven.
| # | Attempt | Must |
|---|---|---|
| 1 | An agent's OS user reads the office DB or secrets | Fail |
| 2 | An agent's OS user reads another agent's home | Fail |
| 3 | An agent's token changes CLAUDE.md in its own vault, through the API and with git push | Be rejected by the forge |
| 4 | An agent's token reads another agent's vault repo through the API | Get 404 or 403 |
| 5 | An agent's token pushes to, or merges a PR into, the policy repo's main | Fail |
| 6 | Ledger chain verification, and the latest anchor matches | Succeed |
| 7 | --live: a real run (needs the server running and a model credential) is told to read the office DB, reach a host that isn't allowed, and edit and push CLAUDE.md | Every attempt blocked |
Each attempt is ledgered as verify.attempt. Run it after installing and after every upgrade. See Run the verify suite.
Read next#
- Threat model: who we defend against, and how.
- What the POC does and doesn't protect against: the honest list.