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

  1. Use a path that resolves inside the workspace (relative path without leading '..' escape).
  2. If the file legitimately belongs outside the workspace, start the session with cwd at (or above) the intended target directory.
  3. Strip or sanitize the server-provided path in your MCP client wrapper; only pass user-approved relative names.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/89ac00a74b9fb5e4. Report an issue: GitHub.