{"record":{"id":"dca9083a565373cd","repo":"paperclipai/paperclip","slug":"invalid-project-repository-directory","errorCode":null,"errorMessage":"Invalid project repository directory","messagePattern":"Invalid project repository directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/git-workspace-sync.ts","lineNumber":154,"sourceCode":"      maxBuffer: options.maxBuffer,\n      env: options.env,\n    });\n  }\n  return await runLocalGit(localDir, args, options);\n}\n\nexport async function readGitWorkspaceSnapshot(localDir: string, includeRepositories = true): Promise<GitWorkspaceSnapshot | null> {\n  const repositories: NonNullable<GitWorkspaceSnapshot[\"repositories\"]> = [];\n  if (includeRepositories) {\n    const root = path.join(localDir, PROJECT_REPOSITORIES_DIR);\n    const rootStat = await fs.lstat(root).catch((error: NodeJS.ErrnoException) => {\n      if (error.code === \"ENOENT\") return null;\n      throw error;\n    });\n    if (rootStat) {\n      if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error(\"Invalid project repositories directory\");\n      for (const entry of (await fs.readdir(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {\n        if (!entry.isDirectory() || !/^[a-zA-Z0-9_-]+$/.test(entry.name)) throw new Error(\"Invalid project repository directory\");\n        const relative = `${PROJECT_REPOSITORIES_DIR}/${entry.name}`;\n        const snapshot = await readGitWorkspaceSnapshot(path.join(localDir, relative), false);\n        if (!snapshot) throw new Error(`Project repository is not a Git checkout: ${relative}`);\n        repositories.push({ path: relative, snapshot });\n      }\n    }\n  }\n  // Only repository discovery may report an ordinary directory. A failed\n  // snapshot of a confirmed repository must never fall back to directory sync.\n  let insideWorkTree: GitCommandResult;\n  try {\n    insideWorkTree = await runLocalGit(localDir, [\"rev-parse\", \"--is-inside-work-tree\"], {\n      timeout: 10_000,\n      maxBuffer: 16 * 1024,\n    });\n  } catch (error) {\n    if (repositories.length === 0 && isNotAGitRepositoryError(error)) return null;\n    throw error;","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/adapter-utils/src/git-workspace-sync.ts#L136-L172","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","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."],"exampleFix":"// before\nproject-repositories/my repo/     # space rejected by ^[a-zA-Z0-9_-]+$\nproject-repositories/repo.bak     # dot rejected, and may be a file\n// after\nmv project-repositories/'my repo' project-repositories/my-repo\nmv project-repositories/repo.bak ../scratch/repo.bak","handlingStrategy":"validation","validationCode":"const ok = /^[a-zA-Z0-9_-]+$/;\nfor (const entry of await fs.readdir(root, { withFileTypes: true })) {\n  if (!entry.isDirectory() || !ok.test(entry.name)) throw new Error(`Bad repositories entry: ${entry.name}`);\n}","typeGuard":"function isValidRepositoryEntryName(name: string): boolean {\n  return /^[a-zA-Z0-9_-]+$/.test(name);\n}","tryCatchPattern":"try {\n  await snapshot(localDir);\n} catch (err) {\n  if (err instanceof Error && err.message === \"Invalid project repository directory\") {\n    // enumerate entries, sanitize/rename or move offending ones, retry once\n  } else throw err;\n}","preventionTips":["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."],"tags":["filesystem","git","validation","identifier-format"],"backgroundTag":"invalid-identifier-format","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}