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

  1. Set spec.file to a bare filename with no directory components or reserved characters.
  2. Strip any directory portion yourself: path.basename(file) — but only if the base name is itself safe.
  3. Sanitize: replace reserved characters and control chars, trim trailing dots/spaces, then re-validate.
  4. 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

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


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)