The File System as a Tool: Underrated Agent Infrastructure
Four hundred thousand rows won't fit in a context window; a path and a preview cost nothing. A scoped directory solves large results, durable state, and reviewable artifacts at once — if you resolve paths correctly.
Give an agent a scoped directory it can read and write, and several problems that seem to need infrastructure stop needing it. Large intermediate results, artifacts a human will review, state that survives a restart, and data passed between steps all have an obvious home that isn't the context window.
It's the least glamorous tool in the box and among the most useful.
What it solves
Large results stop entering the context. A tool that produces a big output writes it to a file and returns a path plus a summary. The agent knows the data exists, knows its shape, and reads back only what it needs.
export_transactions(q3) -> {path: "run/exports/q3.csv",
rows: 412882, columns: [...],
preview: "first 5 rows..."}
Four hundred thousand rows would blow any context. A path and a preview cost nothing, and the agent can then run a query or a script against the file.
Work products become real artifacts. A generated report, a converted dataset, a patch — these are things a human wants to open, diff, and keep. A file is the natural form, and returning one through the conversation loses formatting, size, and the ability to use ordinary tools on it.
State survives. Files persist across turns, across compaction, and across process restarts. Notes, plans, and partial results in files are recoverable when the transcript isn't.
Ordinary tools become available. If the agent also has code execution, the file system is the interchange: write data, run a script over it, read the summary. Every data-processing library becomes reachable without wrapping any of it as a tool.
The tool set
Small and mostly obvious, with a couple of choices that matter:
read_file(path, offset?, limit?) # ranged reads are essential
write_file(path, content)
list_dir(path)
file_info(path) -> {size, lines, modified}
Ranged reads are not optional. Without offset and limit, an agent asked to look at a large file has one move: read all of it into context. With them, it can look at the head, sample the middle, or read a specific region. The same applies to any search tool over files — return matching lines with a little surrounding context, never whole files.
file_info before reading lets the agent decide whether a file is worth opening at all. A model that can see a file is fifty megabytes will handle it differently from one that discovers this by trying to read it.
Scoping it
The security model is the whole design.
✅ A directory per run. runs/{run_id}/ and nothing above it. Path resolution happens in the handler, and anything resolving outside the root is refused — after resolving symlinks and .., not before.
✅ Read-only mounts for inputs. Source data the agent may read and must not modify, mounted separately from its writable scratch space.
✅ Quotas. Total bytes and file count per run. An agent in a loop will fill a disk, and it won't be malicious about it.
✅ A defined lifetime. Run directories are cleaned up on a schedule. Without this you accumulate scratch data with no owner and no retention policy — which becomes a compliance question later.
⚠️ The path-traversal check is the one to get right, because it's the difference between a scoped scratch directory and access to the host filesystem. Resolve the absolute path, then verify it's under the root. Never do string prefix matching on the unresolved path.
Prompting for it
As with any scratchpad, the tools go unused unless the prompt says when to reach for them. The instruction that works: write intermediate results to files when they're large; return paths rather than contents; read back only the parts you need.
Also worth stating explicitly: files persist between turns. Models don't assume this, and an agent that re-generates something it already wrote is a common and avoidable waste.
🔍 What to log
- Every read and write with path and size.
- Total bytes written per run — a runaway is visible here first.
- Any refused path resolution. Should be zero; anything else is either a bug or an attempt, and both are worth knowing about.
The takeaway
A scoped directory turns the context window from the only workspace into just the conversation. Large results become paths, work products become artifacts a human can open, state survives restarts, and code execution gains an interchange format. Keep it to one directory per run, resolve paths properly before checking them, set quotas and a cleanup schedule, and give the agent ranged reads so a big file doesn't have to become a big context.