affaan-m/ECC · error · Error

File path contains unsafe shell characters

Error message

File path contains unsafe shell characters

What it means

post-edit-format.js throws this on Windows when the formatter binary is a .cmd file and must be spawned with shell:true. Because the shell enables command injection, the hook rejects file paths matching UNSAFE_PATH_CHARS (shell metacharacters such as quotes, &, |, ;) before spawning.

Solutions

  1. Rename the file or move the project to a path without shell metacharacters
  2. Use a non-.cmd formatter binary (node script or .exe) so shell:true is not needed
  3. Run the formatter manually (`npx biome check --write <file>`) for this file
  4. Fix PATH so a plain non-.cmd binary is resolved
Defensive patterns

Strategy: validation

Validate before calling

const UNSAFE_PATH_CHARS = /[&|;<>()"'`^%$!{}\[\],=?~*\s]/; // match hook's set
if (process.platform === 'win32' && !UNSAFE_PATH_CHARS.test(filePath)) {
  formatFile(filePath); // safe to proceed
}

Try / catch

try {
  run({ filePath });
} catch (err) {
  if (err.message === 'File path contains unsafe shell characters') {
    console.warn(`Skipping shell-based format for: ${filePath}`);
    return; // skip or format via non-shell path
  }
  throw err;
}

Prevention

When it happens

Trigger: Editing a file on Windows whose path contains `&`, `|`, `;`, quotes, or other shell metacharacters, then triggering the PostToolUse format hook with a .cmd formatter such as biome.cmd.

Common situations: Project or temp directories with `&` or parentheses in their names, files created with quoted names, npm .cmd shims being resolved as the formatter binary on Windows.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/a54faa4fe72e1071. Report an issue: GitHub.

Appendix: source

Thrown at scripts/hooks/post-edit-format.js:60

    if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
      try {
        const resolvedFilePath = path.resolve(filePath);
        const projectRoot = findProjectRoot(path.dirname(resolvedFilePath));
        const formatter = detectFormatter(projectRoot);
        if (!formatter) return rawInput;

        const resolved = resolveFormatterBin(projectRoot, formatter);
        if (!resolved) return rawInput;

        // Biome: `check --write` = format + lint in one pass
        // Prettier: `--write` = format only
        const args = formatter === 'biome' ? [...resolved.prefix, 'check', '--write', resolvedFilePath] : [...resolved.prefix, '--write', resolvedFilePath];

        if (process.platform === 'win32' && resolved.bin.endsWith('.cmd')) {
          // Windows: .cmd files require shell to execute. Guard against
          // command injection by rejecting paths with shell metacharacters.
          if (UNSAFE_PATH_CHARS.test(resolvedFilePath)) {
            throw new Error('File path contains unsafe shell characters');
          }
          const result = spawnSync(resolved.bin, args, {
            cwd: projectRoot,
            shell: true,
            stdio: 'pipe',
            timeout: 15000
          });
          if (result.error) throw result.error;
          if (typeof result.status === 'number' && result.status !== 0) {
            throw new Error(result.stderr?.toString() || `Formatter exited with status ${result.status}`);
          }
        } else {
          execFileSync(resolved.bin, args, {
            cwd: projectRoot,
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout: 15000
          });
        }

View on GitHub (pinned to 8321021c54)