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
- Set cwd to a path inside workspaceDir before calling buildLocalProcessSandboxSpawnTarget — typically cwd === workspaceDir is the safe choice.
- If the process genuinely needs to run from a parent directory, expand workspaceDir to that parent so cwd falls inside it.
- Resolve symlinks on both paths (fs.realpath) before comparison so an in-workspace symlink that points outside is not silently rejected.
- 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
- Default cwd to workspaceDir at the adapter boundary.
- Resolve symlinks on both paths before comparison so an in-workspace link that points outside is not silently rejected.
- Reconcile workspaceDir and cwd at config load: validate path.relative(workspaceDir, cwd) does not start with "..".
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
- Writable sandbox path "${normalizedExtraPath}" is outside sy
- Sandbox path alias "${aliasPath}" must target the synchroniz
- filesystemScope must be "workspace".
- Sandbox path alias target "${aliasTarget}" does not exist.
- filesystemExtraPaths[${index}] must be an absolute path or {
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/99eac738bd3f5179.
Report an issue: GitHub.