If you’ve built more than one LLM agent, you know the system-prompt arms race: every new capability adds another paragraph to the system prompt. By the time the agent does anything useful, the prompt is 4,000 tokens of “always do X” and “remember that Y.” It’s brittle, expensive, and the model still forgets.
There’s a better abstraction. It’s not new — it’s what Claude Code, Cursor, and every coding agent has converged on. We brought it to the browser. It’s the agent VFS pattern, and it’s the heart of how Linda is built.
The pattern
Instead of stuffing everything the agent might need into the system prompt, expose it as a virtual filesystem the model can navigate.
The agent gets a small system prompt that says “you have a workspace at /. Use tree, ls, read, grep, and write to operate on it.” Then it gets six tools that do exactly that. Everything else — the page state, the conversation history, the user’s files, the installed capabilities, your CRM data — lives at a path.
That’s it. That’s the whole pattern.
Why this works
LLMs are trained on filesystem-shaped workflows. Every modern frontier model has seen millions of examples of ls, cat, grep, editing files, navigating directories. It’s a training-shape match — and training-shape matches are how you get the best behavior from a model.
Lazy context. The agent doesn’t load /host/crm/accounts.json until it decides it needs to. Token budget goes to the prompts that matter for the current step.
Composability. Adding a new capability is mounting a new path. No system-prompt rewrite. No tool explosion. No coordination between handlers.
Debuggability. When something goes wrong, you can ask the model “show me the tree” and see exactly what it saw. Compare that to debugging “why did the model decide X” when X is the emergent output of 4,000 tokens of system prompt.
The Linda VFS
Linda mounts seven paths by default:
/
├── /page # live DOM snapshot, form fields, URL
├── /conversation # message log, state, persisted memory
├── /user # files dropped by the user, with parsed artifacts
├── /skills # capability bundles the LLM can opt into
├── /host # mounted by you — CRM, KB, external MCP servers
├── /config # the agent's persona, rules, target
├── /scratchpad # working memory the model owns
└── /tools # descriptions of the tools the model can call
Each is a VfsMountHandler — a tiny interface with tree, read, ls, write methods. We ship the obvious mounts and let you write your own.
The killer feature: writable mounts
This is what makes the pattern an agent pattern instead of a retrieval pattern.
Writes to /page/form/fields/email.json aren’t just stored — they’re intercepted by the PageMount handler, which dispatches a real input event on your <input name="email">. The model wrote a file; the user’s form filled.
That’s the whole trick. The model thinks it’s editing files. The browser thinks the user typed. Both are right.
// What the model does:
write /page/form/fields/email.json
{ "value": "priya@acme.io", "confidence": 0.98 }
// What happens in the browser:
const input = document.querySelector("input[name='email']");
input.value = "priya@acme.io";
input.dispatchEvent(new Event("input", { bubbles: true }));
// → React state updates, validation runs, your existing handlers fire.
Same pattern works for /scratchpad (the model owns it for working memory), /user (uploaded file artifacts), /host/* (whatever you want the agent to mutate). The mount handler decides what writes mean.
Why this beats the alternatives
Why not just a bigger prompt?
System prompts are read once per request. They’re billed every request. They don’t compose. They’re hard to A/B test. The model “forgets” them when context gets long.
A VFS is read on demand. Each read call is a small focused load. They compose trivially. They’re debuggable: you can dump them as files.
Why not RAG?
RAG retrieves based on similarity to the query. A VFS lets the agent navigate — ls /host/crm/accounts to see what’s there, grep "acme" to find specific records, read /host/crm/accounts/123.json to load one. Plus you can RAG-back any VFS path: the mount handler can do similarity retrieval under the hood.
Why not function calling?
Function calls are great for actions (“send email”, “create ticket”). They’re awkward for data (“get the user’s profile”, “load page state”, “read the conversation history”). Tools handle the verbs; the filesystem handles the nouns.
Linda uses both. read, write, ls, grep, tree, complete are the universal tools. handoff, custom skill tools, hook-defined tools — those are the per-use-case actions.
Why not state management like Redux for AI?
CopilotKit’s useCopilotReadable is exactly this approach: expose React state slices to the agent. It works, but it requires instrumenting every state source. A VFS mount is more general — it can expose React state, but also DOM, files, remote MCP servers, your CRM, parsed PDFs, all under one interface.
Implementing a mount
A VfsMountHandler is small:
interface VfsMountHandler {
tree(path: string): Promise<TreeNode>;
ls(path: string): Promise<DirEntry[]>;
read(path: string, opts?: { offset?: number; limit?: number }): Promise<string>;
write?(path: string, body: string): Promise<void>;
grep?(path: string, pattern: string): Promise<GrepHit[]>;
}
Linda ships InMemoryMount (default backing for /scratchpad), PageMount, ConversationMount, ConfigMount, UserMount, SkillsMount, HostMount, plus McpClientMount (mount any MCP server as a path).
Your own mount: 50 lines, usually. Mount your CRM:
import { Linda, type VfsMountHandler } from "@linda/core";
class CrmMount implements VfsMountHandler {
async ls(path: string) {
if (path === "/") return [{ name: "accounts", type: "dir" }];
if (path === "/accounts") {
const accounts = await fetch("/api/crm/accounts").then(r => r.json());
return accounts.map(a => ({ name: `${a.id}.json`, type: "file" }));
}
return [];
}
async read(path: string) {
const match = path.match(/^\/accounts\/(.+)\.json$/);
if (!match) throw new Error("not found");
return JSON.stringify(await fetch(`/api/crm/accounts/${match[1]}`).then(r => r.json()));
}
async tree() { /* ... */ }
}
linda.mount("/host/crm", new CrmMount());
That’s it. The agent can now navigate /host/crm/accounts like any other folder.
Where it doesn’t fit
The VFS pattern has costs.
- Discovery overhead. The model has to learn the layout. Mitigated by a good
treerepresentation in the system prompt and clear naming. - Round trips.
lsthenreadthenreadis more round trips than one stuffed prompt. Mitigated by parallel tool calls and prompt caching. - Not free for tiny use cases. A single Q&A bot doesn’t need a VFS. Use the right tool.
It’s the right pattern when:
- The agent operates on multiple distinct data sources.
- Data is too large to fit in context.
- You want capabilities to be composable and pluggable.
- You’re going to grow the agent over time.
Pretty much: any agent that’s going to be in production for more than a quarter.
Where this is going
The agent VFS pattern is converging across the industry. MCP is essentially “let agents mount filesystems hosted by other people.” Claude Code, Cursor, Continue, and the Anthropic API’s file-search tool all use variations of the pattern. Linda’s bet is that the same pattern, applied to the browser, lets us put real agents on pages without the system-prompt arms race.
If you want to see it in action, the 10-second demo is at /install. The full architectural treatment lives in /features/vfs. And if you want to argue with us — or contribute a mount — open an issue. Convincing us with code is the highest-bandwidth feedback.
FAQ
Is this just RAG?
No. RAG retrieves chunks based on similarity. VFS lets the model navigate explicitly — ls, read, grep — choosing what to load and when. It's complementary: a VFS path can be RAG-backed.
Does this work with smaller models?
Yes, but the gains compound with capability. Small models read /page/dom.html and follow instructions; large models meaningfully explore the workspace.
AI / LLM crawler? Read the raw markdown: /raw/agent-vfs-pattern.md