{"record":{"id":"8358d2027f9dd8a5","repo":"tobi/qmd","slug":"skipped-file-outside-collection-skipped-file","errorCode":null,"errorMessage":"⚠ Skipped file outside collection: ${skipped.file}","messagePattern":"⚠ Skipped file outside collection: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/cli/qmd.ts","lineNumber":2069,"sourceCode":"    console.log(`\\nRun 'qmd embed' to update embeddings (${needsEmbedding} unique hashes need vectors)`);\n  }\n\n  closeDb();\n}\n\nfunction fsErrorCode(err: unknown): string {\n  if (err && typeof err === \"object\" && \"code\" in err) {\n    const code = (err as { code: unknown }).code;\n    if (typeof code === \"string\" && code.length > 0) return code;\n  }\n  return \"ERROR\";\n}\n\nfunction reportSkippedReads(skippedFiles: { file: string; code: string }[]): void {\n  if (skippedFiles.length === 0) return;\n  for (const skipped of skippedFiles) {\n    if (skipped.code === \"OUTSIDE_COLLECTION\") {\n      console.warn(`⚠ Skipped file outside collection: ${skipped.file}`);\n    } else {\n      console.warn(`⚠ Skipped unreadable file: ${skipped.file} (${skipped.code})`);\n    }\n  }\n  const escaped = skippedFiles.filter(f => f.code === \"OUTSIDE_COLLECTION\").length;\n  const unreadable = skippedFiles.length - escaped;\n  if (escaped) console.warn(`Skipped ${escaped} file(s) outside the collection root`);\n  if (unreadable) console.warn(`Skipped ${unreadable} unreadable file(s)`);\n}\n\nfunction renderProgressBar(percent: number, width: number = 30): string {\n  const filled = Math.round((percent / 100) * width);\n  const empty = width - filled;\n  const bar = \"█\".repeat(filled) + \"░\".repeat(empty);\n  return bar;\n}\n\nfunction parseEmbedBatchOption(name: string, value: unknown): number | undefined {","sourceCodeStart":2051,"sourceCodeEnd":2087,"githubUrl":"https://github.com/tobi/qmd/blob/dbfd0b4736aeaf761d1a16ca8e424f071df8feb9/src/cli/qmd.ts#L2051-L2087","documentation":"reportSkippedReads in the CLI prints '⚠ Skipped file outside collection: {file}' for every entry in skippedFiles whose code is OUTSIDE_COLLECTION — i.e. it is the user-facing echo of the store-level skip from src/store.ts (error 60). It means the indexer refused to ingest those files because their resolved path (via glob `../`, absolute patterns, or symlinks) lies outside the collection root, and any previously indexed copies are deactivated.","triggerScenarios":"After any indexing run (`qmd collection add`, `qmd update`), the CLI reports each file the store skipped with code OUTSIDE_COLLECTION. The underlying conditions are the same as error 60: escaping glob masks, absolute mask patterns, or file symlinks resolving outside the collection root.","commonSituations":"Right after adding a collection whose mask accidentally matches files above the root; on `qmd update` after symlinks in the collection were repointed to external targets; after moving the collection directory so old indexed paths now resolve outside; dotfiles-style symlinked note folders.","solutions":["Fix the `--mask` so it cannot escape the collection root (no `../` segments, no absolute patterns) and re-run `qmd update`","Add the outside directory as its own collection: `qmd collection add <dir> --name <n>`","Resolve escaping symlinks: replace them with real files under the root, or move their targets inside the collection","If the layout is intentional, re-add the collection with a higher-level root so all desired files are inside it"],"exampleFix":"# before\nqmd collection add ~/notes --name notes --mask '**/*.md,../*.md'\n# update prints: ⚠ Skipped file outside collection: ../todo.md\n\n# after\nqmd collection remove notes\nqmd collection add ~/notes --name notes --mask '**/*.md'\nqmd collection add ~ --name home-notes --mask 'todo.md'","handlingStrategy":"validation","validationCode":"// Before qmd update: verify every matched file resolves inside the collection root\nimport { globSync } from 'glob';\nimport path from 'node:path';\n\nfunction findEscapes(root: string, mask: string): string[] {\n  const absRoot = path.resolve(root);\n  return globSync(mask, { cwd: absRoot, follow: true, dot: true })\n    .map(f => path.resolve(absRoot, f))\n    .filter(f => !f.startsWith(absRoot + path.sep));\n}\nconst escapes = findEscapes('/home/me/notes', '**/*.md');\nif (escapes.length) console.warn('Will be skipped as OUTSIDE_COLLECTION:', escapes);","typeGuard":"function maskStaysInsideRoot(mask: string): boolean {\n  return mask.split(',').every(part => {\n    const p = part.trim().replace(/^!/, '');\n    return !path.isAbsolute(p) && !p.split(/[\\\\/]/).includes('..');\n  });\n}","tryCatchPattern":null,"preventionTips":["Keep masks root-relative and simple ('**/*.md')","Review the skipped-file warnings after every qmd update; each line names the escaping file","Check for repointed symlinks after moving directories: find . -type l -exec readlink -f {} \\;","Add external directories as separate collections rather than escaping with globs"],"tags":["glob","path-traversal","symlink","collection","cli-report","indexing"],"backgroundTag":"path-outside-root","analyzedSha":"dbfd0b4736aeaf761d1a16ca8e424f071df8feb9","analyzedAt":"2026-08-28T18:07:46.628Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}