santifer/career-ops · error

escapes the tracker workspace: (workspaceRoot= canonical=…

Error message

${label} escapes the tracker workspace: ${absPath} (workspaceRoot=${workspace} canonical=${canonical} rel=${rel})

What it means

assertInsideWorkspace in generate-pdf.mjs canonicalizes a batch manifest path via realpath and compares it against the canonical workspace root with path.relative. If the relative path is empty, starts with '..', or is absolute, the path resolves outside the workspace and the guard throws with both the raw and canonical paths plus the disagreeing relative segment for diagnosis. This is a fail-closed path-containment (anti path-traversal) check.

Solutions

  1. Fix the manifest entry so input points at a real file inside the tracker workspace (compare the printed workspaceRoot vs canonical in the message)
  2. If the workspace root is behind a symlink (macOS /tmp -> /private/tmp), run from the real (canonical) path or align CAREER_OPS_ROOT with the resolved path
  3. Remove symlinks that point outside the workspace from the input path
  4. Check for a trailing/leading path typo (extra '..', wrong base directory) in the manifest

Example fix

// before
{"input": "../../other-repo/cv.html", "output": "output/cv.pdf"}
// after
{"input": "output/cv-tailored.html", "output": "output/cv-tailored.pdf"}
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative, isAbsolute } from 'node:path';
function insideWorkspace(p, root) {
  const rel = relative(root, resolve(p));
  return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
}
if (!insideWorkspace(entry.input, workspaceRoot)) throw new Error(`entry escapes workspace: ${entry.input}`);

Type guard

const isWorkspacePath = (p) => typeof p === 'string' && insideWorkspace(p, canonicalWorkspaceRoot());

Try / catch

try {
  assertInsideWorkspace(entryInput, 'input');
} catch (err) {
  if (err.message.includes('escapes the tracker workspace')) {
    console.error(err.message); // includes workspaceRoot vs canonical to spot symlink divergence
    process.exit(1);
  }
  throw err;
}

Prevention

When it happens

Trigger: A batch manifest entry whose input (after resolve against the manifest dir) realpath-resolves outside the tracker workspace: paths like '../secrets/cv.html', absolute paths to another repo, or a symlink inside the workspace pointing to an external file. Also hit on macOS CI (#3162) when /tmp or the workspace is a symlink so the canonicalized path diverges from the lexical workspace root.

Common situations: A generated or tampered manifest referencing paths outside the repo; running with a workspace root that itself sits behind a symlink so canonical workspaceRoot and canonical input disagree on the ancestor; copy-pasted manifest entries from another machine.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/a53aa9fd064802ef. Report an issue: GitHub.

Appendix: source

Thrown at generate-pdf.mjs:131

    // 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. Named as its own failure, not as an
    // escape: an intermittent CI-only hit of this guard (#3162) was undiagnosable
    // while both branches threw the same message — "escapes" points a reader at
    // the path, when the actual event was realpath failing underneath it.
    throw new Error(
      `${label} could not be canonicalized against the tracker workspace`
      + ` (${/** @type {any} */ (err)?.code || 'realpath failed'} on ${probe}): ${absPath}`,
    );
  }
  const workspace = canonicalWorkspaceRoot();
  const rel = relative(workspace, canonical);
  if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
    // #3162: an intermittent macOS-CI-only hit of this branch happens on paths
    // that are lexically inside the sandbox, and canonicalization SUCCEEDS
    // before it. Print both sides so the next occurrence names the disagreeing
    // ancestor outright instead of asking a reader to reconstruct it.
    throw new Error(
      `${label} escapes the tracker workspace: ${absPath}`
      + ` (workspaceRoot=${workspace} canonical=${canonical} rel=${rel})`,
    );
  }
  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.
 *
 * Only touches body text — preserves CSS, JS, tag attributes, and URLs.

View on GitHub (pinned to aac998c7ed)