Skip to main content

LangGraph Deep Agents Sandbox: Permissions and Execute

· 9 min read
Vadim Nicolai
Senior Software Engineer

A LangGraph deep agent can list, read, write, edit and delete files from the moment you create it, and with the right backend it can also run shell commands. That is the point of the harness, and it is also the risk. Deep Agents gives you three controls: declarative permissions= rules, sandbox backends that add an execute tool, and a QuickJS interpreter that adds eval. Each one guards a different surface. This post's argument: permissions guard the file tools, not the shell. Once execute exists, the sandbox is the boundary. If you mix those up, you can end up with an agent that obeys every deny rule and still prints your .env from the shell.

LangGraph Deep Agents, a 10-part series. ← Part 4 · Part 6 →

Picking up from part 04 on pluggable backends, this part covers what you put in front of those backends.

What do LangGraph Deep Agents permissions actually cover?​

The permissions docs give the scope precisely. Rules apply only to the built-in filesystem tools: ls, read_file, glob, grep, write_file, edit_file and delete. "read" covers the first four and "write" covers the last three. Custom tools and MCP tools that touch the filesystem are not covered. Permissions also do not apply to sandbox backends, because those can run arbitrary commands through execute. Permissions need deepagents>=0.5.2.

Each rule is a FilesystemPermission with three fields:

  • operations: ["read"], ["write"] or both.
  • paths: glob patterns. ** matches recursively and {a,b} alternates.
  • mode: "allow" (the default), "deny", or "interrupt" (needs deepagents>=0.6.8 and a checkpointer). With "interrupt", a matching write pauses for human approval instead of running, and you resume it through the human-in-the-loop flow.

How does first-match-wins evaluation work in a deep agent?​

Rules are checked in the order you declare them. The first rule whose operations and paths match the call decides the outcome. If no rule matches, the call is allowed. That permissive default is the thing to remember: a rule list without a trailing catch-all deny is an allowlist in name only.

So order runs from most specific to least specific. Here is the docs' pattern for keeping an agent inside a workspace while hiding its secrets file:

from deepagents import FilesystemPermission, create_deep_agent

agent = create_deep_agent(
model=model,
backend=backend,
permissions=[
# 1. Most specific first: the secrets file is never readable or writable
FilesystemPermission(
operations=["read", "write"],
paths=["/workspace/.env"],
mode="deny",
),
# 2. Everything else in the workspace is fair game
FilesystemPermission(
operations=["read", "write"],
paths=["/workspace/**"],
mode="allow",
),
# 3. Catch-all: nothing outside the workspace
FilesystemPermission(
operations=["read", "write"],
paths=["/**"],
mode="deny",
),
],
)

Swap rules 1 and 2 and you get the bug the docs call out: /workspace/** matches .env first, so the deny is never reached. The configuration raises no error. Its only symptom is a secret showing up in a tool result.

Deletes need extra care. Deleting a directory is all-or-nothing: delete checks write on the target and every descendant, and refuses the whole operation if any one of them is denied. Deleting a single file uses ordinary first-match-wins, so a narrow earlier allow beats a later catch-all deny (on deepagents>=0.7.3).

How do you narrow permissions for a subagent?​

Subagents inherit the parent's rules by default. If you set permissions in a subagent spec (next to its system_prompt), that list replaces the parent's rules entirely. It is not merged with them. That makes a read-only reviewer straightforward:

subagents=[
{
"name": "reviewer",
"description": "Read-only code reviewer",
"system_prompt": "Review the code for issues.",
"permissions": [
FilesystemPermission(operations=["write"], paths=["/**"], mode="deny"),
FilesystemPermission(operations=["read"], paths=["/workspace/**"], mode="allow"),
FilesystemPermission(operations=["read"], paths=["/**"], mode="deny"),
],
}
]

Because the list replaces the parent's, you have to restate every deny you still want. If the parent hid /workspace/.env and the subagent's list doesn't mention it, the subagent can read it. Part 08 covers subagents in depth.

When does a LangGraph deep agent sandbox become the boundary?​

The sandboxes page describes a simple architecture. A provider has to implement only one method, execute(), which runs a shell command and returns its output. BaseSandbox builds read, write, edit, delete, ls, glob and grep on top of it. The execute tool is conditional: on every model call the harness checks whether the backend implements SandboxBackendProtocol, and if it doesn't, the tool is filtered out and the agent never sees it.

That is why permissions stop mattering once a sandbox is the backend. A deny rule on read_file("/workspace/.env") does nothing about execute("cat /workspace/.env"). The docs enforce this for composite backends: when the default backend is a sandbox, every permission path has to sit under a known route prefix, such as /memories/**. A path that falls through to the sandbox default, including /**, raises NotImplementedError when you build the agent. The library is telling you that this rule can't be enforced.

A sandbox protects your host. The agent can't read your local files, see your environment variables or touch your processes. The docs are just as clear about what it does not stop:

  • Context injection. Anyone who controls part of the input can tell the agent to run commands inside the sandbox, where it has full control.
  • Network exfiltration. Unless network access is blocked, data can leave over HTTP or DNS.

Hence the docs' hardest rule: never put secrets inside a sandbox. Keep credentials in tools that run on the host, or use a proxy that injects credentials if your provider has one. Treat everything the sandbox produces as untrusted input.

What are backend policy hooks for?​

Path globs can't express rate limits, audit logs or content inspection. For those, the backends docs recommend subclassing or wrapping a backend: for example, a FilesystemBackend subclass whose write and edit return a WriteResult(error=...) or EditResult(error=...) for denied prefixes. Hooks run inside the backend, so they also apply to custom tools that go through it. The permissions= list cannot do that. Use permissions= for path allow/deny rules and hooks for anything that needs logic.

Is the QuickJS eval interpreter a sandbox?​

The interpreters docs add a third surface. Install deepagents[quickjs] and pass middleware=[CodeInterpreterMiddleware()] to create_deep_agent, and the agent gets an eval tool that runs JavaScript in QuickJS compiled to WebAssembly. By default that code has no filesystem, network, shell, package manager or clock access. It can compute, keep state and log to the console. Each eval call has a 5-second timeout and a 64 MB memory_limit by default, and each LangGraph thread gets its own WebAssembly instance.

The docs call this "a scoped interpreter runtime, not a full production sandbox backend." The WebAssembly sandbox contains the engine, not the capabilities you hand it. Programmatic tool calling, enabled with CodeInterpreterMiddleware(ptc=["web_search"]), lets interpreter code call every tool on that allowlist, so the PTC allowlist is a permission boundary. Keep it narrow, and keep wasmtime updated, since a wasmtime bug is the remaining way out of the WebAssembly sandbox. For hostile multi-tenant workloads, the docs also recommend running agents in isolated workers or containers.

Which control blocks what?​

  • permissions= blocks the built-in file tools by path, read or write, first match wins, allowed by default. It has no effect on execute, custom tools or MCP tools.
  • Sandbox backend keeps the agent away from your host. Inside the sandbox the agent has full control, so the sandbox's contents, network access and secrets are the real exposure.
  • Policy hooks apply custom logic to every operation that goes through the backend.
  • QuickJS eval gives pure computation with nothing attached. What it can reach is exactly what its PTC allowlist bridges in.

Decide on the boundary first. With no execute, write permissions and end with a catch-all deny. With execute, design the sandbox as if the agent can already read everything in it, because it can. For approval flows built on mode="interrupt", see part 09.

FAQ​

Do LangGraph Deep Agents permissions apply to the execute tool? No. The docs say permissions apply only to the built-in filesystem tools and not to sandbox backends. Once execute is available, the sandbox's isolation is your control.

What happens if no permission rule matches? The operation is allowed. End every rule list with a /** deny if you want deny-by-default.

Can I give a subagent stricter permissions than its parent? Yes. Set permissions in the subagent spec. It replaces the parent's rules completely, so restate any deny the subagent should keep.


Series: 01 Harness · 02 Quickstart · 03 MCP Tools · 04 Filesystem · 05 Sandbox & Permissions · 06 Skills & Memory · 07 Context · 08 Subagents · 09 Human-in-the-Loop · 10 Production