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
- Fix the manifest entry so input points at a real file inside the tracker workspace (compare the printed workspaceRoot vs canonical in the message)
- 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
- Remove symlinks that point outside the workspace from the input path
- 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
- Keep all manifest input/output paths inside the tracker workspace; use relative paths
- On macOS beware /tmp -> /private/tmp symlinks: derive the workspace root from its realpath
- Do not use symlinks pointing outside the workspace for inputs
- Lint manifests with a containment check before running batch renders
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
- could not be canonicalized against the tracker workspace (…
- ⚠️ Font reference escapes fonts/, keeping original…
- local-parser: path escapes the project root
- output escapes the tracker workspace
- refusing to hash non-regular file
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)