charmbracelet/crush · error
oauth_client_id: %w
Error message
oauth_client_id: %w
What it means
For OAuth-enabled HTTP MCP servers, the pre-registered oauth_client_id is resolved through the shell (supporting $VAR and $(cmd)). If the resolver errors, it is wrapped as 'oauth_client_id: <cause>'.
Source
Thrown at internal/agent/tools/mcp/init.go:1093
// on every exchange and refresh via this saver.
if m.OAuth {
tokenSaver := func(tok *oauth.Token) {
if err := cfg.SetConfigField(config.ScopeGlobal, fmt.Sprintf("mcp.%s.oauth_token", name), tok); err != nil {
slog.Warn("Failed to persist MCP OAuth token", "name", name, "error", err)
} else {
slog.Info("Persisted MCP OAuth token", "name", name)
}
}
// A pre-registered client is required for servers that do not
// support dynamic client registration (e.g. GitHub, Slack).
// Resolve the credentials through the shell like other config
// values so $VAR and $(cmd) work.
var preregistered *oauth.OAuthClient
if strings.TrimSpace(m.OAuthClientID) != "" {
clientID, err := resolver.ResolveValue(m.OAuthClientID)
if err != nil {
return nil, nil, fmt.Errorf("oauth_client_id: %w", err)
}
clientSecret, err := resolver.ResolveValue(m.OAuthClientSecret)
if err != nil {
return nil, nil, fmt.Errorf("oauth_client_secret: %w", err)
}
preregistered = &oauth.OAuthClient{
ClientID: strings.TrimSpace(clientID),
ClientSecret: strings.TrimSpace(clientSecret),
}
}
// Normalize trailing slash for PRM discovery compatibility.
normalizedURL := strings.TrimSuffix(url, "/")
oauthHandler, oauthErr := mcpoauth.NewHandler(name, normalizedURL, m.OAuthToken, preregistered, tokenSaver, mcpoauth.IsInteractive(ctx), m.OAuthCallbackPort)
if oauthErr != nil {
return nil, nil, fmt.Errorf("failed to create OAuth handler for mcp %q: %w", name, oauthErr)
}
authURLs.Set(name, oauthHandler)View on GitHub (pinned to 7944b8e522)
Solutions
- Look at the wrapped cause to see which variable/command failed
- Export the client ID env var before launching crush
- Verify any $(cmd) secret-fetch command exists and exits 0
- Or omit client_id and rely on dynamic client registration
Example fix
// before mcp api type http url '...' oauth oauth_client_id '$OAUTH_ID' # OAUTH_ID unset // after export OAUTH_ID=abc123 # or hardcode the public client id
Defensive patterns
Strategy: validation
Validate before calling
id := os.Getenv("OAUTH_CLIENT_ID")
if strings.TrimSpace(id) == "" {
return errors.New("OAUTH_CLIENT_ID must be exported before starting crush")
} Prevention
- Export OAuth client IDs in the shell profile or CI environment
- Verify secret-manager subcommands ($(cmd)) are installed and succeed
- Prefer dynamic client registration when a static ID is not required
- Log resolution failures early with a config validation step
When it happens
Trigger: OAuth MCP config with a non-empty OAuthClientID containing an unresolvable $VAR or failing $(cmd) substitution passed to resolver.ResolveValue.
Common situations: Client ID kept in a secrets env var that is not exported in the crush process; secret manager command missing from PATH; typo in variable name.
Related errors
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/2b5eb098149d5d8a.
Report an issue: GitHub.