paperclipai/paperclip · error
Writable sandbox path "${normalizedExtraPath}" is outside sy
Error message
Writable sandbox path "${normalizedExtraPath}" is outside synchronized workspace "${workspaceDir}" and has no outbound restore mapping. What it means
Thrown in the filesystemScope="workspace" branch when an extraPath with access "rw" sits outside workspaceDir AND is not covered by any outboundRestorePaths entry. The sandbox only syncs workspaceDir back to the host; a writable bind-mount outside it would silently drop changes on container teardown. The library refuses to start rather than discard writes the caller might depend on.
Source
Thrown at packages/adapter-utils/src/local-process-sandbox.ts:373
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.`,
);
}
}
}
const bwrapCommand = input.options.command?.trim() || "bwrap";
const args = ["--die-with-parent", "--new-session", "--unshare-pid", "--unshare-ipc", "--unshare-uts"];
const env: Record<string, string | undefined> = {};
let cleanup: (() => Promise<void>) | undefined;
let executable = input.executable;
let executableArgs = input.args;
if (filesystemScope === "workspace") {
args.push("--tmpfs", "/", "--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp");
args.push(
"--symlink", "usr/bin", "/bin",
"--symlink", "usr/sbin", "/sbin",View on GitHub (pinned to 67001ec6eb)
Solutions
- If the writable path is supposed to sync back, add its parent to outboundRestorePaths so the restore step recovers it.
- If the writable path is disposable (cache, scratch), move it inside workspaceDir so it is synchronized automatically.
- If you do not need writes, change access to "ro" — read-only paths can sit outside the workspace without a restore mapping.
- Verify outboundRestorePaths entries with path.relative(restorePath, extraPath) in a debugger — the restore check uses non-"..", non-absolute relatives, so a restore path of /tmp covers /tmp/cache but not /var/cache.
Example fix
// before
buildLocalProcessSandboxSpawnTarget({
...input,
options: {
...input.options,
filesystemScope: "workspace",
workspaceDir: "/srv/work",
extraPaths: [{ path: "/tmp/build-cache", access: "rw" }],
outboundRestorePaths: [],
},
});
// after: cache lives inside the synced workspace
buildLocalProcessSandboxSpawnTarget({
...input,
options: {
...input.options,
filesystemScope: "workspace",
workspaceDir: "/srv/work",
extraPaths: [{ path: "/srv/work/.cache", access: "rw" }],
outboundRestorePaths: [],
},
}); Defensive patterns
Strategy: validation
Validate before calling
function assertWritablePathsRestored(opts: {
workspaceDir: string;
extraPaths: { path: string; access: string }[];
outboundRestorePaths: string[];
}): void {
for (const extra of opts.extraPaths) {
if (extra.access !== "rw") continue;
const rel = path.relative(opts.workspaceDir, extra.path);
const inWorkspace = !rel.startsWith("..") && !path.isAbsolute(rel);
const restored = opts.outboundRestorePaths.some((p) => {
const r = path.relative(p, extra.path);
return !r.startsWith("..") && !path.isAbsolute(r);
});
if (!inWorkspace && !restored) {
throw new Error(`Writable path ${extra.path} outside workspace and not in outboundRestorePaths`);
}
}
} Try / catch
try {
return await buildLocalProcessSandboxSpawnTarget(input);
} catch (error) {
if (error instanceof Error && error.message.includes("no outbound restore mapping")) {
input.options.outboundRestorePaths = [...(input.options.outboundRestorePaths ?? []), path.dirname(offendingPath)];
return buildLocalProcessSandboxSpawnTarget(input);
}
throw error;
} Prevention
- Keep writable paths inside workspaceDir whenever possible so they auto-sync.
- Treat outboundRestorePaths as the explicit opt-in for out-of-workspace writes; document each entry.
- Switch disposable caches to access "ro" if writes are not actually needed, or move them under workspaceDir.
When it happens
Trigger: extraPaths: [{ path: "/tmp/cache", access: "rw" }] with workspaceDir: "/srv/work" and no outboundRestorePaths covering /tmp/cache. The check at local-process-sandbox.ts:363-377 iterates only rw entries; ro entries can sit outside the workspace because they cannot diverge from the host.
Common situations: Adapters that bind-mount cache directories, build output dirs, or home-directory dotfiles as rw inside the sandbox without telling Paperclip how to recover their state. Also hit when outboundRestorePaths is populated but the rw path is not nested under any of them (typo, wrong path, missing trailing slash semantics).
Related errors
- Sandbox cwd "${cwd}" must be inside workspaceDir "${workspac
- 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/2e3984508ee0bf44.
Report an issue: GitHub.