paperclipai/paperclip · error · Error
Refusing to activate payload that resolves outside ${paths.i
Error message
Refusing to activate payload that resolves outside ${paths.installsRoot}. What it means
Thrown by assertPayloadPath inside flipCurrentAtomic when the payload directory's real filesystem path (resolved via fs.realpathSync) does not start with the real path of installsRoot. This is the third layer of defense in a three-stage path-traversal check: lexical containment, directory-type verification, and symlink-resolved containment. It catches cases where a symlink inside the installs root points outside it, defeating the earlier lexical check.
Source
Thrown at cli/src/install-store.ts:259
fs.renameSync(temporaryPath, paths.manifestPath);
} finally {
fs.rmSync(temporaryPath, { force: true });
}
}
function assertPayloadPath(payloadPath: string, paths: InstallStorePaths): void {
const relative = path.relative(paths.installsRoot, path.resolve(payloadPath));
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`Refusing to activate payload outside ${paths.installsRoot}.`);
}
const stat = fs.lstatSync(payloadPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) {
throw new Error(`Refusing to activate non-directory payload ${payloadPath}.`);
}
const installsRealPath = fs.realpathSync(paths.installsRoot);
const payloadRealPath = fs.realpathSync(payloadPath);
if (!payloadRealPath.startsWith(`${installsRealPath}${path.sep}`)) {
throw new Error(`Refusing to activate payload that resolves outside ${paths.installsRoot}.`);
}
}
export function flipCurrentAtomic(
payloadPath: string,
paths = resolveInstallStorePaths(),
hooks: { beforeRename?: () => void } = {},
): void {
assertPayloadPath(payloadPath, paths);
ensurePrivateDirectory(paths.cliRoot);
try {
const currentStat = fs.lstatSync(paths.currentPath);
if (!currentStat.isSymbolicLink()) {
throw new Error(`Refusing to replace non-symlink ${paths.currentPath}.`);
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}View on GitHub (pinned to 67001ec6eb)
Solutions
- Inspect the payload path with 'readlink -f <payloadPath>' and compare it against 'readlink -f <paths.installsRoot>' to find which symlink escapes the root.
- Remove or fix the offending symlink so the payload directory genuinely lives under installsRoot.
- If the installs root itself is a symlink or bind mount, make paths.installsRoot point at the real path or remove the indirection.
- Re-run the install from scratch: remove the install store and let the installer recreate the payload directory natively.
Example fix
// before: payload is a symlink escaping installsRoot
// installsRoot/npm/canary -> /tmp/some-other-dir
// after: real directory under installsRoot
fs.rmSync(payloadPath); // remove the symlink
fs.mkdirSync(payloadPath, { recursive: true }); // create real directory
// re-extract/install the payload into the real directory Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
import path from 'node:path';
function validatePayloadPathSafe(payloadPath: string, installsRoot: string): boolean {
try {
const installsReal = fs.realpathSync(installsRoot);
const payloadReal = fs.realpathSync(payloadPath);
return payloadReal.startsWith(`${installsReal}${path.sep}`);
} catch {
return false;
}
}
// Call before flipCurrentAtomic:
if (!validatePayloadPathSafe(payloadPath, paths.installsRoot)) {
throw new Error('Payload realpath escapes installs root; fix symlinks before activating.');
} Try / catch
try {
flipCurrentAtomic(payloadPath, paths);
} catch (error) {
if (error instanceof Error && error.message.includes('resolves outside')) {
// Symlink resolution failure: inspect realpaths, fix the symlink chain
console.error('Payload symlink escapes installs root:', fs.realpathSync(payloadPath));
}
throw error;
} Prevention
- Never create symlinks inside the installs root that point outside it.
- Always use payloadPathFor() to compute payload paths—it constrains identifiers to [A-Za-z0-9._-] and joins under installsRoot.
- After installing a payload, verify with 'readlink -f' that its real path is inside the installs root.
- Avoid bind-mounting or symlink-mounting the installs directory tree.
When it happens
Trigger: Calling flipCurrentAtomic(payloadPath, paths) where payloadPath is a directory that contains, or is reached through, a symlink chain that resolves outside paths.installsRoot. For example, installsRoot/npm/canary is a symlink to /tmp/evil, or the installsRoot itself is a bind-mount/symlink whose real path differs from its lexical path.
Common situations: A previous install was created with a symlinked payload, or the installs directory tree was manually rearranged or symlinked to save disk space. A user or tool moved installsRoot and left a symlink in its place. Cross-filesystem bind mounts where realpath differs from the expected path.
Related errors
- Refusing to remove install store with an invalid manifest at
- Refusing to activate payload outside ${paths.installsRoot}.
- Refusing to prune unsafe install-store path ${sourceRoot}.
- Refusing to use unsafe shim directory ${directoryPath}.
- Refusing to write export file outside output directory: ${re
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/09d4c3fdf8ea224c.
Report an issue: GitHub.