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

  1. List the directory: `ls -la <localDir>/project-repositories/` and find the offending entry (the error names no entry, so check each one).
  2. 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`).
  3. Re-run the snapshot; if it still fails, recreate the workspace and let sync re-clone repositories with canonical names.
  4. 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

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


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)