santifer/career-ops · error

Refusing to write the cover letter outside output/: ${raw}

Error message

Refusing to write the cover letter outside output/: ${raw}

What it means

safeOutputPath() refuses a path that is absolute but outside output/, or that contains any `..` path segment. The design decision is explicit in the source: a path with `..` or an absolute prefix has already chosen a location, so the script refuses rather than silently flattening it to output/<basename> — the user might believe the file landed where they pointed it.

Source

Thrown at generate-cover-letter.mjs:50

 * would escape `output/` — `..` traversal or an absolute path outside it —
 * are rejected instead of being silently flattened to `output/<basename>`.
 *
 * @param {string} raw - Caller-supplied --out / payload.output_path value.
 * @returns {string} Absolute path inside OUTPUT_ROOT.
 */
export function safeOutputPath(raw) {
  if (raw == null || String(raw).trim() === "") {
    throw new Error("Refusing to write the cover letter outside output/: (empty path)");
  }
  const trimmed = String(raw).trim();

  const asWritten = resolve(trimmed);
  if (containedInOutput(asWritten)) return asWritten;

  // Absolute paths and any `..` segment already chose a location; if that
  // location is not inside output/, refuse instead of rewriting to a basename.
  if (isAbsolute(trimmed) || /(^|[\\/])\.\.([\\/]|$)/.test(trimmed)) {
    throw new Error(`Refusing to write the cover letter outside output/: ${raw}`);
  }

  // Bare filename or a relative path that is not already under output/
  // (e.g. --out cover.pdf, or --out output/foo/bar.pdf from another cwd).
  const posix = trimmed.replace(/\\/g, "/").replace(/^\.\//, "");
  const relativeToRoot = posix === "output" || posix === "output/"
    ? ""
    : posix.startsWith("output/")
      ? posix.slice("output/".length)
      : posix;
  const candidate = resolve(OUTPUT_ROOT, relativeToRoot);
  if (containedInOutput(candidate)) return candidate;

  throw new Error(`Refusing to write the cover letter outside output/: ${raw}`);
}

/** True when absPath is a file (not output/ itself) still inside OUTPUT_ROOT. */
function containedInOutput(absPath) {

View on GitHub (pinned to 60398d6549)

Solutions

  1. Use a bare filename (`--out cover.pdf`) or a path under output/ (`--out output/acme/cover.pdf`) — subdirectories under output/ are preserved.
  2. If you genuinely need the file elsewhere, render into output/ then copy it: `node ... --out cover.pdf && cp output/cover.pdf /tmp/`.

Example fix

# before
node generate-cover-letter.mjs --payload p.json --out /tmp/acme.pdf

# after
node generate-cover-letter.mjs --payload p.json --out acme.pdf
cp output/acme.pdf /tmp/  # if needed elsewhere
Defensive patterns

Strategy: validation

Validate before calling

import { isAbsolute } from 'node:path';

function isSafeOutCandidate(raw) {
  const t = String(raw ?? '').trim();
  if (t === '') return false;
  if (isAbsolute(t)) return t.includes('/output/') || t.includes('\\output\\'); // at most heuristic; prefer relative
  return !/(^|[\\/])\.\.([\\/]|$)/.test(t);
}
if (!isSafeOutCandidate(args.out)) {
  console.error('Write inside output/ (e.g. --out cover.pdf), then copy the file where you need it.');
  process.exit(1);
}

Try / catch

try {
  const abs = safeOutputPath(userPath);
} catch (err) {
  if (err.message.startsWith('Refusing to write the cover letter outside output/')) {
    // deliberate refusal, not a bug: tell the user the sandbox rule
    console.error('Cover letters are written under output/. Re-run with --out <filename>.');
    process.exit(1);
  }
  throw err;
}

Prevention

When it happens

Trigger: `--out /tmp/cover.pdf` (absolute, outside output/); `--out ../cover.pdf` or `--out ../../etc/tmp/x.pdf` (traversal segments); `--out output/../../cover.pdf` (the `..` regex fires even though it starts with output/).

Common situations: Users used to pointing PDF tools at /tmp for a quick look; CI scripts passing an artifacts directory outside the repo; habit from other tools that silently rewrite stray paths into their output dir.

Related errors


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