can1357/oh-my-pi · error · Error
Refusing to download outside the workspace: ${downloadPath}
Error message
Refusing to download outside the workspace: ${downloadPath} What it means
Resource download paths declared by MCP frames are user-authoritative, and resolveToCwd deliberately honors absolute paths and '..'. To prevent a remote server frame from writing anywhere the process can reach, the handler confines the path to the workspace with confineToWorkspace; if the path escapes it, the download is refused. This is a security boundary against path traversal.
Source
Thrown at packages/coding-agent/src/cursor.ts:825
const texts = textItems.map(item => item.text as string);
const blobItem = read.contents.find(item => item.blob !== undefined);
const blob = blobItem?.blob;
const textMimeType = textItems[0]?.mimeType;
const blobMimeType = blobItem?.mimeType;
if (downloadPath) {
// Text resources download as their own bytes; a blob decodes first.
const payload =
texts.length > 0 ? texts.join("\n") : blob !== undefined ? Buffer.from(blob, "base64") : undefined;
if (payload === undefined) return null;
// The path is workspace-relative BY CONTRACT, but it arrives from the
// server, and `resolveToCwd` deliberately honors absolute paths and
// `..` for user-authored tool input. Taking it at its word would let a
// frame write anywhere this process can reach, so confine it here
// rather than trusting the declaration.
const cwd = this.options.getCwd?.() ?? this.options.cwd;
const absolutePath = confineToWorkspace(downloadPath, cwd);
if (!absolutePath) throw new Error(`Refusing to download outside the workspace: ${downloadPath}`);
await writeWithoutFollowingLinks(absolutePath, payload);
// The path echoed back is the one the frame asked for; the model
// addresses it the same relative way.
return { uri, mimeType: texts.length > 0 ? textMimeType : blobMimeType, downloadPath };
}
if (texts.length > 0) return { uri, mimeType: textMimeType, text: texts.join("\n") };
if (blob === undefined) return null;
return { uri, mimeType: blobMimeType, blob: Buffer.from(blob, "base64") };
}
/**
* Settle a completed native Cursor todo call, mirroring its list when the
* server supplied an authoritative one.
*
* Cursor's snapshot is a flat list, so tasks already known locally keep
* their phase and only their status is updated; unknown tasks land in a
* single fallback phase. Statuses come straight from the server snapshot —View on GitHub (pinned to 9690622007)
Solutions
- Use a path that resolves inside the workspace (relative path without leading '..' escape).
- If the file legitimately belongs outside the workspace, start the session with cwd at (or above) the intended target directory.
- Strip or sanitize the server-provided path in your MCP client wrapper; only pass user-approved relative names.
- If traversal came from a third-party MCP server, treat it as untrusted behavior and restrict or drop that server.
Example fix
// before: escapes the workspace
{ downloadPath: "../../etc/hosts.dl" }
// after: confined to workspace
{ downloadPath: "downloads/hosts.dl" } Defensive patterns
Strategy: validation
Validate before calling
import path from "node:path";
function staysInWorkspace(p: string, cwd: string): boolean {
const abs = path.resolve(cwd, p);
return abs === cwd || abs.startsWith(cwd + path.sep);
}
if (!staysInWorkspace(downloadPath, cwd)) {
// rewrite or reject the path before calling readMcpResource
} Try / catch
try {
return await readMcpResource({ server, uri, downloadPath });
} catch (e) {
if (String(e.message).startsWith("Refusing to download outside the workspace")) {
// fall back to a workspace-relative filename derived from the resource URI
return readMcpResource({ server, uri, downloadPath: safeWorkspaceName(uri) });
} else throw e;
} Prevention
- Treat server-supplied download paths as untrusted; never pass absolute paths from frames through unmodified.
- Sanitize '..' segments and absolute prefixes before issuing a download.
- Run sessions with cwd set to the tree you want downloads confined to.
- Audit third-party MCP servers for resource paths that attempt traversal.
When it happens
Trigger: A downloadPath that resolves outside the session working directory — an absolute path elsewhere on disk, or a relative path containing enough '..' segments to escape the workspace (classic path traversal, possibly from a malicious or misconfigured MCP server frame).
Common situations: An MCP server returns a resource with a suggested absolute save path (/tmp/... or C:\...); a crafted resource URI includes ../ traversal; the user runs the agent from a narrower cwd than the file's real location.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Destination paths cannot contain parent traversal or NUL byt
- Shared-folder destination escapes its configured root
- ENXIO
- Refusing to download onto a non-regular file: ${absolutePath
- Refusing to download onto a file with ${stat.nlink} hard lin
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/89ac00a74b9fb5e4.
Report an issue: GitHub.