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, nil

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Inspect the wrapped cause for the failing variable or command
  2. Inject the secret into the environment (export/CI secrets) before running
  3. Test the $(cmd) secret command standalone to confirm it outputs the secret
  4. 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

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


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/c1a432963e612acc. Report an issue: GitHub.