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
- Use a bare filename (`--out cover.pdf`) or a path under output/ (`--out output/acme/cover.pdf`) — subdirectories under output/ are preserved.
- 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
- Adopt the habit of always passing a bare filename or output/-prefixed relative path to --out.
- Render into output/, then copy/move in a separate step — never fight the sandbox.
- Reject any user-supplied path containing '..' upstream in your own tooling.
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
- Unsupported template format: ${format} (expected html or tex
- ⚠️ Font reference escapes fonts/, keeping original referenc
- refusing to archive restricted destination: ${preGuard.reaso
- refusing to archive restricted destination after redirect: $
- --${name} is required
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/18e95d95a7f759f8.
Report an issue: GitHub.