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

  1. 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`
  2. 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
  3. Replace escaping symlinks with real files/directories inside the collection, or move the symlink target under the collection root
  4. 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

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


AI-assisted analysis of tobi/qmd@dbfd0b4736 (2026-08-28). Data as JSON: /api/errors/ea0a8dafe5f70530. Report an issue: GitHub.