Agent File System Access: Designing the Blast Radius
The filesystem is an agent's most consequential tool surface. Path containment, read and write asymmetry, and why files are an injection vector.
Give an agent three tools and it can do useful work: read a file, write a file, run a command. Two of those three touch the filesystem, and between them they account for most of the damage an agent is capable of doing and most of the context it wastes.
Filesystem access is worth designing rather than exposing. The questions are what the agent can see, what it can change, whether the changes are reversible, and what happens when a file it reads contains instructions.
Containment is a resolve-then-check problem
The obvious implementation rejects paths containing .. and calls it done. That is not containment, it is a filter, and filters on unresolved paths lose.
Do it properly: resolve the requested path to an absolute canonical form — following symlinks — and then check that the result is inside the permitted root. Reject if not. Order matters, because a symlink inside the workspace pointing at a home directory resolves outward without ever containing a dot-dot.
Apply the check to every path argument of every tool, including the ones you think are read-only, and including any path embedded in a command you execute. A root directory the agent cannot escape is the single most valuable property of the whole design. Agent sandboxing covers the layers underneath this one.
Reads and writes deserve different rules
A wrong read costs tokens. A wrong write costs work. Treating them symmetrically overprotects one and underprotects the other.
Reads want a wide permitted area and tight output limits: the agent benefits from being able to look at anything in the repository, and suffers immediately if a single read returns forty thousand tokens. Range parameters, line limits and truncation notices belong here. Tool result formatting covers shaping that output.
Writes want a narrow permitted area and a record of what changed. Restricting writes to the working tree while allowing reads across the wider project is a sensible asymmetry, and it costs the agent almost nothing in capability.
Hide what the agent should not read
An exclusion list serves two purposes at once, and the second is the one people forget.
The security purpose is obvious: .env files, credential stores, private keys and anything under a secrets directory should not be readable, because whatever the agent reads goes into a prompt and travels to an API. Redaction at the tool boundary is far more reliable than instructing the model not to look.
The context purpose is that most of a repository is noise. Dependency directories, build output, lock files and minified bundles are enormous and almost never useful. Excluding them from search results stops the agent drowning in matches from vendored code, which improves behaviour measurably.
Make exclusions visible rather than silent. A search that says it skipped a directory teaches the agent something; one that silently returns nothing leaves it confused about why an obvious file cannot be found.
Files are an injection surface
Anything the agent reads becomes prompt content, and the agent cannot reliably tell the difference between your instructions and text that looks like instructions inside a file.
A comment in a dependency, a line in a README, a string in test fixture data — any of these can contain something shaped like a directive. If your agent reads from a repository that accepts external contributions, or processes files a user supplied, this is a live concern rather than a theoretical one.
The mitigations are structural. Wrap file content in an explicit delimiter and state in the system prompt that content inside it is data, never instruction. Keep the genuinely dangerous capabilities behind approval so a successful injection cannot immediately act. And do not assume a model will resist — assume it may not, and bound what follows. Prompt injection and agent security covers the attack in detail.
Make writes reversible
The best undo layer for a coding agent already exists in the repository. Commit or stash before the agent starts, and every change it makes is recoverable with a command you already know.
Outside a repository, take an explicit snapshot of any file before overwriting it. This is cheap and it converts a destructive mistake into an inconvenience, which is the whole point.
Deletes and moves deserve their own treatment. A delete tool is rarely necessary — moving to a quarantine directory achieves the same thing for the agent and keeps the file. If you do expose deletion, it belongs behind an approval gate. Human-in-the-loop design covers placing those gates where a person can actually judge them.
Whole-file writes versus targeted edits
A tool that overwrites an entire file is simple to implement and expensive to use: the model must reproduce the whole file to change three lines, which costs output tokens and introduces the chance of it quietly dropping something it did not think was important.
A targeted edit tool — replace this exact string with that one — is cheaper and safer, because the blast radius of an error is the region matched rather than the whole file. It fails more often, and it fails loudly, which is the better trade.
Make the failure informative. "Search text not found in src/app.ts; the file has changed since it was read" tells the agent exactly what to do next. Whole-file writes should stay available for genuinely new files, where there is nothing to match against. Tool schema design covers writing these tools well.
A working checklist
Resolve every path and check it against a root. Give reads a wide area with hard output limits and writes a narrow one with a record. Exclude secrets and dependency noise, visibly. Delimit file content as data. Take a restore point before the agent starts. Prefer targeted edits, gate deletes.
Then watch the traces for one number in particular: total tokens read per session. It is usually the largest single component of agent spend, it is usually dominated by a handful of oversized reads, and it is usually fixable in an afternoon. Agent loop anatomy covers why that number compounds across turns.
Common questions
How should path containment be implemented?
Resolve the path to an absolute canonical form following symlinks, then check the result is inside the permitted root. Filtering for dot-dot on the raw string misses symlinks that resolve outside the workspace.
Why exclude directories the agent could safely read?
Because dependency directories, build output and minified bundles are enormous and almost never useful. Excluding them keeps searches from drowning in vendored matches, which improves agent behaviour as much as any prompt change.
Are files really a prompt injection risk?
Yes. Anything read becomes prompt content, and a comment in a dependency or a string in fixture data can be shaped like an instruction. Delimit file content as data and keep dangerous capabilities behind approval.