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
- Rename or move the project directory so the absolute path contains no & | < > ^ % ! ; ( ) backtick or $ characters
- Run the formatter manually outside the hook on that file
- 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
- Avoid parentheses, ampersands, and % in Windows project paths
- Prefer the non-.cmd formatter entry so shell:true is not required
- In CI, use workspace paths without spaces or special characters
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
- Formatter exited with status ${result.status}
- Path traversal rejected: ${relPath}
- Path traversal rejected: ${relPath}
- Claude Code command contains characters that are unsafe for
- Invalid ECC repo root: missing install script at ${installAp
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/a54faa4fe72e1071.
Report an issue: GitHub.