bmad-code-org/BMAD-METHOD · error · Error

Unsafe ref name: ${JSON.stringify(ref)}

Error message

Unsafe ref name: ${JSON.stringify(ref)}

What it means

Thrown by quoteShell() in external-manager.js when a Git tag/ref passed to git commands fails the whitelist regex ^[\w.\-+/]+$. This is the shell injection guard for external module refs (tags resolved from the GitHub API or user-supplied --pin values). It mirrors quoteCustomRef but operates on the external module code path.

Source

Thrown at tools/installer/modules/external-manager.js:27

const { decideChannelForModule } = require('./channel-plan');
const { getProjectRoot } = require('../project-root');

const VALID_CHANNELS = new Set(['stable', 'next', 'pinned']);

function normalizeChannelName(raw) {
  if (typeof raw !== 'string') return null;
  const lower = raw.trim().toLowerCase();
  return VALID_CHANNELS.has(lower) ? lower : null;
}

/**
 * Conservative quoting for tag names passed to git commands. Tags are
 * user-typed (--pin) or come from the GitHub API. Only allow the semver
 * character class we use to tag BMad releases; anything else throws.
 */
function quoteShell(ref) {
  if (typeof ref !== 'string' || !/^[\w.\-+/]+$/.test(ref)) {
    throw new Error(`Unsafe ref name: ${JSON.stringify(ref)}`);
  }
  return `"${ref}"`;
}

async function readChannelMarker(markerPath) {
  try {
    if (!(await fs.pathExists(markerPath))) return null;
    const content = await fs.readFile(markerPath, 'utf8');
    return JSON.parse(content);
  } catch {
    return null;
  }
}

async function writeChannelMarker(markerPath, data) {
  try {
    await fs.writeFile(markerPath, JSON.stringify({ ...data, writtenAt: new Date().toISOString() }, null, 2));
  } catch {

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Check the tag name for unexpected characters and use a clean semver tag.
  2. If the tag comes from the GitHub API, verify the repo's tags on the web UI.
  3. Ensure --pin values are simple alphanumeric/semver strings.
  4. Report upstream if a legitimately tagged release has an unusual name.

Example fix

// before
// --pin mymodule=my tag with spaces

// after
// --pin mymodule=v1.2.3
Defensive patterns

Strategy: validation

Validate before calling

function isValidExternalRef(ref) {
  return typeof ref === 'string' && /^[\w.\-+/]+$/.test(ref);
}

// Before passing --pin values to external module install:
const pinTag = options.pin;
if (pinTag && !isValidExternalRef(pinTag)) {
  throw new Error(`Invalid tag name for --pin: ${pinTag}`);
}

Type guard

function isSafeExternalRef(ref) {
  return typeof ref === 'string' && ref.length > 0 && /^[\w.\-+/]+$/.test(ref);
}

Try / catch

try {
  await extMgr.cloneExternalModule(code, options);
} catch (e) {
  if (e.message.startsWith('Unsafe ref name')) {
    console.error('The tag/ref contains invalid characters. Only letters, digits, dots, hyphens, underscores, plus, and slashes are allowed.');
  }
  throw e;
}

Prevention

When it happens

Trigger: The GitHub tags API returns a tag name with unexpected characters; a user passes --pin with a malformed value; resolved.ref from resolveChannel() contains characters outside the allowed set. The ref is used in 'git clone --branch' and 'git fetch ... tag' commands.

Common situations: A tag name contains characters like spaces, parentheses, or colons (rare but possible on non-standard repos); the channel resolver returned a ref with a newline or null byte from a malformed API response; a user typos the --pin value with shell metacharacters.

Related errors


AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13). Data as JSON: /api/errors/aad143c16389cb81. Report an issue: GitHub.