paperclipai/paperclip · critical · Error
Refusing to activate payload outside ${paths.installsRoot}.
Error message
Refusing to activate payload outside ${paths.installsRoot}. What it means
Thrown by assertPayloadPath() (used by flipCurrentAtomic) when the payload path, resolved and made relative to installsRoot, is empty, starts with '..', or is absolute. This prevents activating (symlinking 'current' to) a payload that lives outside the managed installs directory, blocking path-traversal attacks that could point the CLI entrypoint at arbitrary code.
Source
Thrown at cli/src/install-store.ts:250
export function writeInstallManifestAtomic(
manifest: InstallManifest,
paths = resolveInstallStorePaths(),
): void {
ensurePrivateDirectory(paths.cliRoot);
const temporaryPath = `${paths.manifestPath}.tmp-${process.pid}-${Date.now()}`;
try {
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
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);View on GitHub (pinned to 67001ec6eb)
Solutions
- Ensure payloadPath is always derived from payloadPathFor(paths, source, id) which produces a path under installsRoot, rather than constructed from raw input.
- If constructing manually, verify path.resolve(payloadPath) starts with path.resolve(installsRoot) + path.sep before calling flipCurrentAtomic.
- If the manifest's payloadPath is wrong, remove the store and reinstall.
- Never pass user-supplied absolute paths to flipCurrentAtomic.
Example fix
// before
flipCurrentAtomic("/opt/suspicious/payload", paths); // throws
// after
const id = "1.2.3";
const payloadPath = payloadPathFor(paths, "npm", id); // under installsRoot
flipCurrentAtomic(payloadPath, paths); Defensive patterns
Strategy: validation
Validate before calling
import path from "node:path";
import { resolveInstallStorePaths } from "./install-store.js";
function payloadPathWithinInstalls(payloadPath: string, paths = resolveInstallStorePaths()): boolean {
const rel = path.relative(paths.installsRoot, path.resolve(payloadPath));
return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
}
// Before flipCurrentAtomic:
if (!payloadPathWithinInstalls(payload, paths)) throw new Error("payload outside installs root"); Type guard
import path from "node:path";
function isWithinBase(base: string, target: string): boolean {
const rel = path.relative(base, path.resolve(target));
return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
} Try / catch
try {
flipCurrentAtomic(payloadPath, paths);
} catch (err) {
if (err instanceof Error && err.message.startsWith("Refusing to activate payload outside")) {
// payload path was constructed incorrectly; recompute via payloadPathFor
throw err;
}
throw err;
} Prevention
- Always derive payload paths from payloadPathFor(paths, source, id) rather than raw input.
- Never accept user-supplied absolute paths for payload activation.
- Validate path containment before passing to flipCurrentAtomic.
When it happens
Trigger: Called flipCurrentAtomic(payloadPath, paths) where payloadPath resolves outside paths.installsRoot — e.g. an absolute path like '/opt/evil', a relative escape like '../../../tmp/x', or empty. assertPayloadPath computes path.relative and detects the escape before any symlink is created.
Common situations: 1) A caller computed payloadPath from untrusted input without constraining it under installsRoot. 2) Manifest corruption pointing payloadPath outside the store. 3) A bug in install orchestration passing the wrong base path. 4. Tampering attempt to redirect the 'current' symlink at arbitrary code.
Related errors
- Refusing to use non-directory install-store path ${directory
- Refusing to use unsafe install-store marker ${paths.markerPa
- Refusing to remove unsafe install-store path ${paths.cliRoot
- Refusing to remove install store with an invalid manifest at
- Refusing to activate payload that resolves outside ${paths.i
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/a0cb49fdf02131ac.
Report an issue: GitHub.