affaan-m/ECC · error · Error
spec.file must keep generated files directly inside the…
Error message
spec.file must keep generated files directly inside the output directory
What it means
outputPaths() resolves '<file> MASTER.md' and '<file> MASTER.docx' against the resolved output directory and refuses to continue if either destination resolves outside that directory (path.dirname(destination) !== root). This guards against path traversal when spec.file smuggles path components (e.g. '../..' or absolute paths) into the generated filenames.
Solutions
- Pass a bare, single-component filename in spec.file (no slashes, backslashes, or '..').
- Ensure outDir is an existing plain directory, not a symlink chain, so path.resolve(outDir) is the true target.
- If you must write to a subdirectory, create it explicitly and pass that directory as outDir instead of encoding it in file.
- Keep the built-in filename validation (error 722) intact — it prevents most cases that reach this guard.
Example fix
// before outputPaths(outDir, '../../etc/evil'); // after outputPaths(outDir, 'evil'); // writes <outDir>/evil MASTER.md|.docx
Defensive patterns
Strategy: validation
Validate before calling
const path = require('path');
function staysInside(outDir, file) {
const root = path.resolve(outDir);
return ['md', 'docx'].every(ext =>
path.dirname(path.resolve(root, `${file} MASTER.${ext}`)) === root);
}
if (!staysInside(outDir, spec.file)) throw new Error('file escapes output directory'); Type guard
null
Try / catch
try {
outputPaths(outDir, spec.file);
} catch (e) {
if (e.message.includes('directly inside the output directory')) {
console.error('spec.file must be a bare filename, not a path');
} else throw e;
} Prevention
- Never interpolate user input into output file paths without basename() + re-validation.
- Keep outDir as a plain directory (not a symlink chain) so path.resolve is predictable.
- Rely on the library's own filename checks (error 722) rather than bypassing them with raw values.
When it happens
Trigger: Calling outputPaths() (via the build pipeline) with a file value that still resolves oddly — e.g. Windows-style 'C:\evil' names or names that expand with '..' — making the md/docx destination's dirname differ from the resolved outDir.
Common situations: A spec.file like '..\..\Users\x\evil' on a Windows host or a POSIX host accepting Windows-style input; a symlinked or oddly-mounted outDir where path.resolve lands somewhere unexpected.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- artifact path escapes output directory
- output path contains an invalid component
- Refusing unsafe repair source metadata: sources must stay…
- artifact path must stay beneath output root
- artifact permits provider execution
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/5b70c3f16a417dd8.
Report an issue: GitHub.
Appendix: source
Thrown at skills/master-agreement-generator/scripts/build-agreement.js:143
}
const leftover = output.match(/\{\{[A-Z_]+\}\}/g);
if (leftover) {
throw new Error(`template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}`);
}
return `${DRAFT_NOTICE}\n\n${output}`;
}
function pandocAvailable() {
const probe = spawnSync('pandoc', ['--version'], CONVERTER_OPTIONS);
return !probe.error && probe.status === 0;
}
function outputPaths(outDir, file) {
const root = path.resolve(outDir);
const destinations = ['md', 'docx'].map(extension => path.resolve(root, `${file} MASTER.${extension}`));
for (const destination of destinations) {
if (path.dirname(destination) !== root) {
throw new Error('spec.file must keep generated files directly inside the output directory');
}
// lstat also detects dangling links. Check BOTH outputs before the first write,
// even when conversion is disabled. The caller must control this directory;
// these checks do not isolate concurrent hostile filesystem changes.
let stat;
try {
stat = fs.lstatSync(destination);
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (stat?.isSymbolicLink()) {
throw new Error('output destination must not be a symlink');
}
}
return { root, mdPath: destinations[0], docxPath: destinations[1] };
}
function build(templatePath, specPath, outDir, options = {}) {View on GitHub (pinned to 8321021c54)