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

  1. Inspect the payload path with 'readlink -f <payloadPath>' and compare it against 'readlink -f <paths.installsRoot>' to find which symlink escapes the root.
  2. Remove or fix the offending symlink so the payload directory genuinely lives under installsRoot.
  3. If the installs root itself is a symlink or bind mount, make paths.installsRoot point at the real path or remove the indirection.
  4. 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

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


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