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

  1. Pass a bare, single-component filename in spec.file (no slashes, backslashes, or '..').
  2. Ensure outDir is an existing plain directory, not a symlink chain, so path.resolve(outDir) is the true target.
  3. If you must write to a subdirectory, create it explicitly and pass that directory as outDir instead of encoding it in file.
  4. 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

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


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)