What an agent can reach, and what it cannot
Most agent tools isolate a run with a git worktree, which stops two agents colliding and stops nothing else. PortBay dispatches agents into a real local environment on your Mac, so this page is the blast radius instead: the kernel profile each agent runs under, what it denies, the guards that stop a run gone wrong, every path data takes off the machine, and what PortBay does not do.
Every claim below was read out of the app source on 2026-09-06.
How do you sandbox an AI coding agent on a Mac?
macOS ships a kernel sandbox, Seatbelt, applied to a command through the sandbox-exec wrapper. A profile starts at (deny default), re-allows only what the run needs, is inherited by every process the command spawns, and cannot be lifted from inside. PortBay generates one profile per dispatched agent and wraps the agent CLI in it. The profile denies reads of ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, keychains, browser profiles, shell history, and every .env file outside the project the agent was given; writes are confined to that project, the agent's own state directory, and temp. Refused operations surface as kernel violation lines in the run log. Seatbelt filters network by address, not hostname, so per-host egress needs a second layer: a loopback proxy the agent is pointed at. Agent sandboxing is on by default in PortBay; the egress proxy is opt-in, and turns on by itself for runs carrying credentials.
A worktree is not a sandbox
The usual way to isolate an agent run is a git worktree: a separate checkout of the repository so two agents working at once do not overwrite each other. That is a real problem and a worktree is a real solution to it — but the problem it solves is merge collision, not blast radius.
A process running in a worktree still runs as your user account. It can read ~/.ssh, open a sibling project's .env, reach any host it likes, and write anywhere on the disk you can write. A second checkout changes which files the agent is looking at. It changes nothing about what the kernel will let the process do.
PortBay uses worktrees as well, one per card, for exactly the reason everyone else does. The difference is what sits underneath them: the dispatched agent is also wrapped in a kernel sandbox profile. The worktree bounds the merge. The profile bounds the damage. If you are comparing tools in this category, that is the question to ask each of them — not whether runs are isolated from each other, but whether the run is isolated from your machine.
There are two sandboxes and they do not share a default
They confine different things and they are switched on in different places. Assuming the safer default applies to both is the mistake this section exists to prevent.
The agent sandbox
Confines the dispatched agent CLI and every process it spawns. This is the one that denies your credential stores and keeps the agent inside the project it was given. It applies to every card you dispatch, on a fresh install, with nothing to switch on. Turning it off runs the agent at your full user privilege.
The project sandbox
Confines your project's own process — the dev server — and is meant for code you have not read yet, such as a repository you just cloned. It is a per-project choice, off unless you turn it on for that project or an admin preference forces it on for all of them. Community covers 2 sandboxed projects at once; Pro removes the cap.
They are independent switches. Enabling Sandboxed Run on a project does not change how an agent dispatched against it is confined, and the agent sandbox being on does not confine the dev server your project starts. If you want both, set both.
What is on before you change anything
A control that ships off is not a control. Here is the state of every one of them on a fresh install, including the ones that are off.
| Control | Default | What it means |
|---|---|---|
| Agent sandbox | On | Every dispatched agent, and every process it spawns, is wrapped in a generated Seatbelt profile. Turning it off runs the agent at full user privilege. |
| Egress host allowlist | Opt-in | A loopback CONNECT proxy with a deny-by-default host list. Off by default because a too-tight list breaks a working run — but it switches on by itself for any run that carries resolved secrets or an injected database credential. |
| Project sandbox | Per project | Confines the dev server itself, not the agent. Chosen per project, or forced on for every project by an admin preference. Community covers 2 sandboxed projects at once; Pro removes the cap. |
| Require review | Opt-in | Holds an agent-reported Done in the Review column until a human confirms it, instead of advancing the card. |
| Strike cap | 3 runs | After 3 consecutive non-Done outcomes a card stops re-dispatching and lands in Blocked for a human. Set it to 0 for the old infinite-retry behaviour. |
| Turn cap | 60 turns | A run that reaches 60 turns ends. Not configurable without recompiling. |
| Usage analytics | Off | Nothing is sent until you switch it on. Worth knowing: events are written to a local queue whether or not you have consented — up to 500, after which new ones are dropped — and consent gates delivery, not capture. So switching analytics on later also sends the backlog that accumulated while it was off. Delete the queue directory first if you do not want that. |
| Crash upload | Off | Crashes are captured locally and scrubbed of paths and secrets before they touch disk. They leave only when you press Send. |
| Regulated mode | Off | The air-gap switch. Refuses outbound egress at around thirty named seams — updater, analytics, crash upload, model catalog, cloud sync, connectors, hosted models, remote MCP, tunnels, billing — and forces the agent profile to its loopback-only posture. Loopback keeps working, so local models and stdio MCP servers still run. |
Broad reads, denied secrets, narrow writes
The profile allows reads widely, because a toolchain that cannot read its own caches cannot build anything — then denies the credential stores back. Seatbelt matches the last rule, so a later deny beats the earlier allow.
Denied to the agent
- ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, ~/.docker, ~/.azure
- ~/.config/gh, gcloud, fly, doctl — the CLI credential stores
- ~/.password-store, ~/.1password, ~/.terraform.d
- Chrome, Chromium, Brave, Edge, Firefox and Safari profiles
- Library/Keychains and /private/var/db/Keychains
- Library/Messages and Library/Mail
- ~/.npmrc, ~/.netrc, ~/.git-credentials, ~/.pgpass, ~/.vault-token
- Shell history — zsh, bash, python, node, psql, mysql
- Every .env file on the disk, matched by regex, except the project's own
- The PortBay secret vault, denied again after the data directory is allowed
Reachable by the agent
- The project directory the agent was given — read and write, its .env included
- The agent's own state directory, so its CLI can write session logs and its database
- Package manager caches and version managers — .npm, .cargo, .rustup, .pyenv, .gradle, .m2
- Shell rc files and .gitconfig, so a build finds its toolchain
- System frameworks and libraries outside your home directory
- /tmp, /private/tmp and the resolved TMPDIR — every compiler needs scratch space
(version 1) (deny default) ; broad read outside $HOME — toolchains, frameworks, libraries (allow file-read*) ; $HOME flipped to default-deny, toolchain dirs re-allowed (deny file-read-data (subpath "/Users/you")) ; then the credential stores are clawed back — last match wins (deny file-read* (subpath "/Users/you/.ssh")) (deny file-read* (subpath "/Users/you/.aws")) (deny file-read* (literal "/Users/you/.npmrc")) (deny file-read* (regex #"/\.env")) (deny file-read* (subpath "/Library/Keychains")) ; the workspace is re-allowed after the denies, so it reads its own .env (allow file-read* (subpath "/Users/you/code/api")) (allow file-write* (subpath "/Users/you/code/api") (subpath "/private/tmp")) ; the agent CLI keeps its session state, but not its own config — ; hooks and MCP definitions program the NEXT, unconfined run (deny file-write* (subpath "/Users/you/.claude/hooks")) (deny file-write* (subpath "/Users/you/.claude/settings.json"))
That last pair is the subtle one. An agent state directory has to stay writable or the CLI will not start — but a hook script, a permission allowlist or an MCP server definition written into it is not state, it is the instructions for the next run. A prompt-injected agent that appends a hook is confined for the minutes it is dispatched and then runs as you, unconfined, the next time you open that CLI yourself. Those paths are denied after the write allowance, so session logs stay writable and the next run stays yours.
What it looks like when the boundary holds
A refused operation is not a silent failure. The kernel writes a violation line, PortBay filters the run log for them, and a denial is classified apart from a crash so the agent is never told to go fix code that never ran.
# the agent tried to write an SSH key sandbox-exec: deny(1) file-write-create /Users/you/.ssh/id_ed25519 # the kernel violation line, with the offending pid Sandbox: sh(52413) deny(1) file-write-create /private/tmp/x # what the agent itself sees — a plain EPERM /bin/sh: /private/tmp/portbay-outside/escaped.txt: Operation not permitted # not a denial: the runner never started the command at all sandbox-exec: execvp() of '/nonexistent/binary' failed: No such file or directory
The fourth line is the one worth dwelling on. A blocked write and a sandbox that failed to launch are both a non-zero exit with text on stderr, and reporting the second as the first sends an agent off to fix code that never executed. PortBay separates them by exit code and by the runner prefix, and it never merges the two backends into one vocabulary — a bubblewrap error read under Seatbelt would report a denial Seatbelt cannot produce.
What stops a run that has gone wrong
The sandbox bounds what a run can touch. These bound how long it can keep being wrong before a human is put back in the loop.
Repeated identical calls
The third identical tool call with an identical result gets the model a written warning. The fifth aborts the run. The window is the last 20 calls.
Consecutive failures
Three errors in a row on the same board tool warns; five aborts. Five failed edits in a row on the same file aborts with no warning first.
Turn ceiling
A run ends at 60 turns, where a turn is one batch of parallel tool calls. There is no preference for this.
Command timeout
A shell command the agent runs is cancelled at 120 seconds. Other tools carry their own budget, defaulting to 300 seconds.
Strike cap on the card
Three consecutive non-Done outcomes and the card stops re-dispatching, moves to Blocked, and waits for a human. A human edit or move resets the count.
The tools it is given
An agent does not get the whole tool surface. A run is dispatched with a toolset ceiling it cannot widen past, and a boot floor holding only the card protocol it needs to report progress. Groups outside that scope are not advertised to the model at all.
Screen capture waits for a human
The app holds the Screen Recording grant, not the agent. A capture request blocks until someone approves it in the app, and a request nobody answers times out as denied rather than allowed.
The secret vault
Stored credentials sit in a vault encrypted under a key held in the macOS Keychain. The generated profile denies the vault directory a second time after the data directory is allowed, and a sandboxed process has no route to the Keychain entry either — so the agent cannot read the secrets even though it shares the rest of that directory.
Refused before they run
sudo, doas and pkexec are refused for an autonomous run, and there is no grant that waives it — escalation exists to leave the sandbox, so the kernel would refuse it a moment later anyway. System-level deletes and reads of .env files, ~/.ssh and cloud credential stores are refused too, and those two classes can be escalated by an explicit approval policy.
A struck-out card lands in Blocked and stops dispatching itself. Two optional passes can run before it gets there: a rescue attempt that retries with a different agent, and a teacher call that reads the failed trace and writes what it learned into the project rule book. Both are off by default. Only a human moves a card into Rejected.
Everything that leaves the machine
PortBay is local-first, which is a claim worth checking rather than believing. This is the complete list, including the paths that are not PortBay making the call.
| What | When | Detail |
|---|---|---|
| Your code | Never | PortBay does not upload your source. Project files, the task board, cards and agent comments live on the machine, and no account is needed to use it. |
| Registry sync (Pro) | If you sign in on a second Mac | Your project registry — which projects exist, their settings — is encrypted on the device with a 256-bit account key (AES-256-GCM) before upload, and the index labels are blinded HMAC tags the server cannot invert. This is encrypted in transit and at rest, not end-to-end: the account key is provisioned to any device you sign in on, so the server can decrypt. Machine-specific values never cross — file paths, install locations, device settings, and every consent flag, because syncing a consent would grant a privilege on a machine where nobody agreed to it. |
| Agent CLI to its model provider | Every cloud run | Claude Code, Codex, Cursor and the rest talk to their own providers under your existing subscription, exactly as they do in a terminal. Your prompts and the file contents they read go with them. Dispatch a local Ollama model instead and the run stays on the machine. |
| Cloud agent keys (BYOK) | You supply them | Anthropic and OpenAI endpoints, both overridable to a self-hosted gateway. Keys live in the macOS Keychain and are sent only to the provider you chose. |
| Package registries and git | Your build runs | npm, PyPI, crates.io, the Go proxy and GitHub — reached by the agent's own install and clone commands, not by PortBay. |
| Update check | When you check | A signed manifest from the GitHub releases page. An HTTP request with no payload; the download is verified against a published key before it installs. |
| Model catalog | When you look for models | A signed manifest from a GitHub release, cached for 24 hours. Model files themselves come from Hugging Face. |
| Usage analytics | Off by default | If enabled: an allowlisted event name, a random install id, app version, OS, architecture, CPU count and locale, to cloud.portbay.app. No paths, no hostnames, no project names, and every string passes the secret scrubber first. Capture to a local queue happens regardless of consent — consent gates delivery — so turning it on ships the backlog too. |
| Crash reports | Off by default | Captured locally and scrubbed of paths and secrets before they are written. They reach Sentry only when you press Send on a specific report, and only if a reporting endpoint is configured. |
The largest line in that table is the second one, and it is not PortBay. Dispatching Claude Code sends your prompts and the files it reads to Anthropic, under your own account, exactly as running it in a terminal does. PortBay narrows which files it can read; it does not change where the ones it does read go. Regulated mode closes the PortBay side of that table at a single seam, and forces the agent profile to loopback only.
Regulated mode, if nothing may phone home
For the case where the answer to every question on this page has to be no: one switch that refuses outbound egress at every seam that leaves the machine.
The guarantee is enforced in the Rust core rather than by hiding buttons, so it holds when a command is invoked directly and when the interface is bypassed. Around thirty named seams check it and fail closed with an explicit refusal: crash upload, analytics, the updater, the model catalog and every model download, cloud config sync and device registration, task-source connectors for GitHub, GitLab and Jira, cloud provider chat and any BYOK card dispatched to a hosted model, remote MCP servers, tunnels, web search and page fetches, avatar and favicon loads, sign-in and token refresh, billing, and licence refresh.
Loopback survives on purpose. A local Ollama model, the local MCP servers your agent talks to over stdio, and dictation against the local daemon all keep working, so an air-gapped machine is still a working machine. Regulated mode also forces the agent profile into its loopback-only posture, which is the one configuration where the kernel boundary alone stops an agent reaching any host off the box.
Two deliberate carve-outs, because a mode that costs you what you already hold is a mode nobody leaves on: a cached sign-in session is kept rather than cleared when refresh is refused, and a cached signed entitlement keeps being honoured, so a Pro user is never downgraded for being unable to reach the licence server.
Supply chain is not blast radius
Is this binary safe to run, and is it safe to let an agent operate inside it, are separate questions with separate answers. Conflating them is how a vendor ends up answering the easy one.
The first is about the artefact. Every release is built, signed, notarized and stapled by CI; updates are verified against a published key before they install; each release ships a bill of materials listing every dependency compiled in; and the whole app is open source. That is covered on the download page, and it is table stakes.
The second is this page. A perfectly signed binary running an agent with your shell privileges still has your SSH key in reach. Signing tells you the code is the code the project published; it tells you nothing about what that code lets an autonomous process do at three in the morning while you are asleep. That is what the profile, the guards and the egress table above are for.
What PortBay does not do
Read this section before the rest, if you are evaluating for a team. A documented failure mode is worth more to you than a page with none.
There is no team plan, and no admin console
No SSO, no SAML, no SCIM, no seats, no roles, no central policy server, no MDM profile, and no cross-machine audit log. A licence covers 1 Mac on Community and 2 on Pro. PortBay is a single-developer tool, and every control on this page is set by the developer sitting at that Mac. If your security model requires that a lead can set a policy their team cannot change, PortBay does not do that today.
No SOC 2, no ISO 27001
Quoting the project's own SECURITY.md: PortBay is built to satisfy enterprise security review; it holds no SOC 2 or ISO certification today, and a formal SOC 2 programme starts when it sells to teams. There is no DPA to sign, because there is no processor relationship — nothing is processed off your machine.
The agent sandbox fails open
If Seatbelt is missing, or the profile cannot be written, dispatch continues without confinement rather than refusing. That is a deliberate choice — a sandbox bug should not brick your board — and it is the right one to know about. Regulated mode and read-only cards are the two paths that refuse the run instead.
An agent opened in your editor runs outside the sandbox
Dispatching a card into a desktop app or IDE hands the work to that editor, which spawns the agent itself later, with no command line for PortBay to wrap. Those runs are unconfined and are recorded as unconfined. Dispatch in CLI mode to get the boundary.
The read deny-list is curated, not exhaustive
Development toolchains read so widely across a disk that a strict read allowlist breaks real builds, so the profile allows broad reads and then denies the known credential stores back. It covers the targets a malicious dependency actually goes for. It is not proof that no secret in an unusual location is readable.
An allowlisted host is still a way out
The egress proxy decides which hosts a run may reach. It says nothing about what flows to them, and it never inspects traffic. An agent allowed to reach github.com in order to clone can also push to a repository you do not own. Treat the allowlist as narrowing the blast radius, not as data-loss prevention.
Seatbelt is not a virtual machine
sandbox-exec constrains what a process may do through the kernel; it is not the isolation boundary a VM or a microVM gives you, and Apple has had it deprecated since 2016 while still shipping it. Treat it as trust-but-verify friction for code you are inspecting, not as containment for code you believe is hostile. For that, use a machine you can throw away.
Linux confinement is coarser
The Linux build confines with bubblewrap instead, and its only network lever is all-or-nothing, so the outbound and unrestricted policies behave identically today. Arbitrary .env files elsewhere on the disk cannot be denied by pattern the way they are on macOS.
Reporting a vulnerability
Open a private advisory on the repository, which is the preferred route, or email [email protected]. A PGP key is published if you want to encrypt the report.
The published targets are three business days to acknowledge and ten to reproduce or come back with questions, with disclosure after a patched release exists. In scope: the public codebase, its local IPC and HTTP surfaces, certificate and signing verification, and unintended data exfiltration. Out of scope: the hosted backend, bugs needing physical access, third-party project code outside a sandboxed run, and social engineering. The full policy carries the key fingerprint and the supported-version table.
Read the profile before you trust it
Free & open source · macOS 11+ · The profile is generated on your machine and written to disk, so you can read the one your own agent runs under.
