bmad-code-org/BMAD-METHOD · error · Error
Could not resolve stable tag for '${moduleCode}' (${error.me
Error message
Could not resolve stable tag for '${moduleCode}' (${error.message}). ${hint} What it means
Thrown by cloneExternalModule() when resolveChannel() throws (GitHub API rate limit or network failure) AND there is no cached clone to fall back to. The error includes the underlying failure message and a contextual hint about setting GITHUB_TOKEN or using --next/--pin to bypass the tag lookup.
Source
Thrown at tools/installer/modules/external-manager.js:328
channel: cachedMarker.channel,
version: cachedMarker.version || 'main',
ref: cachedMarker.version && cachedMarker.version !== 'main' ? cachedMarker.version : null,
sha: cachedMarker.sha,
repoUrl: moduleInfo.url,
resolvedFallback: false,
planSource: 'cached',
});
return moduleCacheDir;
}
// No cache to fall back on — this is effectively a fresh install with
// no offline safety net. Surface a clear error with actionable guidance.
const isRateLimited = /rate limit/i.test(error.message);
const hint = isRateLimited
? process.env.GITHUB_TOKEN
? 'Your GITHUB_TOKEN may have expired or been rate-limited on its own budget. Try a different token or wait for the reset.'
: 'Set a GITHUB_TOKEN env var (any personal access token with public-repo read) to raise the 60-req/hour anonymous limit.'
: `Check your network connection, or rerun with \`--next=${moduleCode}\` / \`--pin ${moduleCode}=<tag>\` to skip the tag lookup.`;
throw new Error(`Could not resolve stable tag for '${moduleCode}' (${error.message}). ${hint}`);
}
if (resolved.resolvedFallback && !silent) {
if (resolved.reason === 'no-stable-tags') {
await prompts.log.warn(`No stable releases found for ${moduleInfo.name}; installing from main.`);
} else if (resolved.reason === 'not-a-github-url') {
await prompts.log.warn(`Cannot determine stable tags for ${moduleInfo.name} (non-GitHub URL); installing from main.`);
}
}
// Validate pin before we burn time cloning. Best-effort: skip on non-GitHub URLs.
if (planEntry.channel === 'pinned') {
const parsed = parseGitHubRepo(moduleInfo.url);
if (parsed) {
try {
const exists = await tagExists(parsed.owner, parsed.repo, planEntry.pin);
if (!exists) {
throw new Error(`Tag '${planEntry.pin}' not found in ${parsed.owner}/${parsed.repo}.`);View on GitHub (pinned to b70486b9bd)
Solutions
- Set a GITHUB_TOKEN environment variable (any personal access token with public-repo read) to raise the rate limit to 5000 req/hr.
- If already using a token, verify it is valid and not rate-limited on its own budget.
- Rerun with --next=<moduleCode> to skip the stable tag lookup and clone from main.
- Rerun with --pin <moduleCode>=<tag> to specify an exact tag and skip the API lookup.
- Check network connectivity to api.github.com.
Example fix
# before (anonymous, rate limited) npx bmad-method install # after (with token) export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx npx bmad-method install # or bypass tag lookup npx bmad-method install --next=my-module
Defensive patterns
Strategy: retry
Validate before calling
// Check rate limit status before fresh install
function hasGithubToken() {
return !!process.env.GITHUB_TOKEN;
}
// For CI/automated installs, always set a token
if (!hasGithubToken() && process.env.CI) {
console.warn('No GITHUB_TOKEN set — anonymous rate limit (60/hr) may be hit.');
}
// Use --pin or --next to bypass the tag API entirely
const options = {
channelOptions: {
pins: new Map([['module-code', 'v1.0.0']]), // skips tag lookup
},
}; Try / catch
try {
await extMgr.cloneExternalModule(moduleCode, options);
} catch (e) {
if (e.message.includes('Could not resolve stable tag') && e.message.includes('rate limit')) {
// Retry with --next to skip tag API
console.log('Rate limited. Retrying with --next to bypass tag lookup...');
await extMgr.cloneExternalModule(moduleCode, {
...options,
channelOptions: { nextSet: new Set([moduleCode]) },
});
return;
}
throw e;
} Prevention
- Set GITHUB_TOKEN in CI and development environments to avoid anonymous rate limits.
- Use --next=<code> or --pin <code>=<tag> to skip the GitHub tags API entirely.
- Cache module clones so re-installs don't hit the API.
- Monitor rate limit headers if making many API calls.
When it happens
Trigger: Fresh install of an external module (no cache) when the GitHub tags API returns 403 (rate limited, 60 req/hr anonymous limit) or the network is unreachable. If a cached clone existed with a channel marker, the code falls back to it instead of throwing.
Common situations: CI environment without GITHUB_TOKEN that hits the 60-req/hr anonymous rate limit; corporate firewall blocks api.github.com; GITHUB_TOKEN expired or revoked; DNS failure; the user is installing many modules in quick succession without a token.
Related errors
- Failed to clone external module '${moduleCode}' at ${resolve
- Unexpected response from ${url}
- Failed to clone ${parsed.cloneUrl}${refSuffix}: ${error_.mes
- Tag '${planEntry.pin}' not found in ${parsed.owner}/${parsed
- Unsafe ref name: ${JSON.stringify(ref)}
AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13).
Data as JSON: /api/errors/e6c06b90dc339e8f.
Report an issue: GitHub.