Skip to content
WIP

This guide is a work in progress. Barry is also a work in progress.

Security model and limits

This page says what Barry protects, what it doesn’t, and where each guarantee stops. Read it before you hand an agent a credential, expose a service beyond your Mac, or rely on a bound to stop a determined adversary. Every limit here comes from the code or from Barry’s own design records, and the page names them plainly so you can decide which risks to accept.

The short version: Barry draws one hard boundary, between your user account and the vault daemon’s separate account. Everything else that runs as you, including your agent sessions, shares one level of trust. Inside that level, Barry narrows what a session can see and call, and those controls stop ordinary mistakes well. They don’t make a hostile process running as you safe.

Barry’s services talk over local HTTP, and a single instance secret, BARRY_SECRET, authorizes a call. A caller presents it as a bearer token or as an x-barry-secret header. Barry compares the two values in constant time and refuses a length mismatch, and it rejects a request that repeats either header, so a duplicate field can’t slip a second value past the parser. The services read the secret on every request, so a rotation takes effect without a restart. See internal.ts for the check.

No trusted-address shortcut exists. A request from 127.0.0.1 gets no special treatment, because every hosting provider, whether a local name, a private tunnel, or a public tunnel, connects from loopback. Trusting loopback would trust the internet the moment a tunnel pointed at a service. A service that starts without a secret to compare against answers 500 rather than accepting anything, and an authorization check that times out answers 503.

Browser-facing services, such as the memory browser and the metrics dashboard, can’t ask a page to carry the secret. You type it once on a sign-in page, and the service sets an HttpOnly, SameSite=Strict cookie that holds a signed value derived from the secret with its own 30-day expiry. Rotating BARRY_SECRET signs every browser out.

The cost of one secret: every process running as your user can read it. The MCP configuration Barry writes for a harness carries the secret as a bearer header, so the harness process holds it too. Treat any process under your account as able to call Barry’s local services with full owner authority.

A phone or another client doesn’t need the full secret. barry device pair reviews a pending pairing request and approves it with a set of scopes, a list of identity IDs, and an optional expiry. barry device list shows each credential’s grants and usage, and barry device revoke withdraws one. The device command reference lists the flags.

A device credential resolves to a principal that names its user, its identity grants, and its scopes. The scope vocabulary covers sessions, events, actions, tools, identities, and artifacts, but the routes that accept devices today open reads: listing and reading sessions and their messages, subscribing to a session’s events, reading an identity, and reading artifacts. A device may read sessions but not start or control them, and the MCP server refuses device credentials outright. Barry checks a device credential against the identities store on each request, so a revocation takes effect on the next one.

Keep “owner” in its two senses apart. barry owner shows or binds the user who owns this installation; the instance secret resolves to that user. A workflow owner is the machine that holds a workflow’s state and hands steps to workers.

The vault daemon runs as a separate operating-system user, _barryvault, and listens only on a unix socket that the kernel guards at connect(). It ignores BARRY_SECRET on purpose: every process running as you can read that secret, so a vault that accepted it would open to exactly the processes the separate account exists to exclude. The vault issues its own tokens instead, and keeps the bearer token for its MCP endpoint apart from its signing key. sudo barry setup --system creates the account and installs the daemon’s code root-owned, so an ordinary user can’t rewrite what runs as _barryvault.

That separation protects the store’s integrity, the daemon’s memory, and the server code. It doesn’t keep secrets from an agent running as you:

  • Each identity’s vault master password and client credentials live in your login Keychain without an access-control list, so any process running as you can read them, and the master password alone can log in and decrypt.
  • Your user belongs to the _barryvault group by design, so your own processes, agent sessions included, can reach the vault’s API over the socket.
  • Anything that reaches root crosses the boundary.

The vault README walks through each of these, and ADR 0042 records that moving key custody into the daemon’s account remains a design direction, not finished work.

When a session starts, Barry resolves the secrets an identity declares and hands each tool the values that tool names. That routing keeps a value out of tools that never asked for it, but the session’s own process sits under your user. In the words of the secrets guide: treat a declared secret as readable by the session you hand it to. A bag service that opens a vault can also decrypt every item in that vault, so give a service its own vault when that matters. The secrets walkthrough covers declaring and scoping them.

Three things never reach the agent through resolution: the vault master password, the vault client credentials, and any secret the identity didn’t declare. Barry also strips every declared credential name from the environment it passes the harness, so an ambient key in your shell doesn’t leak into a session.

In the MCP server, each tool handler receives only the secrets its definition lists, and a call fails with a missing-secrets error when one of them didn’t resolve. The SDK’s scoped tool invoker, which workflow tool activities build on, goes further. scoped-invocation.ts authorizes the call, fetches only the credentials the tool declares, and then authorizes it again. If the policy fingerprint changed while it waited, it refuses with “execution policy changed during invocation admission.” It also refuses when the run’s approved fingerprint doesn’t match, when the tool’s read or write classification or its contract hash changed, or when the destination can’t enforce the run’s resource restrictions. A stored receipt never bypasses a revocation: the invoker checks current authorization even before it replays a finished result.

A worker machine authenticates with its own credential, not the instance secret. That credential opens only POST /api/worker. It opens no console, run inspection, start, session, identity, or store API. The owner machine stores a hash of it and never sends a worker its instance secret, an identity’s whole environment, or the contents of a vault.

A task runs on a worker only when that worker can enforce everything the task requires: its credentials, workspace, runtime, and bounds. Versioned Node bag jobs that use no files and declare no secrets qualify as they are. Eligible scripts and agent actions run remotely with credentials provisioned for that worker or a narrowly scoped delegated credential, and they declare their workspace up front. Anything else stays on the owner machine, or stays visibly blocked.

Scopes narrow what a worker credential can claim. They don’t verify results or sandbox a hostile worker operator. A stolen credential can claim any job in its scope, read those jobs’ inputs, and report fabricated results, so invite only machines you operate. barry workers invite --jobs sets the scope, and barry workers revoke refuses the worker’s next call. Distributed worker trust covers the full threat model, and Distributed nodes explains the protocol.

Enforcement depends on the harness and mode

Section titled “Enforcement depends on the harness and mode”

Barry enforces a bound through more than one layer, and the harness and its launch mode decide which layers apply. The bounds reference lists the fields; this section covers how far each one reaches.

  • Barry’s own tools. The MCP server filters tools by trait and bound and checks every call, whatever the harness.
  • Other MCP servers. A session starts with a strict MCP configuration that loads only the servers Barry configured. <identity> session start --relax merges your user-level MCP servers in, and their tools bypass Barry’s tool filter and call-time guards.
  • Native tools. Native tools stay off unless a trait grants them, and a bound takes them back. A mode declares which native tools it can control. Cursor leaves some tools uncontrolled, and in the guest and instrumented modes of Antigravity and Codex an operator can lift the hook gate with /hooks. A bound that would need to take back an uncontrolled tool refuses the launch.
  • Per-call gating. Claude Code’s guest and instrumented modes run the vendor’s own interface with no Barry PreToolUse hook, so their native tool settings act as static restrictions, not per-call authorization. The Claude Code modes record this beside the evidence for each mode.
  • Guards. File, shell, and network guards inspect structured tool parameters and recognizable shell commands. Interpreter code, arbitrary scripts, and URLs built at runtime fall outside them. The SDK bounds notes call them best effort against ordinary mistakes.
  • Egress sandbox. network.enforce: sandbox runs the agent behind a proxy that filters outbound traffic by hostname, and only Claude Code’s hosted mode declares one. The proxy can’t tell an HTTP read from a write to the same host, so partial action denials still rely on guards.

Adversarial protection needs the sandbox, and even the sandbox governs only egress. Bag tools that make requests from their own process answer to their trait and tool grants, not to the agent’s network bound.

Where Barry can’t prove something is safe, it refuses rather than guesses:

  • A session the MCP server can’t resolve gets no tools, and the server retries on a backoff. A connection that names no session gets only the default-trait grant, never the full toolset, and one that names an unknown session gets a 403. ADR 0018 records why.
  • Bag health reports distinguish “nothing needs attention” from “Barry couldn’t find out,” so an outage never reads as a clean bill of health (ADR 0013).
  • A session whose required credentials have no source, or won’t read, doesn’t start.
  • <identity> session start --bound stops when Barry can’t record the session with its bound, instead of running unbounded.
  • A requested egress sandbox that the harness or model endpoint can’t enforce refuses the session, and the sandbox fails rather than runs unsandboxed when it can’t start.
  • In Codex, a missing, failing, slow, or malformed policy script produces an explicit deny. ADR 0029 also records the edge: deleting the launcher or disabling hooks can still let a call through.
  • A store stamped by a newer build refuses to open, so an older build never reads a schema it doesn’t fully understand.

For the processes and stores these rules protect, see Services and stores.