affaan-m/ECC · warning

File path contains unsafe shell characters

Error message

File path contains unsafe shell characters

What it means

On Windows, when the resolved formatter binary ends in .cmd the hook must spawn it with shell:true. cmd.exe interprets characters like & | < > ^ % ! ; ( ) backtick and $ as operators or variable expanders, so the hook rejects any edited file path containing them before spawning. This is a command-injection guard; it is caught and silently skipped (formatting is non-blocking).

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 01e15490f0)

Solutions

  1. Rename or move the project directory so the absolute path contains no & | < > ^ % ! ; ( ) backtick or $ characters
  2. Run the formatter manually outside the hook on that file
  3. Invoke the non-.cmd formatter entry (node_modules/.bin/biome or prettier directly) so shell:true is not used

Example fix

// before
C:\work\repo (1)\src\file.ts
// after
C:\work\repo-1\src\file.ts
Defensive patterns

Strategy: validation

Validate before calling

const UNSAFE = /[&|<>^%!;`()$]/;
function assertSafeShellPath(filePath) {
  if (process.platform === 'win32' && UNSAFE.test(filePath)) {
    throw new Error(`Path unsafe for cmd.exe: ${filePath}`);
  }
}

Type guard

function isSafeForCmdShell(filePath) {
  return typeof filePath === 'string' && !/[&|<>^%!;`()$]/.test(filePath);
}

Try / catch

try { spawnSync(bin, args, { shell: true }); }
catch (err) {
  if (/unsafe shell characters/.test(err.message)) {
    console.warn('Skipping format: project path contains shell metacharacters.');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Editing a JS/TS file whose absolute path contains a metacharacter, e.g. C:\dev\repo (copy)\src\file.ts, D:\work\a&b\file.js, or a path with %VAR%-style segments.

Common situations: Project directories named with parentheses or ampersands on Windows, CI agent workspace paths containing parens, or paths produced by archive extractors that append ' (1)'.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/a54faa4fe72e1071. Report an issue: GitHub.