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

  1. Point outputs inside the workspace output area, relative to the manifest: "output": "../output/x.pdf" for a manifest in batch/.
  2. If deliverables must live elsewhere, render into output/ and copy afterwards.
  3. 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

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


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