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
- Inspect the offending path with 'ls -la' and confirm what type of entry it is.
- If it is a symlink or stray file you created, remove it ('rm <path>') and let the CLI recreate the directory.
- If the symlink is unexpected and you did not create it, investigate for tampering before removing — do not blindly delete.
- 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
- Never symlink ~/.paperclip/cli or its installs subdirectory.
- Audit the store directory after restore-from-backup operations.
- Avoid dotfile managers that symlink the .paperclip tree.
- If mounting the store on a network filesystem, ensure it presents real directories.
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
- Refusing to modify path not owned by the current user: ${tar
- Refusing to use unsafe install-store marker ${paths.markerPa
- Refusing to remove unsafe install-store path ${paths.cliRoot
- Refusing to activate payload outside ${paths.installsRoot}.
- Refusing to activate non-directory payload ${payloadPath}.
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/aabb5ff211e477e2.
Report an issue: GitHub.