Founders

How to Set a Blast Radius Limit for an Agent's File Access

Antonio Evans9 min read

A gloved hand hovers over a single highlighted folder on a laptop screen.
Key points

Key points

  1. 1Define the blast radius by writing an explicit allowlist of directories or a single git branch, not by trusting the agent to stay put.
  2. 2Enforce the boundary below the agent, using read-only mounts, a container sandbox or kernel isolation so a rejected write fails before it touches data.
  3. 3Apply the reversibility rule: any write that cannot be undone with one command belongs outside the allowed set.
  4. 4Log every rejected write as a structured event with actor, path, reason and correlation ID so the boundary is auditable.

Why the boundary must match the reversibility rule

A blast radius limit is only useful if it maps to something you can actually undo. The practical version of that idea is a decision rule: if a write cannot be reversed with a single command, it must fall outside the allowed set. A file created inside a scratch directory is reversible with a delete. A database migration that altered a production schema is not, so it should never be inside an agent's write scope.

The distinction matters because agents operate in a repeated observe, decide, act, evaluate loop, and each iteration is another chance to encounter untrusted content or take an unintended action (Penligent on rogue agents). A prompt injection hidden in a file the agent reads looks identical to ordinary data in a standard log, which is why the boundary has to be enforced by the runtime and not by the model's judgment (LoginRadius on agent auditing).

The reversibility rule gives you a tiebreaker for the hard cases. When a reviewer asks "should the agent be able to write here?", the answer is yes only if you can name the command that undoes the change. If you cannot name it, the write is blocked and routed to a human or a pull request.

There is a second reason to anchor the limit to reversibility rather than to intent. Traditional permissions assume software behavior can be constrained through authentication, authorization, isolation and monitoring, but autonomous agents decide which actions are necessary, which makes intent a poor control surface (Penligent). Reversibility is a property of the artifact, not of the agent's reasoning, so it holds even when the reasoning goes wrong.

Run this permission checklist before the agent's first write

Work through the list in order. Each item produces something concrete you can put in a config file or a review comment, which is the difference between a policy document and a working limit.

StepQuestionArtifact you producePass condition
1What is the smallest directory or branch the task needs?Absolute path or branch nameA single path or ref, not a list of roots
2Can every allowed write be undone with one command?Undo command per pathEvery path has a named undo
3Is the boundary expressed as an explicit allowlist?Runtime config entryDefault deny, no wildcards
4Is the block enforced below the agent?Mount flags, sandbox profile or syscall filterRejected write fails at the kernel
5Does every rejection emit a log record?Log schema and destinationStructured fields, not free text
6Is there a path for legitimate escalations?Pull request or approval queueHuman review, not agent self-approval
7Does the agent run as its own identity?Non-human account or workload identityActions attributable to the agent, not a shared service account

Step 3 is where most implementations leak. The principle of least privilege says give the agent only the access it needs, and the way to express that in code is an allowlist that rejects everything not named (mbrenndoerfer on agent permissions). A wildcard like /repo/** is not an allowlist; it is a perimeter with a hole in it.

Step 4 is where most implementations fail silently. If the only thing stopping a write is a check inside the agent framework, then a bug or a cleverly worded instruction can route around it. The check has to live where the agent cannot argue with it.

Choose the boundary: a directory, a branch, or both

The two common shapes are a directory boundary and a git branch boundary, and they solve different problems. Pick based on what the agent is doing, not on what is easiest to configure.

A directory boundary is right for agents that read and write files as artifacts: documentation, generated code, data exports, scratch analysis. The limit is an absolute path, and the enforcement is a mount or sandbox that makes everything outside that path unreachable or read-only.

A branch boundary is right for agents that change source code under version control. The agent gets push access to one branch and nothing else, and the branch protection rules on the default branch keep merged history out of reach. The reversibility property here comes from the branch itself: a commit on a throwaway branch is undone by resetting the ref or deleting the branch, which is a single command.

Many production setups need both. An agent that edits code and writes a build artifact might get write access to /workspace/src and a single branch, with read-only access to the rest of the checkout. That combination is small enough to reason about and large enough to do real work.

The rule of thumb: if the undo command is git branch -D or rm, the write is inside the boundary. If the undo command would be a database restore, a cloud resource rollback, or a support ticket to a customer, the write is outside it.

Enforce the limit at the filesystem, not in the prompt

An instruction in a system prompt is a suggestion. A read-only mount is a fact. The reliable pattern is to make the boundary a property of the environment the agent runs in, so that a rejected write fails before it reaches storage.

The strongest version of this is kernel-level isolation. AgentFS, a sandboxing filesystem built by Turso, pushes the access boundary to namespaces on Linux and sandbox profiles on macOS rather than decorating files with permission metadata (AgentFS walkthrough). The Linux path forks a child, calls unshare to give it a private mount table, and bind-mounts a copy-on-write overlay onto the working directory. The key detail is that a mount marked read-only at the virtual filesystem layer causes the kernel to reject writes before they ever reach the underlying filesystem, regardless of what the inode permission bits say (AgentFS).

A lighter version is a container with a read-only root and a single writable volume. The backend comparison from Vstorm's pydantic-ai-backend work is a useful mental model: an in-memory backend for tests, a local backend for development, and a Docker sandbox for anything that touches real files, all behind the same interface so the agent code does not change (Vstorm on safe filesystems). Swapping the backend is the isolation dial.

Here is a minimal Docker invocation that expresses the boundary. The whole container root is read-only, and only the allowed directory is writable:

docker run --rm \
  --read-only \
  --tmpfs /tmp \
  -v /srv/agent-work:/work:rw \
  -v /srv/repo:/repo:ro \
  --user 10001:10001 \
  agent-image:local

The :rw on /srv/agent-work and :ro on /srv/repo are the allowlist. A write to /repo/anything fails with a read-only filesystem error, which is exactly the signal you want to capture.

One caveat: you cannot rely on chmod alone. When the agent runs on your behalf, file permission bits do not distinguish it from you, which is the reason AgentFS moved the boundary to the mount table in the first place (AgentFS). Use ownership and mounts together, not permission bits by themselves.

Write the allowlist into the agent's runtime configuration

The mount enforces the limit; the runtime config tells the agent what the limit is so it does not waste turns trying to cross it. Both are needed. The config is not the control, but it is the instruction that makes the rejection rare.

A workable pattern is a single config block with explicit paths and an explicit branch, parsed at startup and validated against the actual mounts. If the config names a path that is not writable in the container, the process refuses to start. That startup check catches the most common drift: someone widens the config and forgets the mount, or widens the mount and forgets the config.

ALLOWED = {
    "write_dirs": ["/work/out", "/work/scratch"],
    "read_only_dirs": ["/repo", "/work/reference"],
    "write_branch": "agent/scratch",
    "undo": {"rm -rf /work/out/*": "scratch outputs",
             "git push --delete origin agent/scratch": "agent branch"},
}

def is_allowed_write(path: str) -> bool:
    return any(path.startswith(d) for d in ALLOWED["write_dirs"])

The undo map is the reversibility rule made concrete. Every writable path has an entry, and the entry is a command a human can run without judgment. If a proposed path has no undo entry, it does not get added to write_dirs.

Tool descriptions are the other half of this. If the agent's write tool description says "write a file" with no scope, the model will treat the whole filesystem as fair game. A description that names the allowed directories and states that everything else will fail is cheap and reduces noisy attempts.

Log every rejected write with structure, not free text

A blocked write that nobody records is a boundary you cannot audit. The log entry is the artifact that turns a limit into evidence, and it is the thing you will read first when something goes wrong.

Standard application logs are a poor fit for this. They capture that an HTTP call failed, not which agent under whose authority attempted which write and why it was refused, and they tend to collapse the chain of intent across many tool calls (LoginRadius). Agent activity needs structured telemetry that treats the agent as a first-class identity.

A rejected-write record should carry at least these fields:

FieldExampleWhy it matters
eventfs.write_rejectedStable name for filtering and alerts
agent_idagent-42Attributes the attempt to a specific non-human identity
session_ids-9f3aGroups the attempt with the rest of the run
correlation_idreq-7781Ties the attempt back to the originating user request
path/repo/.envThe exact target
reasonoutside_allowlistDistinguishes policy rejection from a runtime error
policy_versionv12Tells you which config was live at the time
timestampRFC 3339Orders the trail across hosts

The reason field deserves care. outside_allowlist, read_only_mount and agent_self_denied are different signals. The first two mean your boundary worked; the third means the agent's own check stopped something the environment would also have stopped, which is a hint that the config and the mounts have drifted apart.

Correlation IDs are not optional in multi-step runs. Without one, reconstructing what happened after an incident means guessing, because the model's path through the tools is not deterministic and a replay will not reproduce it (LoginRadius). The correlation ID is the thread you pull.

Build the escalation path so the limit does not block real work

A boundary with no exit turns into a workaround. If the agent hits the limit on legitimate work every day, someone will widen the mount to / and the whole exercise is undone. Plan the escalation before you need it.

The cleanest escalation is a pull request. The agent writes to its own branch, and anything that needs to reach main goes through review by a human or by a review agent. Anthropic's Code Review feature, for example, runs multiple agents in parallel against pull request diffs and posts ranked findings as comments, which is a reasonable model for what a review step looks like (tessl on AI code review agents). The important property is not who reviews, it is that the merge is a separate, revocable step.

For data changes, the escalation is a human running a versioned migration, not the agent editing a live schema. Database migration tooling exists precisely to make schema change repeatable, reviewable and version-controlled, and the argument that AI should generate migrations while the tool remains the safety rail applies directly here (Doran Gao on migrations). The reversibility rule explains why: a live schema change is not undoable with one command, so it cannot be inside the agent's write scope.

Two practical thresholds keep the escalation honest. First, count rejections per week and segment them by reason and path prefix. A steady stream of outside_allowlist rejections on one prefix usually means the boundary is drawn in the wrong place, not that the agent is misbehaving. Second, track how often an escalation is approved. If nearly all of them are approved, the write probably belongs inside the limit; if almost none are, the agent is attempting work it should not.

FAQ

FAQ

What exactly counts as a blast radius limit for file access?

It is the explicit set of directories or git refs an agent is allowed to write to, expressed as an allowlist and enforced below the agent so that anything outside the set fails. The limit is not a prompt instruction; it is a mount flag, sandbox profile or syscall filter plus a runtime config that agrees with it. The test of a good limit is that you can name the single command that undoes any allowed write.

Should I use a directory boundary or a git branch boundary?

Use a directory boundary when the agent produces artifacts such as generated code, exports or scratch analysis, and a branch boundary when it changes source under version control. Many setups need both, with read-only access to the checkout and write access to a single throwaway branch. The deciding question is always the undo command: rm or git branch -D means inside, a database restore or cloud rollback means outside.

Why is a read-only mount better than a permission check in the agent?

A check inside the agent framework runs in the same process that can be influenced by a prompt injection or a bug. A read-only mount is enforced by the kernel before the write reaches storage, and the copy-on-write overlay approach used by AgentFS shows why mount-level control is more reliable than inode permission bits when the agent acts on your behalf (AgentFS). The config still matters, but it stops the attempt rather than the write.

What should a rejected-write log entry contain?

At minimum: event name, agent identity, session ID, correlation ID, target path, rejection reason, policy version and timestamp. The correlation ID is what lets you reconstruct a run after an incident, because agent behavior is non-deterministic and replaying the same prompt will not reproduce the path (LoginRadius). Free-text logs are not enough; the fields need to be queryable.

How do I get rejected writes out of the logs and into a decision?

Review rejection counts weekly, segmented by reason and path prefix, and compare them against escalation approvals. A high rejection rate on one path prefix usually means the boundary is misplaced, while a high approval rate on escalations means that write belongs inside the limit. If you would rather have a specialist review the permission model and the runtime config with you, a free 30-minute AI discovery call is a reasonable place to start.

Sources

Sources

  1. Rogue AI Agents: Why Autonomous Agents Are Becoming ...
  2. Auditing and Logging AI Agent Activity: A Guide for Engineers
  3. AI Agent Permissions: Least Privilege and Sandboxing
  4. AgentFS How to Stop AI Agents from Messing with Your Files
  5. How to Give Your AI Agent a Filesystem (Safely): Docker ...
  6. Claude Code's new AI code review agents scan pull ...
  7. Database Migrations in the AI Era: Why Schema Foundations Still Matter
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles