paperclipai/paperclip · error
Invalid project repository directory
Error message
Invalid project repository directory
What it means
While enumerating entries inside `<localDir>/project-repositories`, readGitWorkspaceSnapshot validates each entry name and kind. Every entry must be a plain directory whose name matches /^[a-zA-Z0-9_-]+$/; anything else (a file, a symlink, or a directory with characters outside the allowlist) throws this error, because repository directory names are used as safe relative path components.
Solutions
- List the directory: `ls -la <localDir>/project-repositories/` and find the offending entry (the error names no entry, so check each one).
- Move or delete any files, symlinks, and directories whose names contain characters other than A-Z a-z 0-9 _ - (e.g. `mv 'my repo' repo`).
- Re-run the snapshot; if it still fails, recreate the workspace and let sync re-clone repositories with canonical names.
- Keep tooling and humans from writing into `project-repositories/`; use a sibling directory for scratch files.
Example fix
// before project-repositories/my repo/ # space rejected by ^[a-zA-Z0-9_-]+$ project-repositories/repo.bak # dot rejected, and may be a file // after mv project-repositories/'my repo' project-repositories/my-repo mv project-repositories/repo.bak ../scratch/repo.bak
Defensive patterns
Strategy: validation
Validate before calling
const ok = /^[a-zA-Z0-9_-]+$/;
for (const entry of await fs.readdir(root, { withFileTypes: true })) {
if (!entry.isDirectory() || !ok.test(entry.name)) throw new Error(`Bad repositories entry: ${entry.name}`);
} Type guard
function isValidRepositoryEntryName(name: string): boolean {
return /^[a-zA-Z0-9_-]+$/.test(name);
} Try / catch
try {
await snapshot(localDir);
} catch (err) {
if (err instanceof Error && err.message === "Invalid project repository directory") {
// enumerate entries, sanitize/rename or move offending ones, retry once
} else throw err;
} Prevention
- Never write scratch files, notes, or backups into project-repositories/.
- Name clones with kebab-case identifiers only (letters, digits, _, -).
- Run a lint-style check on the repositories dir in CI for workspace images.
- Keep tool artifacts in a sibling directory outside the synced root.
When it happens
Trigger: A file, symlink, socket, or oddly-named directory sits inside `project-repositories/` — e.g. a name containing dots, spaces, slashes, or non-ASCII characters, or a non-directory entry discovered during snapshot()/gitSnapshot().
Common situations: Editor/tooling artifacts like `repo.git.bak`, `my repo`, `.DS_Store`-style files, or notes dropped into the repositories folder; a clone directory renamed with a version suffix like `repo@v2`; backups extracted with entries like `repo.old/` inside the repositories dir.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- Invalid project repositories directory
- Project repository is not a Git checkout
- 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/dca9083a565373cd.
Report an issue: GitHub.
Appendix: source
Thrown at packages/adapter-utils/src/git-workspace-sync.ts:154
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) {
if (repositories.length === 0 && isNotAGitRepositoryError(error)) return null;
throw error;View on GitHub (pinned to 3f1d897a7c)