paperclipai/paperclip · error
Project repository is not a Git checkout
Error message
Project repository is not a Git checkout: ${relative} What it means
For each valid-looking directory under `project-repositories/`, the library recurses with includeRepositories=false to confirm it is a real Git checkout. If the recursive snapshot returns null — the directory is not inside a Git work tree at its own top level — this error names the offending relative path, because repository discovery must never silently fall back to directory sync for a confirmed repository entry.
Solutions
- Inspect the named path (`<localDir>/project-repositories/<name>`): run `git -C <path> rev-parse --is-inside-work-tree` to confirm it is not a checkout.
- Re-clone or restore the repository into that directory so it has a working `.git` at its top level.
- If the directory is junk (extracted tarball, empty leftovers), remove it and let workspace sync re-create it.
- Avoid bare repos or nested submodules-as-toplevel here; each entry must itself be a standard non-bare work tree.
Example fix
// before project-repositories/api/ # extracted tarball, no .git // after rm -rf project-repositories/api git clone git@github.com:org/api.git project-repositories/api
Defensive patterns
Strategy: validation
Validate before calling
for (const dir of repositoryDirs) {
const r = await runGit(dir, ["rev-parse", "--is-inside-work-tree"]);
if (r.stdout.trim() !== "true") throw new Error(`Not a git checkout: ${dir}`);
} Type guard
null
Try / catch
try {
await snapshot(localDir);
} catch (err) {
if (err instanceof Error && err.message.startsWith("Project repository is not a Git checkout:")) {
const repoPath = err.message.split(": ")[1];
// re-clone or remove the named repository, then retry
} else throw err;
} Prevention
- Only populate project-repositories via real `git clone` (non-bare) into the entry directory itself.
- Avoid extracting tarballs or copying source trees into the repositories dir.
- Protect .git from cleanup scripts and file-sync tools that prune dotfiles.
- After any interrupted clone, verify `git rev-parse --is-inside-work-tree` before snapshotting.
When it happens
Trigger: An entry directory under `project-repositories/` passes the name/kind check but contains no `.git` (empty dir, partially failed clone, `.git` deleted, or a bare repo whose toplevel does not match), so readGitWorkspaceSnapshot(..., false) yields null.
Common situations: A clone was interrupted by disk-full or Ctrl-C leaving no `.git`; someone ran `rm -rf repo/.git`; the directory holds a `git init --bare` repo or just an extracted source tarball; a filesystem sync (Dropbox-style) stripped `.git`.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- Invalid project repositories directory
- 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/ccaa063c089f0072.
Report an issue: GitHub.
Appendix: source
Thrown at packages/adapter-utils/src/git-workspace-sync.ts:157
}
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;
}
if (insideWorkTree.stdout.trim() !== "true") {
return null;View on GitHub (pinned to 3f1d897a7c)