tobi/qmd · warning
OUTSIDE_COLLECTION
OUTSIDE_COLLECTION
Error message
OUTSIDE_COLLECTION
What it means
This error is recorded when a file matched by a collection's glob mask resolves to a location outside the collection root directory. The indexer (src/store.ts) explicitly refuses to ingest such files and does not mark them as seen, so any previously indexed 'escaped' row gets deactivated on the next pass. It is a skip code, not a crash: ingestion continues with remaining files.
Source
Thrown at src/store.ts:1655
let indexed = 0, updated = 0, unchanged = 0, processed = 0;
const skippedFiles: ReindexSkippedFile[] = [];
const seenPaths = new Set<string>();
// Literal paths of every file in this scan. Passed to the legacy-path
// migration so it never adopts a row that still belongs to a live file.
const livePaths = new Set(files.map(f => normalizePathSeparators(f)));
for (const relativeFile of files) {
const filepath = getRealPath(resolve(collectionPath, relativeFile));
// Store the literal relative path so the filesystem path can always be
// reconstructed as: resolve(collection.path, storedPath).
// handelize() is NOT applied at index time — it is display-only.
const path = normalizePathSeparators(relativeFile);
// Glob `../` segments, absolute patterns, and file symlinks can resolve
// outside the collection root. Do not ingest those files, and do not mark
// them seen so a previous escaped row is deactivated on this pass.
if (!isPathInsideDir(collectionPath, filepath)) {
processed++;
skippedFiles.push({ file: relativeFile, code: "OUTSIDE_COLLECTION" });
options?.onProgress?.({ file: relativeFile, current: processed, total });
continue;
}
seenPaths.add(path);
let content: string;
try {
content = readFileSync(filepath, "utf-8");
} catch (err) {
// Skip files that can't be read (ETIMEDOUT on APFS compressed files,
// EAGAIN on iCloud evicted files, EACCES, etc.) instead of aborting
// the rest of the collection (#460).
processed++;
skippedFiles.push({ file: relativeFile, code: fsErrorCode(err) });
options?.onProgress?.({ file: relativeFile, current: processed, total });
continue;
}
View on GitHub (pinned to dbfd0b4736)
Solutions
- Fix the glob mask so it only matches paths under the collection root (remove `../` segments and absolute patterns), then run `qmd collection add` again with the corrected `--mask`
- If the file legitimately lives outside the root, add the directory it lives in as its own collection instead of trying to reach it with a relative glob
- Replace escaping symlinks with real files/directories inside the collection, or move the symlink target under the collection root
- If the escape is intentional and trusted, re-scope the collection root one level higher so the target path is inside it
Example fix
# before qmd collection add ~/notes --mask '**/*.md,../journal/*.md' # after qmd collection add ~/notes --mask '**/*.md' qmd collection add ~/journal --name journal --mask '**/*.md'
Defensive patterns
Strategy: validation
Validate before calling
import path from 'node:path';
function assertMaskInsideRoot(root: string, mask: string): string[] {
const problems: string[] = [];
for (const part of mask.split(',')) {
const p = part.trim().replace(/^!/, '');
if (path.isAbsolute(p)) problems.push(`absolute pattern: ${p}`);
if (p.split('/').includes('..')) problems.push(`escaping segment '..': ${p}`);
}
return problems;
}
// run before qmd collection add / update:
const issues = assertMaskInsideRoot('/home/me/notes', '**/*.md,../*.md');
if (issues.length) throw new Error('Mask escapes collection root: ' + issues.join('; ')); Type guard
function isSafeMask(mask: string): boolean {
return mask.split(',').every(part => {
const p = part.trim().replace(/^!/, '');
return !path.isAbsolute(p) && !p.split(/[\\/]/).includes('..');
});
} Prevention
- Always use root-relative mask patterns without '..' segments
- Prefer adding another collection over reaching outside the root with globs
- Audit symlinks in the collection before running update: find . -type l -exec readlink -f {} \; and check targets are inside the root
- After reorganizing directories, run qmd update and review the 'Skipped file outside collection' warnings
When it happens
Trigger: Calling the indexing/ingest routine (e.g. via `qmd collection add` or `qmd update`) when: (1) the glob mask contains `../` segments that escape the root, (2) the mask uses an absolute path pattern pointing outside the collection, or (3) a matched path is a symlink whose target lives outside the collection root. The check `!isPathInsideDir(collectionPath, filepath)` then evaluates true and the file is pushed to skippedFiles with code OUTSIDE_COLLECTION.
Common situations: Users add a collection with a mask like `**/../notes/*.md`, use an absolute mask (`/etc/**/*.conf`) that is not under the collection directory, or have symlinked folders (dotfiles repos, shared note dirs) whose targets sit elsewhere on disk. Moving or restructuring a collection directory so previously indexed files now resolve outside the root also triggers it on the next update.
Related errors
- ⚠ Skipped file outside collection: ${skipped.file}
- OUTSIDE_COLLECTION
- Collection '${collection}' has no indexed documents.\nRun 'q
- Collection not found: ${collection}\n${hint}
- No indexed documents found.\nIndex a collection with 'qmd co
AI-assisted analysis of tobi/qmd@dbfd0b4736 (2026-08-28).
Data as JSON: /api/errors/ea0a8dafe5f70530.
Report an issue: GitHub.