charmbracelet/crush · error
oauth_client_secret: %w
Error message
oauth_client_secret: %w
What it means
Same resolution path as the client ID but for oauth_client_secret: the value is resolved through the shell resolver and failures are wrapped as 'oauth_client_secret: <cause>'.
Source
Thrown at internal/agent/tools/mcp/init.go:1097
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)
return &mcp.StreamableClientTransport{
Endpoint: url,
OAuthHandler: oauthHandler,
}, oauthHandler, nilView on GitHub (pinned to 7944b8e522)
Solutions
- Inspect the wrapped cause for the failing variable or command
- Inject the secret into the environment (export/CI secrets) before running
- Test the $(cmd) secret command standalone to confirm it outputs the secret
- If using dynamic client registration, drop the pre-registered secret entirely
Example fix
// before mcp api type http url '...' oauth oauth_client_secret '$(op read vault/secret)' # op not on PATH // after install the 1Password CLI or use: oauth_client_secret '$SECRET_VALUE'
Defensive patterns
Strategy: validation
Validate before calling
sec := os.Getenv("OAUTH_CLIENT_SECRET")
if strings.TrimSpace(sec) == "" {
return errors.New("OAUTH_CLIENT_SECRET must be exported before starting crush")
} Prevention
- Inject secrets via the environment (never commit them to config)
- Test the secret-fetch command standalone and confirm exit code 0
- Ensure CI secret injection runs before launching crush
- Fall back to dynamic client registration to avoid static secrets
When it happens
Trigger: OAuth MCP config where OAuthClientSecret contains an unresolvable $VAR or a failing $(cmd) substitution when resolver.ResolveValue is called.
Common situations: Secret not present in the environment (CI missing secret injection); secret-manager CLI (e.g. op read, 1password) not installed; permission errors making the fetch command fail.
Related errors
- oauth_client_id: %w
- interactive OAuth authorization required
- failed to start OAuth callback listener: all candidate ports
- OAuth callback listener closed
- mcp '%s' does not use OAuth authentication
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/c1a432963e612acc.
Report an issue: GitHub.