santifer/career-ops · error

Refusing to write the cover letter outside output/: (empty p

Error message

Refusing to write the cover letter outside output/: (empty path)

What it means

safeOutputPath() in generate-cover-letter.mjs is the first guard on the --out / payload.output_path value: null, empty, or whitespace-only input is rejected immediately because an empty destination is meaningless and would otherwise normalize to the output/ directory itself rather than a letter file.

Source

Thrown at generate-cover-letter.mjs:40

import { resolveTemplate } from "./cv-templates.mjs";

const __dirname = dirname(fileURLToPath(import.meta.url));
const OUTPUT_ROOT = resolve(__dirname, "output");

/**
 * Resolve a requested cover-letter output path.
 *
 * Paths that stay inside `output/` keep their relative subdirectory (the
 * application-bundle layout `generate-pdf.mjs` already supports). Paths that
 * 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/")

View on GitHub (pinned to 60398d6549)

Solutions

  1. Pass a real filename: `--out acme-cover.pdf` (anchored under output/) or `--out output/acme/cover.pdf`.
  2. If the path is computed, default it before invoking: `--out "${SLUG:-cover}.pdf"`.
  3. Fix the payload generator to either set output_path or omit the flag entirely.

Example fix

# before
OUT_PATH=""
node generate-cover-letter.mjs --payload p.json --out "$OUT_PATH"

# after
OUT_PATH="acme-cover.pdf"
node generate-cover-letter.mjs --payload p.json --out "$OUT_PATH"
Defensive patterns

Strategy: validation

Validate before calling

const raw = args.out ?? payload.output_path;
if (raw == null || String(raw).trim() === '') {
  console.error('Provide a cover-letter filename, e.g. --out acme-cover.pdf');
  process.exit(1);
}
const outPath = safeOutputPath(raw);

Type guard

/** @param {unknown} v */
function isNonEmptyPath(v) {
  return typeof v === 'string' && v.trim() !== '' && v.trim() !== '.';
}

Prevention

When it happens

Trigger: Calling the script with `--out ""` or `--out " "`; a payload JSON where output_path is absent and caller code forwards `payload.output_path` unconditionally (undefined); shell script passing an unset variable: `--out "$OUT_PATH"` with OUT_PATH empty.

Common situations: Automation/glue code that builds the out path from optional job fields (company or role slug empty); env var not exported in a wrapper script; copy-pasted command with a placeholder never filled in.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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