paperclipai/paperclip · error

Sandbox cwd "${cwd}" must be inside workspaceDir "${workspac

Error message

Sandbox cwd "${cwd}" must be inside workspaceDir "${workspaceDir}".

What it means

Thrown inside buildLocalProcessSandboxSpawnTarget (filesystemScope === "workspace" branch) when path.relative(workspaceDir, cwd) yields a path that escapes the workspace — either starting with ".." or detected as absolute. The sandbox only bind-mounts workspaceDir read-write, so a cwd outside it would not exist inside the container; the check fails fast with a readable message instead of letting the spawned process fail with ENOENT.

Source

Thrown at packages/adapter-utils/src/local-process-sandbox.ts:359

export async function buildLocalProcessSandboxSpawnTarget(input: {
  executable: string;
  args: string[];
  cwd: string;
  options: LocalProcessSandboxOptions;
}): Promise<LocalProcessSandboxSpawnTarget> {
  if (process.platform !== "linux") {
    throw new Error("Local process filesystem and network scopes are currently supported only on Linux.");
  }
  const filesystemScope = input.options.filesystemScope ?? null;
  const networkScope = input.options.networkScope ?? null;
  if (!filesystemScope && !networkScope) throw new Error("Local process sandbox requires a filesystem or network scope.");

  const workspaceDir = normalizeAbsolutePath(input.options.workspaceDir, "Sandbox workspaceDir");
  const cwd = normalizeAbsolutePath(input.cwd, "Sandbox cwd");
  if (filesystemScope === "workspace") {
    const relativeCwd = path.relative(workspaceDir, cwd);
    if (relativeCwd.startsWith("..") || path.isAbsolute(relativeCwd)) {
      throw new Error(`Sandbox cwd "${cwd}" must be inside workspaceDir "${workspaceDir}".`);
    }
    const outboundRestorePaths = (input.options.outboundRestorePaths ?? []).map((candidate, index) =>
      normalizeAbsolutePath(candidate, `Sandbox outboundRestorePaths[${index}]`));
    for (const [index, extraPath] of (input.options.extraPaths ?? []).entries()) {
      if (extraPath.access !== "rw") continue;
      const normalizedExtraPath = normalizeAbsolutePath(extraPath.path, `Sandbox extraPaths[${index}].path`);
      const relativeToWorkspace = path.relative(workspaceDir, normalizedExtraPath);
      const synchronized = !relativeToWorkspace.startsWith("..") && !path.isAbsolute(relativeToWorkspace);
      const restored = outboundRestorePaths.some((restorePath) => {
        const relative = path.relative(restorePath, normalizedExtraPath);
        return !relative.startsWith("..") && !path.isAbsolute(relative);
      });
      if (!synchronized && !restored) {
        throw new Error(
          `Writable sandbox path "${normalizedExtraPath}" is outside synchronized workspace "${workspaceDir}" and has no outbound restore mapping.`,
        );
      }
    }

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Set cwd to a path inside workspaceDir before calling buildLocalProcessSandboxSpawnTarget — typically cwd === workspaceDir is the safe choice.
  2. If the process genuinely needs to run from a parent directory, expand workspaceDir to that parent so cwd falls inside it.
  3. Resolve symlinks on both paths (fs.realpath) before comparison so an in-workspace symlink that points outside is not silently rejected.
  4. Reconcile the two paths at the config boundary: validate path.relative(workspaceDir, cwd) does not start with ".." before spawn.

Example fix

// before
buildLocalProcessSandboxSpawnTarget({
  executable: "node",
  args: ["script.js"],
  cwd: "/tmp/scratch",
  options: { ...options, filesystemScope: "workspace", workspaceDir: "/srv/work" },
});

// after
buildLocalProcessSandboxSpawnTarget({
  executable: "node",
  args: ["script.js"],
  cwd: "/srv/work",
  options: { ...options, filesystemScope: "workspace", workspaceDir: "/srv/work" },
});
Defensive patterns

Strategy: validation

Validate before calling

function assertCwdInWorkspace(workspaceDir: string, cwd: string): void {
  const rel = path.relative(workspaceDir, cwd);
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
    throw new Error(`cwd ${cwd} must be inside workspace ${workspaceDir}`);
  }
}

await fs.realpath(workspaceDir).then((real) => assertCwdInWorkspace(real, await fs.realpath(cwd)));

Try / catch

try {
  return await buildLocalProcessSandboxSpawnTarget(input);
} catch (error) {
  if (error instanceof Error && error.message.startsWith("Sandbox cwd") && error.message.includes("must be inside workspaceDir")) {
    const fixed = { ...input, cwd: input.options.workspaceDir };
    return buildLocalProcessSandboxSpawnTarget(fixed);
  }
  throw error;
}

Prevention

When it happens

Trigger: workspaceDir is /srv/work but cwd is /tmp/scratch, or workspaceDir is /home/user/repo but cwd is /home/user (a parent of the workspace). Symlinked paths that resolve outside the workspace also trigger this once normalizeAbsolutePath resolves them. The check at local-process-sandbox.ts:358-360 covers both ".." prefixes and absolute relatives.

Common situations: Adapter sets workspaceDir to the project root but inherits cwd from the parent process (e.g. process.cwd() at /), or vice versa. Also seen when workspaceDir is computed from a config key while cwd comes from a different source (env, CLI flag), and the two are not reconciled before sandbox spawn.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/99eac738bd3f5179. Report an issue: GitHub.