santifer/career-ops · error

${label} escapes the tracker workspace: ${absPath}

Error message

${label} escapes the tracker workspace: ${absPath}

What it means

assertInsideWorkspace() in generate-pdf.mjs canonicalizes the deepest existing ancestor of the path with realpathSync(). If that call throws — the ancestor vanished between existsSync() and realpathSync() (a TOCTOU race), a parent directory denies read/traverse (EACCES), or an NFS/symlink quirk — containment is unprovable, and the guard fails closed rather than fall back to a lexical path a symlinked ancestor could slip past.

Source

Thrown at generate-pdf.mjs:88

 * @throws {Error} when the canonical path escapes the tracker workspace.
 */
function assertInsideWorkspace(absPath, label) {
  let probe = absPath;
  const tail = [];
  while (!existsSync(probe)) {
    tail.unshift(basename(probe));
    const parent = dirname(probe);
    if (parent === probe) break; // reached the filesystem root
    probe = parent;
  }
  let canonical;
  try {
    canonical = existsSync(probe) ? resolve(realpathSync(probe), ...tail) : absPath;
  } catch {
    // Canonicalization failed (realpath raced away, permission error): containment
    // is unprovable, so fail closed rather than fall back to a lexical form that a
    // symlinked ancestor could slip past.
    throw new Error(`${label} escapes the tracker workspace: ${absPath}`);
  }
  const rel = relative(__workspaceRoot, canonical);
  if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
    throw new Error(`${label} escapes the tracker workspace: ${absPath}`);
  }
  return absPath;
}

// Ensure output directory exists (fresh setup)
mkdirSync(resolve(workspaceRoot, 'output'), { recursive: true });

/**
 * Normalize text for ATS compatibility by converting problematic Unicode.
 *
 * ATS parsers and legacy systems often fail on em-dashes, smart quotes,
 * zero-width characters, and non-breaking spaces. These cause mojibake,
 * parsing errors, or display issues. See issue #1.
 *

View on GitHub (pinned to 60398d6549)

Solutions

  1. Re-run once — a transient realpath race self-heals when the filesystem settles.
  2. If persistent, check traverse permission on every parent: `namei -l <path>` shows where access stops, and fix ownership/permissions there.
  3. Avoid symlinked ancestors for the workspace; use the real directory path.
Defensive patterns

Strategy: try-catch

Validate before calling

import { existsSync } from 'node:fs';
import { dirname } from 'node:path';

// cheap pre-flight: the deepest existing ancestor must be stable and readable
function ancestorsReadable(p) {
  let d = p;
  while (!existsSync(d)) d = dirname(d);
  try { accessSync(d, constants.R_OK); return true; } catch { return false; }
}

Try / catch

try {
  assertInsideWorkspace(path, 'input');
} catch (err) {
  if (err.message.includes('escapes the tracker workspace')) {
    if (isTransientFsError) { await once(setTimeout, 250); assertInsideWorkspace(path, 'input'); } // one retry for realpath races
    else { console.error(`Path containment failed for ${path} — check permissions/symlinks on its parents.`); process.exit(1); }
  } else throw err;
}

Prevention

When it happens

Trigger: A concurrent process deletes or renames a parent directory mid-render; the workspace sits under a parent dir with restrictive permissions; a symlinked ancestor whose target is on a flaky mount.

Common situations: Parallel batch renders racing a cleanup job over output/; running as a different user than the workspace owner; containerized runs where only part of the path is visible.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/068a3cece297dcb2. Report an issue: GitHub.