ruvnet/ruflo · error · Error

Invalid command: contains shell metacharacters

Error message

Invalid command: contains shell metacharacters

What it means

ReleaseManager.execCommand() validates git command strings with validateCommand(), rejecting anything containing ; & | ` $ ( ) < >. Critically, prepareRelease() itself builds the commit command `git commit -m "chore(release): <version>"` — the literal parentheses in 'chore(release)' match the guard, so with the default commit: true the release flow throws this error at the commit step after the version bump and changelog were already written.

Source

Thrown at v3/@claude-flow/deployment/src/release-manager.ts:29

 * Allowed git commands for security - prevents command injection
 */
const ALLOWED_GIT_COMMANDS = [
  'git status --porcelain',
  'git rev-parse HEAD',
  'git log',
  'git tag',
  'git add',
  'git commit',
  'git describe',
];

/**
 * Validate command against allowlist to prevent command injection
 */
function validateCommand(cmd: string): void {
  // Check for shell metacharacters
  if (/[;&|`$()<>]/.test(cmd)) {
    throw new Error(`Invalid command: contains shell metacharacters`);
  }

  // Must start with an allowed command prefix
  const isAllowed = ALLOWED_GIT_COMMANDS.some(prefix => cmd.startsWith(prefix));
  if (!isAllowed) {
    throw new Error(`Command not allowed: ${cmd.split(' ')[0]}`);
  }
}
import type {
  ReleaseOptions,
  ReleaseResult,
  PackageInfo,
  GitCommit,
  ChangelogEntry,
  VersionBumpType
} from './types.js';

export class ReleaseManager {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Pass commit: false to prepareRelease() and create the release commit yourself: git add package.json CHANGELOG.md && git commit -m 'chore(release): x.y.z' — you get createTag: true for the annotated tag, which passes validation
  2. Report/upstream the bug: the built-in commit message conflicts with the module's own metacharacter regex
  3. Avoid parentheses, &&, $(), and redirects in any command string that reaches ReleaseManager

Example fix

// before
await manager.prepareRelease({ bumpType: 'patch' }); // throws at `git commit -m "chore(release): ..."`

// after
const r = await manager.prepareRelease({ bumpType: 'patch', commit: false });
execSync('git add package.json CHANGELOG.md');
execSync('git commit -m "chore(release): ' + r.newVersion + '"');
Defensive patterns

Strategy: validation

Validate before calling

// The library's own commit message contains parentheses, so bypass its commit step
await manager.prepareRelease({ bumpType, commit: false, createTag: true });
execSync('git add package.json CHANGELOG.md');
execSync(`git commit -m "chore(release): ${newVersion}"`); // your own exec, no allowlist

Try / catch

const r = await manager.prepareRelease(options);
if (!r.success && /shell metacharacters/.test(r.error ?? '')) {
  // version/changelog were written but commit/tag aborted: finish git steps manually
}

Prevention

When it happens

Trigger: prepareRelease() with commit: true (the default): the generated `git commit -m "chore(release): 1.2.3"` contains ( and ) and is rejected by validateCommand. Also triggered by caller-built strings passed through ReleaseManager containing &&, $(...), or parentheses in paths or messages.

Common situations: Any first run of prepareRelease() that gets far enough to commit — the tag step and `git add` pass, but the commit line trips the parenthesis check; release scripts with chained git commands.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/bbfb88dbc0a72cf9. Report an issue: GitHub.