santifer/career-ops · error · Error
output escapes the tracker workspace: ${entryOutput}
Error message
output escapes the tracker workspace: ${entryOutput} What it means
In the batch loop, each entry's output is resolved relative to the manifest's own directory (not cwd) and must satisfy isWorkspaceOutputPath() — inside the tracker workspace's output area even through symlinked ancestors. A manifest whose output escapes is treated as malformed or tampered and fails for that entry only: nothing is read or written for it, and the batch continues.
Source
Thrown at generate-pdf.mjs:1287
const entryReport = (spec.reportNum ?? '').toString().trim();
if (entryReport && !/^\d+$/.test(entryReport)) {
throw new Error(`invalid reportNum "${entryReport}" (use the numeric report number)`);
}
// Resolve manifest-supplied input/output relative to the manifest's own
// directory, not process.cwd(), so a manifest renders identically wherever
// the batch is launched from. Absolute paths in the manifest still win
// (resolve() ignores the base when the tail is absolute).
const entryInput = resolve(manifestDir, spec.input);
const entryOutput = resolve(manifestDir, spec.output);
// Path-containment guards (realpath-based): keep the read and write inside
// the tracker workspace even through a symlinked ancestor. A batch
// manifest that escapes the workspace is malformed/tampered and is
// recorded as a per-entry failure rather than read or written.
assertInsideWorkspace(entryInput, 'input');
if (!isWorkspaceOutputPath(entryOutput, workspaceRoot)) {
throw new Error(`output escapes the tracker workspace: ${entryOutput}`);
}
let html = await readFile(entryInput, 'utf-8');
// Same order as the single render: reorder first so the guard judges the
// document that will actually be printed. Without this the batch path
// rendered N CVs with cv.sections silently inert.
html = reorderCvSections(html, cvSectionOrder);
validateCvSectionOrder(html, cvMarkdown, { allowReorder: globals.allowReorder });
html = normalizeTextForATS(html).html;
entries.push({
_idx: i,
html,
outputPath: entryOutput,
format: entryFormat,
baseDir: dirname(entryInput),
reportNum: entryReport,
inputPath: entryInput,View on GitHub (pinned to 60398d6549)
Solutions
- Point outputs inside the workspace output area, relative to the manifest: "output": "../output/x.pdf" for a manifest in batch/.
- If deliverables must live elsewhere, render into output/ and copy afterwards.
- For symlinked checkouts, run from the real path (see the assertInsideWorkspace errors) so containment resolves correctly.
Example fix
// before (batch/manifest.json, workspace root is ..)
{ "input": "a.html", "output": "../reports/a.pdf" }
// after
{ "input": "a.html", "output": "../output/a.pdf" } Defensive patterns
Strategy: validation
Validate before calling
import { resolve, relative, isAbsolute } from 'node:path';
function outputStaysInWorkspaceOutput(spec, manifestDir, workspaceRoot) {
const out = resolve(manifestDir, spec.output);
const rel = relative(resolve(workspaceRoot, 'output'), out);
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
}
const offenders = manifest.filter(e => !outputStaysInWorkspaceOutput(e, manifestDir, workspaceRoot));
if (offenders.length) { console.error(`Outputs must stay under <workspace>/output/: ${offenders.map(o => o.output).join(', ')}`); process.exit(1); } Prevention
- Author manifest outputs relative to the manifest's own directory and aim them under output/.
- Render into output/ and copy deliverables elsewhere afterwards — don't bend the manifest to reach external dirs.
- When moving or renaming the repo, regenerate manifests instead of reusing absolute paths.
When it happens
Trigger: A manifest at batch/manifest.json with "output": "../reports/x.pdf" — resolving above the output area; an absolute output like "/srv/pdfs/x.pdf" outside the workspace; the workspace reached through a symlink so a lexically-inside path canonicalizes outside.
Common situations: Manifests written when the repo lived at a different location; shared manifests reused across projects with different layouts; attempts to write deliverables straight into a reports/ or uploads/ directory.
Related errors
- Invalid or blocked URL: ${rejected.reason}
- each entry needs a string "input" and "output"
- invalid format "${entryFormat}" (use: ${validFormats.join(',
- invalid reportNum "${entryReport}" (use the numeric report n
- refusing to archive restricted destination: ${preGuard.reaso
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/87444413b91a3a81.
Report an issue: GitHub.