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
- Pass a real filename: `--out acme-cover.pdf` (anchored under output/) or `--out output/acme/cover.pdf`.
- If the path is computed, default it before invoking: `--out "${SLUG:-cover}.pdf"`.
- 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
- Default computed filenames in the calling script: `--out "${SLUG:-cover}.pdf"`.
- Treat an unset output_path in a payload as 'let the tool choose', i.e. omit the flag, never forward undefined.
- Fail fast in wrappers when a required variable is empty (`: "${OUT:?set OUT}"` in bash).
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
- --${name} is required
- --${name} must not contain tabs or newlines
- --${name} must be a percentage (e.g. 70 or 70%), got "${v}"
- unknown option: ${arg}
- --limit must be an integer from 1 to 100
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/f66da0dd03240cf7.
Report an issue: GitHub.