Agent sandbox hardening checklist
Run this before you let a CLI agent loose on a repository you care about, and again after every harness update. The last two sections decide whether the rest is real — an untested boundary is just a config file with good intentions.
Updated
Work through it in order. The first three sections are what you configure, the fourth is what you prove, and the fourth is the one people skip.
Bound the process
- The agent runs under a profile with a deny-default stance and an explicit allow list, launched by a script rather than typed by hand.
- Exactly one workspace directory is writable; your home directory isn't.
- Writes to the agent's own code, skills, extensions, plugins and prompt directories are denied.
- The profile, the launcher and the service wrapper all sit outside the agent's write scope.
- Escalation is an out-of-band request a human reviews, not a permission the agent can grant itself.
- Credential paths like ~/.ssh, ~/.aws and ~/.gnupg are denied, and no escalation grant can open them.
- Interactive terminals and pseudo-terminal allocation still work, so the agent's UI doesn't fail in a way that looks like a bug in the agent.
- Compatibility allowances are labelled as compatibility inside the profile, not as trusted exceptions.
Profile pitfalls that aren't escapes
- Denying the filesystem root denies the root inode, so the dynamic linker aborts before your program starts: allow the root literal and deny beneath it.
- A working directory inside a denied region breaks getcwd, and tools report it as a missing command rather than a permission problem.
- Paths are matched after symlink resolution, so /tmp is really /private/tmp — which silently moves the path you thought you granted.
- Traversal needs metadata reads; without them, resolving an intermediate directory fails and looks like a broken binary.
- Escalation slots are fixed at launch: an empty slot has to point at a path that doesn't exist, so the shipped profile grants nothing extra.
- The rule that matches last wins, so exceptions are a targeted rule placed after a broader one — verify that on your own OS version instead of trusting the summary.
If you run it in a container
- The Docker or Podman socket isn't mounted, and the container isn't privileged.
- The container runs as your uid:gid, not root.
- The workspace is mounted; your home directory isn't, and config mounts are read-only.
- Egress is either off or forced through a proxy you log, with no direct route out and DNS resolved through the same path.
- Capabilities are dropped to none and no-new-privileges is set.
- The root filesystem is read-only with an explicit tmpfs for temp writes.
- CPU, memory and PID limits are set, so a runaway loop can't take the machine.
- User namespaces are enabled or the runtime is rootless, so a bind mount isn't owned by root on the host.
- The default seccomp profile and the runtime's AppArmor or SELinux profile are still applied, not replaced by an unconfined setting.
Credentials
- No long-lived provider key is readable from inside the boundary.
- Egress substitution is in place: the process holds a placeholder and a proxy outside the boundary swaps in the real value.
- Each injected credential is scoped to the hosts it may reach, and the proxy is the only route to them.
- Tokens are short-lived, with a lifetime measured in minutes rather than months.
- Repository-controlled configuration can't redirect the harness to another endpoint before a trust decision happens.
- Extension, plugin and MCP server code runs under the same restrictions as the harness, or you've accepted in writing that it doesn't.
- You've checked the non-HTTP channels too: image URLs in rendered output, DNS queries, logs and chat transcripts.
Prove it, then keep proving it
- Write inside the workspace succeeds; a write to your home directory fails.
- Reading harness config succeeds; reading ~/.ssh fails, including after an escalation grant.
- Writing to a sibling project and to the profile both fail.
- A control run removes one deny, shows the same test allow the action, then puts it back — proving the test can fail instead of passing vacuously.
- The log of what the kernel actually blocked gets reviewed after each change, and unexpected denials get investigated rather than silenced with a grant.
- The whole set is re-run after every OS update, harness update and profile edit, by a named person.
- A session that claims to be sandboxed is verified by probe, not by environment variable: an environment variable is a claim, a denied write is evidence.
Related
What contains a CLI agent
Four failure modes, the profile that answers them, and why enforcement can't live inside the agent's own instructions.
Seatbelt, container, VM
Which failures each isolation layer contains, five container defaults that delete the boundary, and which kind of VM is a real one.
Where the credential lives
Two shapes of agent isolation, the one that keeps keys out of the process, and the failure both shapes still miss.
AI security assessment
Security assessment of GenAI and LLM systems: prompt injection, tool and agent abuse, data leakage, model supply chain and monitoring gaps.
AI security
How to secure GenAI, LLM and agentic AI systems: current OWASP threat classes, layered controls, agent authority, retrieval isolation and monitoring.
STRIDE for AI systems
Applying STRIDE to LLM and agentic applications: what each category means when untrusted text can act as instructions, where STRIDE stops covering multi-agent failure modes, and the controls per element.