In Linux everything is a file. Or at least, most of the system is exposed as one.
Devices, running processes, network state, kernel state: much of it appears through filesystem-like interfaces that you can read and write using the same small set of commands.
/proc/cpuinfo isn’t a file sitting on disk anywhere, but you can cat it just like anything else.
That uniformity made the system composable. It enabled combinations of simple utils that nobody specifically needed to design for. It also means you can discover things without knowing exactly where they are in advance.
Windows went in the other direction.
A lot of configuration lives in the Registry, a structured database accessed through dedicated APIs and tools rather than ordinary filesystem operations.
This is a perfectly reasonable design for a desktop OS built primarily for people using graphical interfaces. To inspect or change information, you generally need to know which interface or operation was designed for it.
Neither design is wrong.
Systems built for a specific purpose let users focus their effort on the task at hand.
Agents are a new kind of user, and they are not a person with a mouse. They can drive a graphical UI with a combination of taking screenshots, deciding between ambiguous targets and catching errors from whatever pops up on the screen.
This is slow and inefficient enough that even browser agents increasingly avoid working through the browser GUI when they can inspect the structured state or interact with the DOM directly.
Aside from model intelligence, the environment determines what an agent can actually do. Limited tools mean limited actions, even with the best model available. With the right environments we can already see how capable the models are.
The way many agent platforms are being built today is by gradually exposing product features as tools, one by one.
Even well-designed tools with progressive disclosure suffer from a version of the same problem Windows would have for agents: the model needs to understand not only the business requirements, but also which tools exist, how to discover them, the limitations of each tool and which specific tools it needs to combine for a particular job.
Tools are custom built, take JSON in, spit JSON out. If an edgecase falls outside of what the tools were designed for, the Agent will start to go on a journey trying to stitch together toolcalls, or is simply unable to fulfil the request.
So we approached the problem from a different perspective.
We engineered the platform to be accessible entirely through a terminal by representing product state and actions through a filesystem interface.
As far as our agents are concerned our entire platform is files.
So who gets the terminal?
Does that mean our customers review invoices by SSHing into a server?
Obviously not. Nobody wants to cd into a directory to see whether an invoice cleared and no finance manager should ever see a shell prompt.
Most platforms effectively pick a side.
Build for humans, and agents get tools duct-taped on.
Build for agents, and users get forced into the shell. But why pick?
We have two interfaces over the same underlying state: a web application for people, and a Linux-style filesystem for agents.
Humans get concepts familiar to humans. Agents get concepts familiar to agents.
Take our reconciliation table. In the web app it’s a table with rows, review panel, approvers, commenters, data and reference traces. In the agent’s world, every row is a directory:
/reconcile/invoice-818/
summary.md
input/
subject_header.eml
invoice.pdf
comments/
2026-03-04-johnsmith/
comment.md
attachment.pdf
2026-03-06-approver.md
/reconcile/invoice-881/
summary.md
input/
another_subject_header.eml
invoice.pdf
comments/
2026-03-04-johndoe.md
2026-03-06-approver.md
references/
vendor-statement.csvNow a user asks: why was this flagged as a duplicate?
There’s no extra endpoint for that question. There is no explain_duplicate_flag tool.
The agent doesn’t need one. It reads the comments, follows the attachment, and notices the evidence doesn’t match the row that was flagged. It checks the neighboring records and finds the one that actually does.
You aren’t building tools for every possible question. You should build an environment where the answers to questions you haven’t anticipated are already lying around.
And to be clear about what the filesystem is: it’s a projection of your platform state, not a copy of it. Both interfaces read and write the same state.
The obvious objection
A couple of things about this probably sound wrong.
A filesystem doesn’t give you an application-level schema or type system by default. And in a multi-tenant system, giving an agent a shell sounds like a data leak waiting to happen.
It may sound like we’re suggesting taking a product state, dumping it into files in a VM somewhere, and rebuilding a web app on top of that.
With two sources of truth there’s going to be synchronization problems forever.
These problems only arise if the filesystem is a storage layer. But it’s not, it’s an interface.
We use FUSE: Filesystem in Userspace (our implementation is called FakeFUSE; that’s probably a post of its own). FUSE registers a mount point with the kernel and routes every VFS operation against it (open, read, readdir, write, stat,etc.) out of the kernel and into callbacks in a userspace process. When something touches the mount, the kernel doesn’t consult a block device; it dispatches the syscall to our daemon, which computes the response at request time and hands the bytes back. To the caller it’s indistinguishable from a real file. (This is the same thing that procfs does: /proc/cpuinfo has no backing store, the kernel synthesizes it per read. FUSE just lets you implement that pattern in userspace, against your own domain.)
So when the agent runs:
cat /reconcile/invoice-818/summary.mdthere isn’t a summary.md sitting on disk waiting to be fetched. The request lands in our read handler which then calls the same underlying application service that powers the web app, then renders the result as Markdown instead of DOM.
And the card in the browser isn’t sitting on the disk either. Both interfaces are projections of the same underlying state, generated at request time. They differ only in how that state is presented.
Writes work the same way in reverse:
echo approved > statuslands in a write handler, which calls the same service method as the Approve button in the web app.
If the row isn’t in a state where approval is legal, the write fails. The filesystem doesn’t bypass the application’s rules any more than the web UI does. An agent can attempt an invalid transition but can’t perform one.
That’s the architecture: one state model, two interfaces.
Is this the final answer for agent interfaces? Probably not. FUSE has real costs, and rendering application state as files requires opinionated choices, especially for data that doesn’t map cleanly onto a filesystem. But six months in, our agents answer questions and take actions we never designed for, using little more than coreutils, and we haven’t written a bespoke tool since.

