[GH-ISSUE #544] LocalShellBackend writes to filesystem root when agent emits absolute paths (fix: virtualMode: true) #275

Closed
opened 2026-06-05 17:21:25 -04:00 by yindo · 1 comment
Owner

Originally created by @shreyas-lyzr on GitHub (May 19, 2026).
Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/544

Problem

When a ComputerAgent is configured with LocalShellBackend({ rootDir: workdir }) (the recommended setup for containerized runs), LLM-generated write_file calls that use absolute paths (e.g. "/output.txt") silently bypass the rootDir and try to write to the real filesystem root, causing EROFS: read-only file system on macOS and silent sandbox escape on Linux.

Root causeFilesystemBackend.resolvePath() with virtualMode: false (the default):

resolvePath(key) {
    // virtualMode = false branch:
    return path.resolve(this.cwd, key);  // ← path.resolve IGNORES this.cwd when key is absolute
}

path.resolve("/home/work", "/output.txt") returns "/output.txt" — not "/home/work/output.txt". The rootDir is silently discarded for any path that starts with /.

This is the natural shape that Anthropic/Claude models emit: they're trained to think in absolute paths, so they produce /notes.txt, /workdir/output.txt, etc. without prompting.

Reproduction

import { createDeepAgent, LocalShellBackend } from "deepagents";
const backend = new LocalShellBackend({ rootDir: "/tmp/sandbox" });
await backend.initialize();
// … agent chat: "Create a file /test.txt with content hello"
// → Error writing file '/test.txt': EROFS: read-only file system, open '/test.txt'
// OR silently creates /test.txt on Linux (outside the intended sandbox)

Fix (one line)

Enable virtualMode: true when constructing LocalShellBackend for any sandboxed workdir:

const backend = new LocalShellBackend({ rootDir: ctx.workdir, virtualMode: true });

With virtualMode: true, FilesystemBackend.resolvePath() strips the leading /, resolves relative to cwd, and rejects traversal — exactly the correct sandbox semantics:

// virtualMode = true branch:
const vpath = key.startsWith("/") ? key : "/" + key;
if (vpath.includes("..") || vpath.startsWith("~")) throw new Error("Path traversal not allowed");
const full = path.resolve(this.cwd, vpath.substring(1));  // ← strips leading /

Verified behavior after fix

Agent path virtualMode: false virtualMode: true
"/output.txt" EROFS / escape <rootDir>/output.txt
"notes/scratch.txt" <rootDir>/notes/scratch.txt <rootDir>/notes/scratch.txt
"../escape.txt" <rootDir>/../escape.txt (escapes!) Path traversal not allowed

Suggested change

In the deepagents source, wherever LocalShellBackend is instantiated with a user-supplied rootDir for agent tool use, set virtualMode: true. The virtualMode flag already exists precisely for this use case — the fix is just that it isn't the default.

Alternatively, consider making virtualMode: true the default when rootDir is explicitly specified, since "you set a root, absolute paths should be virtual" is almost always the intended behavior.

Context

Discovered while integrating ComputerAgent (https://github.com/open-gitagent/ComputerAgent) with Lyzr Studio as an alternate LLM backend. The virtualMode: true escape hatch is what made the fix possible — thank you for building it in. The only issue is that it's opt-in rather than on-by-default.

deepagents@1.10.2.

Originally created by @shreyas-lyzr on GitHub (May 19, 2026). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/544 ## Problem When a ComputerAgent is configured with `LocalShellBackend({ rootDir: workdir })` (the recommended setup for containerized runs), LLM-generated `write_file` calls that use absolute paths (e.g. `"/output.txt"`) silently bypass the `rootDir` and try to write to the **real filesystem root**, causing `EROFS: read-only file system` on macOS and silent sandbox escape on Linux. **Root cause** — `FilesystemBackend.resolvePath()` with `virtualMode: false` (the default): ```js resolvePath(key) { // virtualMode = false branch: return path.resolve(this.cwd, key); // ← path.resolve IGNORES this.cwd when key is absolute } ``` `path.resolve("/home/work", "/output.txt")` returns `"/output.txt"` — not `"/home/work/output.txt"`. The rootDir is silently discarded for any path that starts with `/`. This is the natural shape that Anthropic/Claude models emit: they're trained to think in absolute paths, so they produce `/notes.txt`, `/workdir/output.txt`, etc. without prompting. ## Reproduction ```ts import { createDeepAgent, LocalShellBackend } from "deepagents"; const backend = new LocalShellBackend({ rootDir: "/tmp/sandbox" }); await backend.initialize(); // … agent chat: "Create a file /test.txt with content hello" // → Error writing file '/test.txt': EROFS: read-only file system, open '/test.txt' // OR silently creates /test.txt on Linux (outside the intended sandbox) ``` ## Fix (one line) Enable `virtualMode: true` when constructing `LocalShellBackend` for any sandboxed workdir: ```ts const backend = new LocalShellBackend({ rootDir: ctx.workdir, virtualMode: true }); ``` With `virtualMode: true`, `FilesystemBackend.resolvePath()` strips the leading `/`, resolves relative to `cwd`, and rejects traversal — exactly the correct sandbox semantics: ```js // virtualMode = true branch: const vpath = key.startsWith("/") ? key : "/" + key; if (vpath.includes("..") || vpath.startsWith("~")) throw new Error("Path traversal not allowed"); const full = path.resolve(this.cwd, vpath.substring(1)); // ← strips leading / ``` ## Verified behavior after fix | Agent path | `virtualMode: false` | `virtualMode: true` | |---|---|---| | `"/output.txt"` | EROFS / escape | `<rootDir>/output.txt` ✓ | | `"notes/scratch.txt"` | `<rootDir>/notes/scratch.txt` ✓ | `<rootDir>/notes/scratch.txt` ✓ | | `"../escape.txt"` | `<rootDir>/../escape.txt` (escapes!) | `Path traversal not allowed` ✓ | ## Suggested change In the `deepagents` source, wherever `LocalShellBackend` is instantiated with a user-supplied `rootDir` for agent tool use, set `virtualMode: true`. The `virtualMode` flag already exists precisely for this use case — the fix is just that it isn't the default. Alternatively, consider making `virtualMode: true` the default when `rootDir` is explicitly specified, since "you set a root, absolute paths should be virtual" is almost always the intended behavior. ## Context Discovered while integrating ComputerAgent (https://github.com/open-gitagent/ComputerAgent) with Lyzr Studio as an alternate LLM backend. The `virtualMode: true` escape hatch is what made the fix possible — thank you for building it in. The only issue is that it's opt-in rather than on-by-default. `deepagents@1.10.2`.
yindo closed this issue 2026-06-05 17:21:25 -04:00
Author
Owner

@hntrl commented on GitHub (Jun 2, 2026):

Thanks for the writeup @shreyas-lyzr!

Agree this is probably the correct behavior, but we’re not planning to flip the default globally right now since its a breaking change. For sandbox-style path semantics, the supported/ideal configuration is:

new LocalShellBackend({ rootDir: workdir, virtualMode: true })

I'm going to add some slight jsdoc updates in #577 to hopefully make this more explicit

<!-- gh-comment-id:4607401406 --> @hntrl commented on GitHub (Jun 2, 2026): Thanks for the writeup @shreyas-lyzr! Agree this is probably the correct behavior, but we’re not planning to flip the default globally right now since its a breaking change. For sandbox-style path semantics, the supported/ideal configuration is: ```ts new LocalShellBackend({ rootDir: workdir, virtualMode: true }) ``` I'm going to add some slight jsdoc updates in #577 to hopefully make this more explicit
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#275