affaan-m/ECC · error · Error
spec.file must be a portable filename without path…
Error message
spec.file must be a portable filename without path components or control characters
What it means
The library enforces that spec.file is a portable, safe filename: no path separators (/ or \), no Windows-reserved characters (<>:"|?*), no control characters, no trailing dot/space, and not a Windows reserved device name (con, prn, aux, nul, com1-9, lpt1-9). It throws when the value would be unsafe or non-portable as a generated filename across hosts.
Solutions
- Set spec.file to a bare filename with no directory components or reserved characters.
- Strip any directory portion yourself: path.basename(file) — but only if the base name is itself safe.
- Sanitize: replace reserved characters and control chars, trim trailing dots/spaces, then re-validate.
- Avoid Windows reserved device names entirely, even with extensions like 'con.md'.
Example fix
// before
const spec = { file: 'contracts/NDA MASTER', short: 'NDA', role: 'consultant' };
// after
const spec = { file: 'NDA MASTER', short: 'NDA', role: 'consultant' }; Defensive patterns
Strategy: validation
Validate before calling
const FILENAME_RE = /^[^<>:"/\\|?*\x00-\x1f]+$/;
function isPortableFilename(file) {
return typeof file === 'string' && file.trim() !== '' &&
FILENAME_RE.test(file) && !/[. ]$/.test(file) &&
!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i.test(file);
}
if (!isPortableFilename(spec.file)) throw new Error(`unsafe filename: ${spec.file}`); Type guard
function isSafeFilename(v) {
return typeof v === 'string' && v.length > 0 &&
!/[<>:"/\\|?*\p{Cc}]/u.test(v) && !/[. ]$/.test(v);
} Try / catch
try {
outputPaths(outDir, spec.file);
} catch (e) {
if (e.message.startsWith('spec.file must be a portable filename')) {
console.error(`Fix spec.file: ${spec.file} is not a portable filename`);
} else throw e;
} Prevention
- Treat spec.file as a bare filename only; never let users pass paths there.
- Sanitize user-supplied names: strip path components, reserved chars, control chars, trailing dots/spaces.
- Test specs against both POSIX and Windows rules since the library enforces the stricter cross-platform set.
When it happens
Trigger: spec.file = 'contracts/NDA' (path component), spec.file = 'NDA?' or 'NDA: draft' (reserved chars), spec.file = 'NDA.' (trailing dot), spec.file = 'COM1' (reserved device name), or a file name containing a newline/tab.
Common situations: Users pasting a full path or Windows path into a spec field intended only for a bare filename, filenames with characters legal on macOS but not Windows, or programmatically built names with trailing whitespace/period.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- -32602
- a claim token is required
- a confirmed nonempty coordinate is required
- a generated candidate cannot claim original-source identity
- A managed install plan with operations is required.
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/a9ff901c17f5cd63.
Report an issue: GitHub.
Appendix: source
Thrown at skills/master-agreement-generator/scripts/build-agreement.js:92
});
return `| ${cells.join(' | ')} |`;
}).join('\n');
}
function buildValues(spec, now) {
if (!spec || typeof spec !== 'object') {
throw new Error('spec must be an object');
}
for (const key of ['file', 'short', 'role']) {
if (typeof spec[key] !== 'string' || spec[key].trim() === '') {
throw new Error(`spec.${key} is required`);
}
}
// Reject path syntax on every host, including Windows paths supplied on POSIX.
if (/[<>:"/\\|?*\p{Cc}]/u.test(spec.file) ||
/[. ]$/.test(spec.file) ||
/^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(spec.file)) {
throw new Error('spec.file must be a portable filename without path components or control characters');
}
const clauses = ROLE_CLAUSES[spec.role];
if (!clauses) {
throw new Error(`unknown role "${spec.role}"; expected one of ${Object.keys(ROLE_CLAUSES).join(', ')}`);
}
const cp = spec.short;
const fill = text => text.split('{cp}').join(cp);
const supplement = typeof spec.supplement === 'string' && spec.supplement.trim() ? `${spec.supplement.trim()}; ` : '';
return {
FEE_TITLE: clauses.title,
CP_SHORT: cp,
DATE: spec.date || defaultDate(now),
CP_LEGAL: spec.legal || BLANK,
CP_JURIS: spec.juris || BLANK,
CP_ADDR: spec.addr || BLANK,
ROLE_CLAUSE: fill(clauses.role),
FEE_CLAUSE: fill(clauses.fee),View on GitHub (pinned to 8321021c54)