paperclipai/paperclip · critical · Error

Refusing to activate non-directory payload ${payloadPath}.

Error message

Refusing to activate non-directory payload ${payloadPath}.

What it means

Thrown by assertPayloadPath() when the payload path passed the in-store containment check but the entry at that path is not a real directory — it is a regular file or a symbolic link (lstatSync reports isSymbolicLink or !isDirectory). flipCurrentAtomic creates a directory symlink, so the target must be a real directory; a file or symlink payload could break activation or be a vector for confusion.

Source

Thrown at cli/src/install-store.ts:254

): 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);
  ensurePrivateDirectory(paths.cliRoot);
  try {
    const currentStat = fs.lstatSync(paths.currentPath);
    if (!currentStat.isSymbolicLink()) {

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Inspect the payload path: 'ls -la <payloadPath>' and confirm it is a directory containing the unpacked package (node_modules/paperclipai/dist).
  2. If it is a file or symlink, remove it and reinstall the payload so it is unpacked as a real directory.
  3. If a symlink was intentionally placed, remove it and let the installer create a real payload directory.
  4. Verify the identifier passed to payloadPathFor matches an actually-unpacked payload, not a downloaded archive.

Example fix

$ ls -la ~/.paperclip/cli/installs/npm/1.2.3
-rw-r--r--  1.2.3  (regular file, e.g. a tarball)
$ rm ~/.paperclip/cli/installs/npm/1.2.3
$ paperclipai install   # unpacks payload as real directory
Defensive patterns

Strategy: validation

Validate before calling

import fs from "node:fs";

function isRealPayloadDirectory(payloadPath: string): boolean {
  try {
    const st = fs.lstatSync(payloadPath);
    return st.isDirectory() && !st.isSymbolicLink();
  } catch { return false; }
}

// Before flipCurrentAtomic:
if (!isRealPayloadDirectory(payload)) throw new Error("payload is not a real directory");

Type guard

import fs from "node:fs";

function isRealDirectory(p: string): boolean {
  const st = fs.lstatSync(p);
  return st.isDirectory() && !st.isSymbolicLink();
}

Try / catch

try {
  flipCurrentAtomic(payloadPath, paths);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Refusing to activate non-directory payload")) {
    // payload extraction failed; reinstall the payload
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: Called flipCurrentAtomic(payloadPath, paths) where payloadPath is inside installsRoot but fs.lstatSync(payloadPath) shows it is a file or a symlink rather than a real directory.

Common situations: 1) The install payload extraction failed, leaving a file (e.g. a tarball) instead of an unpacked directory. 2) A symlink was placed at the payload path. 3) The payload directory was replaced with a file after install. 4) Wrong identifier passed such that it resolves to a file artifact.

Related errors


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