paperclipai/paperclip · critical · Error

Refusing to use non-directory install-store path ${directory

Error message

Refusing to use non-directory install-store path ${directoryPath}.

What it means

Thrown by ensurePrivateDirectory() inside install-store.ts when a path that must be a real private directory is, after mkdirSync + lstatSync, found to be either not a directory or a symbolic link. The install store requires real 0o700 directories to prevent symlink-based path-confusion attacks where an attacker redirects the store into an arbitrary location.

Source

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

  previous: InstallRecord[];
};

export type InstallStorePaths = {
  paperclipHome: string;
  cliRoot: string;
  installsRoot: string;
  manifestPath: string;
  markerPath: string;
  lockPath: string;
  currentPath: string;
  shimPath: string;
};

function ensurePrivateDirectory(directoryPath: string): void {
  fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
  const stat = fs.lstatSync(directoryPath);
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
    throw new Error(`Refusing to use non-directory install-store path ${directoryPath}.`);
  }
  fs.chmodSync(directoryPath, 0o700);
}

function assertOwnedByCurrentUser(stat: fs.Stats, targetPath: string): void {
  const getuid = process.getuid;
  if (typeof getuid === "function" && stat.uid !== getuid()) {
    throw new Error(`Refusing to modify path not owned by the current user: ${targetPath}.`);
  }
}

function writeFileAtomic(filePath: string, contents: string, mode: number): void {
  const temporaryPath = path.join(
    path.dirname(filePath),
    `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
  );
  try {
    fs.writeFileSync(temporaryPath, contents, { mode, flag: "wx" });

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Inspect the offending path with 'ls -la' and confirm what type of entry it is.
  2. If it is a symlink or stray file you created, remove it ('rm <path>') and let the CLI recreate the directory.
  3. If the symlink is unexpected and you did not create it, investigate for tampering before removing — do not blindly delete.
  4. Move any needed data out of the path first if it contains real install payloads.

Example fix

// before: ~/.paperclip/cli is a symlink
$ ls -la ~/.paperclip/cli
lrwxrwxrwx  cli -> /tmp/evil

// after: remove symlink, let CLI recreate as real dir
$ rm ~/.paperclip/cli
$ paperclipai install ...
Defensive patterns

Strategy: validation

Validate before calling

import fs from "node:fs";

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

// Before calling install-store ops:
if (!isRealPrivateDirectory(cliRoot)) { /* remove stray entry or repoint */ }

Type guard

import fs from "node:fs";

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

Try / catch

try {
  initializeInstallStore(paths);
} catch (err) {
  if (err instanceof Error && err.message.includes("non-directory install-store path")) {
    // surface to user, ask before removing the stray entry
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: Called initializeInstallStore(), assertManagedInstallStore(), or any flow that calls ensurePrivateDirectory on cliRoot or installsRoot, where the path is occupied by a regular file or a symlink (lstatSync().isSymbolicLink() is true, or isDirectory() is false).

Common situations: 1) A symlink was placed at ~/.paperclip/cli pointing elsewhere (accidental or malicious). 2) A previous failed install left a regular file where a directory is expected. 3) A dotfile manager or backup restore turned the directory into a symlink. 4) Filesystem corruption or a bad mount.

Related errors


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