paperclipai/paperclip · error
Invalid project repositories directory
Error message
Invalid project repositories directory
What it means
readGitWorkspaceSnapshot scans the `project-repositories` directory inside a workspace and snapshots each repository. Before enumerating, it lstats that root directory and requires it to be a real directory (not a symlink, not a file). If it exists but is not a plain directory, the library throws this error because the workspace layout is corrupt or has been tampered with.
Solutions
- Inspect `<localDir>/project-repositories` with `ls -la` and check whether it is a file or symlink (`file`, `readlink`).
- Remove the file or symlink: `rm <localDir>/project-repositories` (or `rm <localDir>/project-repositories && mkdir <localDir>/project-repositories` if it should exist).
- Re-create the workspace via the normal workspace-sync flow so repositories are re-cloned into a genuine directory.
- If a symlink to a shared clone cache is intentional, restructure: mount/bind or configure the workspace root instead of symlinking inside it.
Example fix
// before (workspace layout) project-repositories -> /var/cache/shared-clones # symlink: rejected // after rm project-repositories mkdir project-repositories # let workspace sync clone repositories into the real directory
Defensive patterns
Strategy: validation
Validate before calling
import fs from "node:fs/promises";
async function assertPlainDir(p: string) {
const st = await fs.lstat(p);
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(`Not a plain directory: ${p}`);
}
await assertPlainDir(path.join(localDir, PROJECT_REPOSITORIES_DIR)); Type guard
function isPlainDirectoryStat(st: fs.Stats): boolean {
return st.isDirectory() && !st.isSymbolicLink();
} Try / catch
try {
await snapshot(localDir);
} catch (err) {
if (err instanceof Error && err.message === "Invalid project repositories directory") {
// repair: remove file/symlink and re-run workspace sync
} else throw err;
} Prevention
- Never symlink or mount anything named project-repositories inside a synced workspace.
- Provision workspaces only through the library's own sync flow.
- Add a startup health check that lstats the repositories root.
- Document the no-symlink invariant for backup/restore tooling.
When it happens
Trigger: Calling snapshot()/gitSnapshot() on a workspace localDir where `<localDir>/project-repositories` exists but is a regular file, or is a symbolic link (symlinks are explicitly rejected to prevent path escape).
Common situations: A file was accidentally created named `project-repositories`; a setup script symlinked the repositories dir to a shared cache or another volume; a restore/backup tool replaced the directory with a symlink; container image layers substituted the path.
Related errors
- Project repository is not a Git checkout
- Invalid project repository directory
- A full lowercase source SHA is required.
- Assets must be a real directory
- Attempt source is not a real directory
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/5db3c716c9b9b47f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/adapter-utils/src/git-workspace-sync.ts:152
operation,
timeout: options.timeout,
maxBuffer: options.maxBuffer,
env: options.env,
});
}
return await runLocalGit(localDir, args, options);
}
export async function readGitWorkspaceSnapshot(localDir: string, includeRepositories = true): Promise<GitWorkspaceSnapshot | null> {
const repositories: NonNullable<GitWorkspaceSnapshot["repositories"]> = [];
if (includeRepositories) {
const root = path.join(localDir, PROJECT_REPOSITORIES_DIR);
const rootStat = await fs.lstat(root).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return null;
throw error;
});
if (rootStat) {
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("Invalid project repositories directory");
for (const entry of (await fs.readdir(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
if (!entry.isDirectory() || !/^[a-zA-Z0-9_-]+$/.test(entry.name)) throw new Error("Invalid project repository directory");
const relative = `${PROJECT_REPOSITORIES_DIR}/${entry.name}`;
const snapshot = await readGitWorkspaceSnapshot(path.join(localDir, relative), false);
if (!snapshot) throw new Error(`Project repository is not a Git checkout: ${relative}`);
repositories.push({ path: relative, snapshot });
}
}
}
// Only repository discovery may report an ordinary directory. A failed
// snapshot of a confirmed repository must never fall back to directory sync.
let insideWorkTree: GitCommandResult;
try {
insideWorkTree = await runLocalGit(localDir, ["rev-parse", "--is-inside-work-tree"], {
timeout: 10_000,
maxBuffer: 16 * 1024,
});
} catch (error) {View on GitHub (pinned to 3f1d897a7c)