[FEATURE] Plugin extensibility for auth events: hook for auth-failure detection + registration surface for non-OAuth recovery in /mcp

Resolved 💬 1 comment Opened May 6, 2026 by 1fanwang Closed Jun 4, 2026

Problem

Plugins have no first-class way to participate in auth/credential events. Two related gaps that compound when a plugin wraps a service whose auth isn't OAuth/PKCE (e.g. a CLI helper that mints a short-lived bearer token from a local certificate, a service that requires a Kerberos ticket, a tool that reads from a keychain entry the user maintains externally).

Gap 1 — no hook fires on auth-flavored failures

When a tool call returns a 401, an SSL alert, or any credential-expired error, the only public surfaces are:

  • PostToolUseFailure with content matching — works, but each plugin has to substring-search the error JSON for "401" / "Unauthorized" / "expired token" / etc. The match is fragile (different tools format the error differently) and there's no canonical signal that the failure was auth-class vs. something else.
  • StopFailure with authentication_failed matcher — only fires at the end of a turn; too coarse to drive per-tool retry.

A dedicated AuthFailure (or CredentialEvent) hook event would let plugins register exactly one source of truth for "the user's credential to X needs refreshing", instead of N plugins each carrying a per-tool error parser.

Gap 2 — /mcp needs auth is hardcoded to OAuth/PKCE for HTTP/SSE transports

When an MCP server returns OAuth metadata, the status line surfaces 1 MCP server needs auth · /mcp and the /mcp UI walks the user through PKCE. That UX is great. But it only fires for HTTP/SSE servers running OAuth. If the user's downstream auth is non-OAuth (cert-based mTLS handshake, Kerberos, vendor-specific token mint via a CLI), there's no in-product surface to recover — the plugin can offer its own /<plugin>:auth slash command but it lives outside the /mcp needs auth flow that users have already been trained to follow.

A registration surface where a plugin can declare "I provide a recover() for credential X used by MCP server Y" would let /mcp drive plugin-supplied recovery the same way it drives PKCE today. The user gets one consistent reauth UX regardless of underlying auth shape.

Repro

Plugin author writes a skill that wraps a CLI requiring a per-session token (mints from a local cert, ~10s, not OAuth). The token is consumed by an MCP server registered in the same plugin's .mcp.json. Today:

  1. User starts a session. Cert is expired. Plugin's MCP server connects but every tool call returns 401.
  2. /mcp shows the server as connected (cert handshake succeeded at TLS level; the 401 is from the application layer downstream). No "needs auth" badge.
  3. The agent gets 401s from each call, but PostToolUseFailure only fires after the 401 reaches the model. The plugin's hook subscriber has to parse the 401 from the tool result, then orchestrate a refresh out-of-band via a sub-agent / slash command.
  4. After refresh, the MCP server's cached in-memory bearer is stale. The user has to type /mcp (which doesn't help here because the server isn't in needs-auth state) or restart the session.

Each of those four steps would be cleaner with an AuthFailure hook (steps 3 collapse to "subscribe and recover") and a plugin-registered recover handler (step 4 collapses to "the plugin's recover invalidates and reconnects").

Possible shapes

(intentionally not prescribing — leaving room for whatever shape the maintainers prefer)

For Gap 1, a hook event:

{
  "hookEventName": "AuthFailure",
  "session_id": "...",
  "source": "plugin-mcp-server-name" | "tool-name",
  "error_kind": "auth_401" | "tls_alert" | "credential_expired" | "other",
  "tool_use_id": "...",
  "transport": "stdio" | "http" | "sse",
  "details": { /* tool-specific failure metadata */ }
}

The plugin's hook command receives the event via the existing JSON-on-stdin pattern; can return additionalContext to nudge the agent toward /<plugin>:auth-refresh, or run the refresh directly and emit a system reminder.

For Gap 2, a manifest field:

// in plugin.json
{
  "providesAuthRecovery": [
    {
      "id": "my-plugin-cred",
      "label": "My plugin's downstream credential",
      "describesCommand": "auth-refresh",   // resolves to /<plugin>:auth-refresh
      "appliesTo": ["mcp-server-name"]      // optional, scope to specific MCP servers
    }
  ]
}

/mcp then surfaces a row for "My plugin's downstream credential" alongside PKCE-backed servers, and selecting it dispatches the plugin's command. The command's job: refresh the credential, emit a success notification.

The two together make the auth-handler skill pattern that several teams have already had to write (via PostToolUseFailure content-matching) into a built-in capability.

Why one issue, not two

They're independent in implementation but pair up in user experience: detect (Gap 1) and recover (Gap 2). Either alone is half the story:

  • Gap 1 alone: plugins detect the failure but still have to invent their own UX to recover.
  • Gap 2 alone: plugins have a recover surface but still have to PostToolUseFailure-parse to know when to drive it.

Happy to split into two issues if maintainers prefer; flagging them together so the design conversation isn't fragmented.

Prior art

  • The PKCE callback handler in claudeai-proxy / HTTP MCP transports demonstrates the registration shape works for OAuth — extending it to plugin-supplied recovery is incremental.
  • opencode's AuthHook (provider, methods[].authorize(...)) is one shape worth looking at for inspiration; per-plugin refresh closures are the cleanest extensibility surface I've seen across CLI agents.
  • codex's [model_providers.<id>.auth] = { command, args, refresh_interval_ms } is another shape — declarative shell-out instead of programmatic. Lighter weight; might fit Claude Code's plugin shape better.

Related (not duplicates)

I searched open + closed issues for "auth hook", "plugin auth", "needs-auth", "credential refresh". Closest open ones:

  • #21531 (Before/AfterModel hooks RFC) — different surface; LLM request/response interception, not auth events.
  • #55988 (hook surface before memory injection) — different surface.
  • #41064 (Bedrock auth expiration gets stuck) — symptom fix for one auth surface; this issue would generalize.
  • #48670 (needs-auth cache blocks reconnection after successful re-auth) — bug in existing OAuth flow; this issue is about extending the surface to non-OAuth.

Open to filing this as a "discussion" if that's a better starting point for an extensibility ask. Happy to draft a PR if a maintainer signs off on the shape.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗