CherryHQ/cherry-studio · error · Error

Path is outside the workspace: ${userPath}

Error message

Path is outside the workspace: ${userPath}

What it means

Thrown by resolveWorkspaceFile() as a security containment check. After resolving both workspaceRoot and the requested path through realpath (which follows symlinks), it verifies the target's real path starts with realRoot + path.sep. If it does not, the path — after symlink resolution — escapes the workspace boundary. This is defense-in-depth against traversal mistakes and prompt-injection path selection, not a sandbox against code-executing agents.

Source

Thrown at src/main/ai/channels/security/WorkspaceFileGuard.ts:49

    // waste retries on other paths.
    if (isErrnoException(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
      throw new Error(`Session workspace is unavailable: ${workspaceRoot}`)
    }
    throw error
  }

  let realTarget: string
  try {
    realTarget = await realpath(requested)
  } catch (error) {
    if (isErrnoException(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
      throw new Error(`File not found in workspace: ${userPath}`)
    }
    throw error
  }

  if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep)) {
    throw new Error(`Path is outside the workspace: ${userPath}`)
  }

  return readCanonicalLocalFile(requested, realTarget, userPath)
}

View on GitHub (pinned to 726446b54c)

Solutions

  1. Use only paths relative to the workspace root without '../' sequences.
  2. If the path legitimately needs to be outside the workspace, use resolveLocalFile() instead (which does no containment check) — but understand this removes the security boundary.
  3. Audit workspace symlinks: remove or reject symlinks that point outside the workspace root.
Defensive patterns

Strategy: validation

Validate before calling

import path from 'node:path'

function isPathWithinWorkspace(workspaceRoot: string, userPath: string): boolean {
  const resolved = path.resolve(workspaceRoot, userPath)
  return resolved === workspaceRoot || resolved.startsWith(workspaceRoot + path.sep)
}

if (!isPathWithinWorkspace(workspaceRoot, userPath)) {
  throw new Error(`Path '${userPath}' escapes the workspace — use a workspace-relative path without '../'`)
}

Try / catch

try {
  return await resolveWorkspaceFile(workspaceRoot, userPath)
} catch (error) {
  if (error instanceof Error && error.message.startsWith('Path is outside the workspace:')) {
    // Security violation — log and reject, do not retry with alternative paths
    logger.warn('Path escape attempt blocked', { userPath, workspaceRoot })
    throw error
  }
  throw error
}

Prevention

When it happens

Trigger: Called when an agent supplies a path containing '../' sequences that resolve outside the workspace, or when a symlink inside the workspace points to a target outside the workspace. The check fires after realpath resolves everything, so even an obfuscated path like a symlink chain that eventually escapes will be caught.

Common situations: An agent following a prompt-injection instruction tried to read '../../../etc/passwd' or an absolute path outside the workspace; a symlink in the workspace points to /etc or the user's home directory; the agent was given a workspace-adjacent path that resolves to a sibling directory outside the root.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/eae43e674d19dbad. Report an issue: GitHub.