Skip to main content

LangGraph Deep Agents Filesystem: Pluggable Backends

· 9 min read
Vadim Nicolai
Senior Software Engineer

Every long-running agent needs somewhere to put things that do not fit in the next prompt: a draft, a search dump, a plan it will come back to at step 80. The LangGraph deep agents virtual filesystem is that place. The agent sees one set of file tools; underneath, a pluggable backend decides where the bytes actually live. The thesis of this post is simple: the filesystem is the agent's working memory, and the backend decides whether that memory survives the thread. Pick the backend by asking how long each file needs to live, not by what is easiest to wire up.

LangGraph Deep Agents, a 10-part series. ← Part 3 · Part 5 →

Everything below comes from the public Deep Agents backends documentation and the Deep Agents overview.

What tools does the LangGraph deep agents virtual filesystem give the agent?​

The harness hands the model seven file tools:

  • ls lists a directory.
  • read_file reads a file.
  • write_file creates a file.
  • edit_file changes part of an existing file.
  • delete removes a file.
  • glob matches paths by pattern.
  • grep searches file contents.

Sandbox backends and LocalShellBackend add an eighth, execute, which is a different trust model and the subject of part 05.

Two details matter more than they look. First, the tool surface is the same whichever backend you choose. A prompt that says "write your notes to /notes/plan.md" works unchanged whether that file ends up in graph state, on disk or in a database. That is the point of the abstraction: you change storage without retraining the agent's habits.

Second, reads are multimodal. Per the backends docs, read_file returns image files (.png, .jpg, .jpeg, .gif, .webp) as multimodal content blocks on all backends. An agent that saved a screenshot or a chart can open it later and actually look at it, rather than receiving a base64 string it cannot interpret.

Which backend should a LangGraph deep agent use?​

All built-in backends import from deepagents.backends. Each one answers the lifetime question differently.

StateBackend: scoped to the thread (the default)​

StateBackend is what you get when you pass nothing. Files live in LangGraph state, scoped to one thread, and persist across turns through the checkpointer. That is the right default for scratch work: intermediate notes, tool output the agent wants to re-read, a todo file. When the thread ends, the files are effectively gone for every other conversation. Nothing leaks, and nothing touches your disk.

FilesystemBackend: real files on disk​

FilesystemBackend(root_dir=..., virtual_mode=...) reads and writes real files under root_dir, which the docs require to be an absolute path. This is how you point an agent at a repository or a working directory.

The flag to remember is virtual_mode. The docs warn that virtual_mode=False provides no security and recommend virtual_mode=True, which blocks .., ~ and absolute paths outside the root. Treat True as mandatory unless you have a specific reason and another boundary around the process.

LocalShellBackend goes further and adds unrestricted shell execution on the host. The docs call it unsafe for production. If you need execute, use a sandbox instead.

StoreBackend: durable across threads​

StoreBackend(namespace=...) writes through a LangGraph store, so files outlive the thread. This is where long-term memory goes: user preferences, learned conventions, anything the agent should remember next week.

The namespace argument is a factory that receives the runtime and returns a tuple of strings. Common keys are the user identity, the assistant_id and the thread_id, and wildcards are rejected. The namespace is your isolation boundary. Key it by user and one user's memories can never surface in another user's session. The docs note that namespace becomes required in v0.5.0, so set it explicitly now.

There is also ContextHubBackend, which stores files in a LangSmith Context Hub repo. It needs a LangSmith API key in the environment, and its upload_files() accepts UTF-8 text only.

How does CompositeBackend route paths?​

Real agents rarely want one lifetime for everything. You want scratch files to vanish and memories to stick. CompositeBackend does that by path prefix: a default backend plus a routes mapping from prefixes to backends. This is the example from the docs:

from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
from langgraph.store.memory import InMemoryStore

agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
backend=CompositeBackend(
default=StateBackend(),
routes={
"/memories/": StoreBackend(namespace=lambda _rt: ("memories",)),
},
),
store=InMemoryStore(), # Store passed to create_deep_agent, not backend
)

Anything under /memories/ goes to the store and survives the thread. Everything else stays in thread state. Note where the store goes: it is passed to create_deep_agent, not to the backend. In production, swap InMemoryStore for a persistent store and key the namespace by user rather than the constant ("memories",).

The routing rules are worth knowing:

  • Longer prefixes win. A /memories/shared/ route beats /memories/ for paths under it.
  • Listings keep the original prefixes. ls, glob and grep return the full virtual paths, so the agent never sees the seams between backends.
  • Internal data goes to the default. Offloaded large tool results (/large_tool_results/) and conversation history (/conversation_history/) land on the default backend.

That last rule drives the docs' main recommendation. If you want an agent to work on disk, don't make FilesystemBackend the whole backend. Wrap it in a CompositeBackend with StateBackend as the default and route only your project prefix to disk. Otherwise the harness's own artifacts, such as offloaded tool output and history, get written into your working tree.

Can you control what the filesystem middleware does?​

The file tools come from FilesystemMiddleware. One documented knob is offload_binary_content=True (it requires deepagents>=0.7.21), which stores blobs under /blobs/<sha256> by default or under artifacts_root if you set it. It has no effect when blobs route to StateBackend, so pair it with a durable route.

Restricting the file tools with an allowlist​

Since deepagents>=0.7, FilesystemMiddleware takes a tools allowlist. Pass your own instance through middleware= and any built-in file tool left off the list disappears from the model's tool list. This is the documented read-only agent:

from deepagents import create_deep_agent
from deepagents.middleware import FilesystemMiddleware

# Read-only agent: write_file, edit_file, delete, and execute are never shown
agent = create_deep_agent(
model="claude-sonnet-4-6",
middleware=[
FilesystemMiddleware(backend=backend, tools=["read_file", "ls", "glob", "grep"]),
],
)

Three rules come with it. read_file must always be in the list; leaving it out raises ValueError when the agent is created. execute and delete are dropped anyway whenever the backend does not support them, whatever the list says. And custom tools passed through create_deep_agent's own tools= argument are never touched by the allowlist. Your instance replaces the default one for the main agent, and the general-purpose subagent inherits the restriction; declarative subagents do not, so give each its own FilesystemMiddleware(tools=...).

For hiding all file tools from the model, use a harness profile with excluded_tools (part 03). For blocking specific paths or operations, the docs describe FilesystemPermission rules (operations, paths, mode="deny") that are checked before the backend is called (part 05). Keep the two ideas apart. Hiding a tool saves context. Denying a path is the actual security control.

Writing your own backend​

If none of the built-ins fit, for example because your files belong in object storage or an internal document service, subclass BackendProtocol and implement ls, read, write, edit, glob and grep. delete is optional. Subclass SandboxBackendProtocol if you also need execute. The one convention to follow: return error fields instead of raising exceptions, so the model gets a readable failure it can recover from rather than a crashed run.

The docs also mark some older patterns as deprecated: passing backend factory callables, the runtime constructor argument, files_update on results, and Command wrapping in middleware. If an older tutorial uses backend=lambda rt: StateBackend(rt), pass an instance instead.

A rule of thumb for choosing​

  • Scratch work for one conversation: StateBackend, the default.
  • Facts the agent should remember across conversations: route a prefix like /memories/ to StoreBackend, namespaced by user.
  • Editing a real codebase: route the project prefix to FilesystemBackend with virtual_mode=True, inside a CompositeBackend that defaults to state.
  • Running commands: a sandbox backend, not LocalShellBackend.

The model's tools stay the same in every case. What you are really choosing is how long each path lives.

FAQ​

Where does a deep agent store files by default? In LangGraph state, through StateBackend. Files are scoped to the thread and persist across turns via the checkpointer, but other threads cannot see them.

How do I give a LangGraph deep agent long-term memory across threads? Use a CompositeBackend that routes a prefix such as /memories/ to StoreBackend, and pass a store to create_deep_agent. Key the namespace by user so memories stay isolated.

Is FilesystemBackend safe to point at my machine? Only with virtual_mode=True, which blocks .., ~ and absolute paths outside root_dir. With virtual_mode=False the docs say there is no security. Shell access through LocalShellBackend is not production-safe.


Series: 01 Harness · 02 Quickstart · 03 Tools and MCP · 04 Filesystem · 05 Sandbox and permissions · 06 Skills and memory · 07 Context engineering · 08 Subagents · 09 Human-in-the-loop · 10 Production