ruvnet/ruflo · error · Error
contains shell metacharacters
Error message
${label} contains shell metacharacters What it means
Thrown by validatePath() in the daemon command when a path argument (e.g. the --workspace flag) contains any of the shell metacharacters ; & | ` $ < >. The value is later embedded in a forked child's argv and compared against ps/tasklist output, so metacharacters are rejected as defence-in-depth against command injection. This is pure fail-fast input validation: the path is never used or written before the check.
Solutions
- Remove or rename path components containing ; & | ` $ < > — use plain alphanumerics, dashes, and underscores only
- If the $ comes from an unexpanded variable, expand it before invoking the CLI (double quotes or interpolate the value yourself)
- Pass a plain absolute path with no metacharacters, e.g. /home/user/project/.claude-flow
- Do not look for an escape hatch — validatePath intentionally offers none; file an issue if the constraint is blocking
Example fix
# before claude-flow daemon start --workspace '/opt/data$2026' # after claude-flow daemon start --workspace '/opt/data-2026'
Defensive patterns
Strategy: validation
Validate before calling
import { resolve } from 'node:path';
function assertSafeDaemonPath(p: string, label = 'path'): void {
if (p.includes('\0')) throw new Error(`${label}: null bytes`);
if (/[;&|`$<>]/.test(p)) throw new Error(`${label}: shell metacharacters not allowed`);
}
assertSafeDaemonPath(workspace); // before invoking the daemon command Type guard
function isSafeDaemonPath(p: unknown): p is string {
return typeof p === 'string' && !p.includes('\0') && !/[;&|`$<>]/.test(p);
} Try / catch
try {
await cli.daemon.start({ workspace });
} catch (err) {
if (err instanceof Error && err.message.endsWith('contains shell metacharacters')) {
// reject/repair the path, then retry with a sanitized value
} else throw err;
} Prevention
- Keep workspace and binary paths to alphanumerics, dashes, and underscores
- Expand environment variables yourself before passing paths to the CLI
- In test suites, assert isSafeDaemonPath() on any path built from templates
When it happens
Trigger: Invoking `claude-flow daemon ... --workspace '/tmp/my$dir'` (literal $ from an unexpanded variable), or any daemon path argument containing ;, &, |, a backtick, $, <, or >. Single-quoted shell arguments that keep $VAR unexpanded are the classic producer.
Common situations: CI systems that create directories containing $ (some runner temp dirs), paths copy-pasted from markdown wrapped in backticks, scripts that single-quote variables so they arrive literally, or test fixtures with shell-looking strings.
Related errors
- contains null bytes
- escapes project directory
- approval issuance requires an authenticated human identity…
- Event log path contains null bytes
- extracted binary path failed validation
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/c6fb526a7ec890f3.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/commands/daemon.ts:377
}
},
};
/**
* Validate path for security - prevents path traversal and injection
*/
function validatePath(path: string, label: string): void {
// Must be absolute after resolution
const resolved = resolve(path);
// Check for null bytes (injection attack)
if (path.includes('\0')) {
throw new Error(`${label} contains null bytes`);
}
// Check for shell metacharacters in path components
if (/[;&|`$<>]/.test(path)) {
throw new Error(`${label} contains shell metacharacters`);
}
// Prevent path traversal outside expected directories
if (!resolved.includes('.claude-flow') && !resolved.includes('bin')) {
// Allow only paths within project structure
const cwd = process.cwd();
if (!resolved.startsWith(cwd)) {
throw new Error(`${label} escapes project directory`);
}
}
}
/**
* #1914: Resolve the `--workspace` flag to an absolute path, or return null
* if it is absent / not a usable string. Rejects values with null bytes or
* shell metacharacters (defence-in-depth — the value is later embedded in a
* forked child's argv and compared against `ps`/`tasklist` output).
*/View on GitHub (pinned to fa13ee4ad6)