Skip to content

Truthful 401 notice: distinguish a server-rejected valid credential from a missing login - #516

Merged
realtonyyoung merged 1 commit into
mainfrom
tonyyoung/ai-1843-truthful-401-notice
Aug 10, 2026
Merged

Truthful 401 notice: distinguish a server-rejected valid credential from a missing login#516
realtonyyoung merged 1 commit into
mainfrom
tonyyoung/ai-1843-truthful-401-notice

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

What

The six MCP servers' 401 sites now render a store-aware, truthful message instead of the flat Not logged in. Run 'kcap login' on the host shell. A new AuthRejectionNotice (Core) classifies the raw token-store snapshot against the request's target server and renders one of four lines:

Store state Message
Missing the legacy wording, byte-identical
Expired expired + automatic refresh failed → kcap login
Wrong server names issuing + target servers → kcap login / kcap use
Looks valid "the server rejected kcap's credentials (HTTP 401) even after re-reading the token store — yet the stored login for user looks valid locally (expires in Nh)… run kcap loginrestarting the daemon will not help"

Why — AI-1843

During the 2026-08-10 kurrent-tenant incident, the server's ephemeral signing key rotated across pod replacements and 401'd tokens that kcap status concurrently (and correctly) reported as valid. The flows MCP kept printing "Not logged in", the user read the two surfaces as contradicting each other, concluded kcap login was ineffective, and reached for a daemon restart — which never helps, because every kcap process re-reads the token store on 401.

The issue's suspected root cause ("the MCP child caches auth at process start and never re-reads") does not existSendWithRefreshRetryAsync, the daemon's AccessTokenProvider, and the watchers all re-read ~/.config/kcap/tokens/ per 401/negotiate, and the incident logs show each surface healing within seconds of each kcap login. What was genuinely wrong client-side is the message: it told a logged-in user they weren't, and its literal reading sent them away from the one remedy that works. The root cause (the ephemeral signing key) is fixed server-side in kurrent-io/kcap-server#1392.

Notes

  • The classifier is pure (Classify/Render) with the IO wrapper doing a raw, non-mutating store read — building an error string must never rotate a WorkOS refresh token.
  • Analytics keeps its pure MapResponse (unit-tested); the 401 is intercepted at the async call site.
  • The per-server NotLoggedInMessage consts now alias AuthRejectionNotice.NotLoggedIn (single source).
  • Any store fault while building the line degrades to the legacy message.
  • Tests: AuthRejectionNoticeTests (new, 7); ReviewerVendorFallbackTests 44/44, McpAnalyticsServerTests 15/15, McpFlowsServerTests 52/52, McpSessionsServerTests (integration) 10/10 unchanged; scripts/check-linear-ids.sh clean.

Part of AI-1843.

🤖 Generated with Claude Code

The MCP servers answered every post-recovery 401 with "Not logged in.
Run 'kcap login' on the host shell." — factually wrong in the incident
that motivated this (AI-1843): the tenant server's ephemeral signing key
rotated across pod replacements and 401'd tokens that `kcap status`
concurrently (and correctly) reported as valid. Users read the two
surfaces as contradicting each other, concluded `kcap login` was
ineffective, and reached for a daemon restart — which never helps,
because every kcap process re-reads the same token store on 401 (the
issue's suspected "caches auth at process start" bug does not exist; the
recovery path was working as designed on every surface).

The new AuthRejectionNotice classifies the raw store snapshot (never the
refresh-aware accessor — building an error string must not rotate a
credential) and renders one of four truthful lines:

- Missing      -> the legacy wording, byte-identical;
- Expired      -> expired + refresh failed, run kcap login;
- WrongServer  -> names both servers, offers kcap login / kcap use;
- LooksValid   -> the server rejected a locally-valid credential: name
                  the signed-in user and expiry, point at a server-side
                  auth change, prescribe kcap login, and say explicitly
                  that restarting the daemon will not help.

All six MCP servers' 401 sites route through it (analytics keeps its
pure MapResponse; the 401 is intercepted at the async call site), and
the per-server NotLoggedInMessage consts now alias the single source.
Any store fault while building the line degrades to the legacy message.

Part of AI-1843 (the root cause — persisting the signing key — is
kcap-server #1392).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

AI-1843

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Truthful MCP 401 notices via token-store-aware AuthRejectionNotice

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Replace MCP 401 “Not logged in” with token-store-aware, truthful diagnostics.
• Centralize 401 classification/rendering in Core, preserving legacy text for missing logins.
• Add unit tests covering missing/expired/wrong-server/locally-valid credential scenarios.
Diagram

graph TD
  A["MCP servers"] --> B["AuthRejectionNotice"] --> C["TokenStore (raw read)"] --> D[("Token files")]
  A --> E{{"kcap-server API"}}
  B --> F["ServerIdentity"]
  subgraph Legend
    direction LR
    _proc["Component"] ~~~ _db[("Storage")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Return structured auth diagnosis from TokenStore/HttpClient factory
  • ➕ Keeps 401 messaging consistent across all callers, not just MCP servers
  • ➕ Avoids per-call-site async store reads by carrying diagnostic context (e.g., TokenResolution) alongside requests
  • ➖ Harder to guarantee “non-mutating” behavior (must ensure no refresh/rotation just to build text)
  • ➖ Broader refactor risk; MCP servers intentionally bypass some shared retry paths (autoRetryUnauthorized: false)
2. Server-provided 401 problem details (RFC-7807)
  • ➕ Most accurate root-cause messaging when the server knows why the token was rejected
  • ➕ No local token-store IO needed for messaging
  • ➖ Requires server changes and consistent rollout across environments
  • ➖ Still needs client fallback for older servers/proxies that return plain 401s

Recommendation: The PR’s approach is appropriate: a centralized, pure classifier/renderer with an IO wrapper that performs a raw (non-refreshing) store read and degrades to the legacy message on faults. It achieves truthful UX without risking token rotation as a side effect of error formatting, and the MCP call sites can remain in control of their response-mapping purity and retry behavior.

Files changed (8) +246 / -19

Enhancement (1) +99 / -0
AuthRejectionNotice.csAdd store-aware 401 classification and user-facing messaging +99/-0

Add store-aware 401 classification and user-facing messaging

• Introduces StoredCredentialState and AuthRejectionNotice to classify a raw StoredTokens snapshot against the target server and render a truthful 401 guidance line. Includes an async helper that reads the current profile’s token file without triggering refresh/rotation and falls back to the legacy message on any store fault.

src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs

Bug fix (6) +31 / -19
McpAnalyticsServer.csIntercept 401s to return store-aware AuthRejectionNotice text +9/-1

Intercept 401s to return store-aware AuthRejectionNotice text

• Aliases the legacy NotLoggedInMessage constant to AuthRejectionNotice.NotLoggedIn for a single source of truth. Adds an explicit 401 interception at the async call site to fetch the store-aware notice while keeping MapResponse pure and unit-testable.

src/Capacitor.Cli/Commands/McpAnalyticsServer.cs

McpFlowResultServer.csUse AuthRejectionNotice for persistent 401 tool results +2/-2

Use AuthRejectionNotice for persistent 401 tool results

• Replaces hard-coded “Not logged in” responses on 401 with an async call to AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot), improving guidance when a locally-valid credential is rejected by the server.

src/Capacitor.Cli/Commands/McpFlowResultServer.cs

McpFlowsServer.csReplace hard-coded 401 messaging with store-aware AuthRejectionNotice +7/-6

Replace hard-coded 401 messaging with store-aware AuthRejectionNotice

• Updates multiple 401 handling sites (POST paths, retry paths, and polling/status paths) to return AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot). Adjusts inline documentation to reflect that the surfaced 401 message is now store-aware and preserves the legacy wording only for truly missing credentials.

src/Capacitor.Cli/Commands/McpFlowsServer.cs

McpMemoryServer.csReturn AuthRejectionNotice text for 401 responses +4/-3

Return AuthRejectionNotice text for 401 responses

• Points the NotLoggedInMessage constant at AuthRejectionNotice.NotLoggedIn and replaces 401 tool-result responses with AuthRejectionNotice.ForPersistentUnauthorizedAsync(baseUrl). Updates comments to document the new store-aware 401 output contract.

src/Capacitor.Cli/Commands/McpMemoryServer.cs

McpSessionsServer.csUpgrade 401 responses to store-aware AuthRejectionNotice messaging +5/-4

Upgrade 401 responses to store-aware AuthRejectionNotice messaging

• Aliases NotLoggedInMessage to AuthRejectionNotice.NotLoggedIn and replaces 401 tool-result paths (including the search auto-widen flow) with AuthRejectionNotice.ForPersistentUnauthorizedAsync(baseUrl). Updates the 401 retry helper documentation accordingly.

src/Capacitor.Cli/Commands/McpSessionsServer.cs

McpWorkItemsServer.csUse AuthRejectionNotice for 401 tool errors +4/-3

Use AuthRejectionNotice for 401 tool errors

• Aliases NotLoggedInMessage to AuthRejectionNotice.NotLoggedIn and replaces hard-coded 401 output with AuthRejectionNotice.ForPersistentUnauthorizedAsync(baseUrl). Updates comments to reflect that 401 messaging is now store-aware while remaining safe on failures.

src/Capacitor.Cli/Commands/McpWorkItemsServer.cs

Tests (1) +116 / -0
AuthRejectionNoticeTests.csAdd unit tests pinning truthful 401 notice classification and rendering +116/-0

Add unit tests pinning truthful 401 notice classification and rendering

• Adds tests validating classification across Missing/Expired/WrongServer/LooksValid states and ensuring the missing-login message remains byte-identical. Covers server-identity normalization (e.g., :443) and asserts the “looks valid locally” message includes user, expiry context, login remedy, and the explicit daemon-restart warning.

test/Capacitor.Cli.Tests.Unit/AuthRejectionNoticeTests.cs

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Capability 401 misdiagnosed 🐞 Bug ≡ Correctness
Description
In McpFlowResultServer borrowed-reviewer (daemon capability) mode, a 401 now calls
AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot), which reads the local token store and
can render “WrongServer/LooksValid” diagnoses even though this process intentionally does not
authenticate and may not even be talking to the server. This can mislead users and add unnecessary
IO on the only result-delivery channel.
Code

src/Capacitor.Cli/Commands/McpFlowResultServer.cs[249]

+                return (await AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot), true);
Evidence
Borrowed mode is explicitly described as not authenticating and not having access to the token
store, yet the new 401 path invokes a helper that always resolves the profile and reads tokens from
disk, which is incompatible with that mode and can produce irrelevant diagnostics.

src/Capacitor.Cli/Commands/McpFlowResultServer.cs[63-69]
src/Capacitor.Cli/Commands/McpFlowResultServer.cs[121-126]
src/Capacitor.Cli/Commands/McpFlowResultServer.cs[244-250]
src/Capacitor.Cli/Commands/McpFlowResultServer.cs[321-326]
src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[79-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`McpFlowResultServer` supports a borrowed-reviewer mode that delivers through a daemon capability URL and **explicitly does not use the token store**. The PR changed both 401 handlers to call `AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot)`, which reads the token store and classifies credentials against `apiRoot`, producing misleading “server rejected credentials / wrong server” messaging for capability-based requests.

### Issue Context
- Borrowed mode is chosen when `KCAP_FLOW_CAPABILITY_URL` is set; in this mode the client is a plain `HttpClient` and must not depend on token-store access.
- On a 401 in borrowed mode, the message should not attempt token-store classification.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/McpFlowResultServer.cs[248-250]
- src/Capacitor.Cli/Commands/McpFlowResultServer.cs[324-326]

### Suggested change
- In both 401 branches, gate the call:
 - If `submitUrlOverride`/`messageUrlOverride` is non-null (borrowed mode), return `AuthRejectionNotice.NotLoggedIn` or a capability-specific 401 line (no token-store read).
 - Otherwise (normal mode), keep `await AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot)`.
- Keep the tool result marked as error (`true`) as today.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Verbose AuthRejectionNotice XML comment 📘 Rule violation ⚙ Maintainability
Description
Verbose incident narratives were added to XML documentation in both the production code and the
related unit test, going beyond what’s needed to convey non-obvious intent or constraints. This
reduces readability and conflicts with the guideline to keep comments minimal and high-signal.
Code

src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[R23-26]

+/// <para>The MCP servers used to answer every such 401 with a flat
+/// "Not logged in. Run 'kcap login' on the host shell." — factually wrong in the incident
+/// that motivated this: a server-side signing-key rotation 401'd a token that
+/// <c>kcap status</c> concurrently (and correctly) reported as valid, so the user read the
Evidence
PR Compliance ID 5 requires keeping comments brief and avoiding verbose narration in favor of
concise, high-signal intent/constraint statements. The cited XML documentation added in
src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs contains a lengthy incident story and
behavioral explanation, and the file-level XML doc comment in
test/Capacitor.Cli.Tests.Unit/AuthRejectionNoticeTests.cs similarly includes a multi-line
incident/user-behavior narrative; together, these citations show the comments are more expansive
than necessary and should be shortened to succinct descriptions of purpose and test coverage.

CLAUDE.md: Keep code comments minimal; prefer self-explanatory code over verbose commentary
src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[19-31]
test/Capacitor.Cli.Tests.Unit/AuthRejectionNoticeTests.cs[5-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Verbose XML documentation in `AuthRejectionNotice` and `AuthRejectionNoticeTests` narrates historical incident/background context rather than briefly stating non-obvious intent, constraints, and what the tests cover; comments should be reduced to minimal, high-signal documentation.

## Issue Context
PR Compliance ID 5 prefers brief, focused comments that add value beyond what the code already expresses, avoiding lengthy narratives and historical storytelling; replace the incident-style prose with concise statements of intent/constraints for production code and concise descriptions of test assertions/coverage for the unit test.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[19-31]
- test/Capacitor.Cli.Tests.Unit/AuthRejectionNoticeTests.cs[5-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Token-store IO masked 🐞 Bug ☼ Reliability
Description
AuthRejectionNotice.ForPersistentUnauthorizedAsync catches all exceptions and returns the legacy
NotLoggedIn text, masking real IO/permission/cancellation failures during profile/token reads as
“not logged in”. This contradicts TokenStore’s documented rule that real IO/permission faults must
not be masked as unauthenticated, and it removes critical debugging signal.
Code

src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[R85-87]

+        } catch {
+            return NotLoggedIn;
+        }
Evidence
The new helper explicitly swallows all exceptions from token-store reads and maps them to
NotLoggedIn, which conflicts with TokenStore’s stated contract that real IO/permission errors must
not be masked as unauthenticated.

src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[73-87]
src/Capacitor.Cli.Core/Auth/TokenStore.cs[86-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AuthRejectionNotice.ForPersistentUnauthorizedAsync` currently uses a bare `catch` and returns `NotLoggedIn` for *any* failure while resolving the profile or reading tokens. This hides real token-store/config IO and permission issues (and may also swallow cancellation), producing a misleading authentication diagnosis.

### Issue Context
`TokenStore` explicitly distinguishes missing/corrupt files from true IO/permission faults and documents that the latter must propagate and must not be treated as unauthenticated.

### Fix Focus Areas
- src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[79-87]
- src/Capacitor.Cli.Core/Auth/TokenStore.cs[86-93]

### Suggested change
- Replace `catch { return NotLoggedIn; }` with targeted handling:
 - Re-throw `OperationCanceledException` so cancellations behave normally.
 - For `IOException` / `UnauthorizedAccessException` (and possibly config-load exceptions), return a **generic, non-path-leaking** message like: "Could not read kcap token store (check file permissions). Run 'kcap login' after fixing access." or similar.
 - Optionally keep `NotLoggedIn` only for truly expected, non-actionable cases (though most expected cases already return `null` from TokenStore without throwing).
- Avoid including exception messages in the tool output to prevent leaking local paths.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +23 to +26
/// <para>The MCP servers used to answer every such 401 with a flat
/// "Not logged in. Run 'kcap login' on the host shell." — factually wrong in the incident
/// that motivated this: a server-side signing-key rotation 401'd a token that
/// <c>kcap status</c> concurrently (and correctly) reported as valid, so the user read the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Verbose authrejectionnotice xml comment 📘 Rule violation ⚙ Maintainability

Verbose incident narratives were added to XML documentation in both the production code and the
related unit test, going beyond what’s needed to convey non-obvious intent or constraints. This
reduces readability and conflicts with the guideline to keep comments minimal and high-signal.
Agent Prompt
## Issue description
Verbose XML documentation in `AuthRejectionNotice` and `AuthRejectionNoticeTests` narrates historical incident/background context rather than briefly stating non-obvious intent, constraints, and what the tests cover; comments should be reduced to minimal, high-signal documentation.

## Issue Context
PR Compliance ID 5 prefers brief, focused comments that add value beyond what the code already expresses, avoiding lengthy narratives and historical storytelling; replace the incident-style prose with concise statements of intent/constraints for production code and concise descriptions of test assertions/coverage for the unit test.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[19-31]
- test/Capacitor.Cli.Tests.Unit/AuthRejectionNoticeTests.cs[5-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


if (response.StatusCode == HttpStatusCode.Unauthorized)
return ("Not logged in. Run 'kcap login' on the host shell.", true);
return (await AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot), true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Capability 401 misdiagnosed 🐞 Bug ≡ Correctness

In McpFlowResultServer borrowed-reviewer (daemon capability) mode, a 401 now calls
AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot), which reads the local token store and
can render “WrongServer/LooksValid” diagnoses even though this process intentionally does not
authenticate and may not even be talking to the server. This can mislead users and add unnecessary
IO on the only result-delivery channel.
Agent Prompt
### Issue description
`McpFlowResultServer` supports a borrowed-reviewer mode that delivers through a daemon capability URL and **explicitly does not use the token store**. The PR changed both 401 handlers to call `AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot)`, which reads the token store and classifies credentials against `apiRoot`, producing misleading “server rejected credentials / wrong server” messaging for capability-based requests.

### Issue Context
- Borrowed mode is chosen when `KCAP_FLOW_CAPABILITY_URL` is set; in this mode the client is a plain `HttpClient` and must not depend on token-store access.
- On a 401 in borrowed mode, the message should not attempt token-store classification.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/McpFlowResultServer.cs[248-250]
- src/Capacitor.Cli/Commands/McpFlowResultServer.cs[324-326]

### Suggested change
- In both 401 branches, gate the call:
  - If `submitUrlOverride`/`messageUrlOverride` is non-null (borrowed mode), return `AuthRejectionNotice.NotLoggedIn` or a capability-specific 401 line (no token-store read).
  - Otherwise (normal mode), keep `await AuthRejectionNotice.ForPersistentUnauthorizedAsync(apiRoot)`.
- Keep the tool result marked as error (`true`) as today.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +85 to +87
} catch {
return NotLoggedIn;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Token-store io masked 🐞 Bug ☼ Reliability

AuthRejectionNotice.ForPersistentUnauthorizedAsync catches all exceptions and returns the legacy
NotLoggedIn text, masking real IO/permission/cancellation failures during profile/token reads as
“not logged in”. This contradicts TokenStore’s documented rule that real IO/permission faults must
not be masked as unauthenticated, and it removes critical debugging signal.
Agent Prompt
### Issue description
`AuthRejectionNotice.ForPersistentUnauthorizedAsync` currently uses a bare `catch` and returns `NotLoggedIn` for *any* failure while resolving the profile or reading tokens. This hides real token-store/config IO and permission issues (and may also swallow cancellation), producing a misleading authentication diagnosis.

### Issue Context
`TokenStore` explicitly distinguishes missing/corrupt files from true IO/permission faults and documents that the latter must propagate and must not be treated as unauthenticated.

### Fix Focus Areas
- src/Capacitor.Cli.Core/Auth/AuthRejectionNotice.cs[79-87]
- src/Capacitor.Cli.Core/Auth/TokenStore.cs[86-93]

### Suggested change
- Replace `catch { return NotLoggedIn; }` with targeted handling:
  - Re-throw `OperationCanceledException` so cancellations behave normally.
  - For `IOException` / `UnauthorizedAccessException` (and possibly config-load exceptions), return a **generic, non-path-leaking** message like: "Could not read kcap token store (check file permissions). Run 'kcap login' after fixing access." or similar.
  - Optionally keep `NotLoggedIn` only for truly expected, non-actionable cases (though most expected cases already return `null` from TokenStore without throwing).
- Avoid including exception messages in the tool output to prevent leaking local paths.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@realtonyyoung
realtonyyoung merged commit 032f477 into main Aug 10, 2026
5 of 6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/ai-1843-truthful-401-notice branch August 10, 2026 17:50
alexeyzimarev added a commit that referenced this pull request Aug 10, 2026
#516 landed AuthRejectionNotice on main while this branch was in review,
carrying a StoredCredentialState vocabulary whose LooksValid case is
exactly this branch's "the server rejected a locally-valid credential".
Two near-identically-named auth-notice types is the drift the original
type existed to prevent, so there is now one.

The surfaces keep different renderings of the shared states, because
their length budgets genuinely differ: Render() stays the MCP form
(several sentences in a tool result), RecordingNotice() is the one-line
form a Claude systemMessage and a vendor stderr line can carry.
FromAuthStatus() maps the AuthStatus the hook already holds onto the
states, so the per-turn hook path pays none of the disk reads
ForPersistentUnauthorizedAsync makes to classify.

No wording changes: every rendered string is byte-identical, including
WrongServer still rendering as the not-authenticated line in the short
form. Naming both servers there would be more truthful and is now a
one-line follow-up, but it is a behaviour change and not this PR's.

AuthLapseNoticeTests folded into AuthRejectionNoticeTests, plus coverage
for the AuthStatus mapping and the WrongServer short-form choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alexeyzimarev added a commit that referenced this pull request Aug 10, 2026
…#513)

* Design: actionable re-login nudge on a hook HTTP 401

A credential the local token store believes is usable but the server
rejects surfaces today as an opaque Claude hook-error banner (bare
"HTTP 401", exit 1) or, on session-start/session-end, as complete
silence. Neither says recording has stopped, and neither says
`kcap login`.

Spec rides the implementation PR per the repo spec convention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Plan: hook HTTP 401 re-login nudge

Five tasks: collect the notice wording into a Core type (verbatim move of
the two existing strings), nudge from the shared stop path, nudge from the
session-start arm that drops in silence today, make the vendor stderr line
actionable, then README plus full-suite and AOT verification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Collect the auth-lapse notice wording into one Core type

* Nudge the user to re-login when a stop hook is rejected with 401

* Say why session-start was dropped when the server rejects the credential

* Name the fix on the vendor hooks' 401 stderr line

* Document the in-agent notice for a server-rejected credential

* Fix wave from the final whole-branch review of the hook-401 login nudge

Tightens the AI-1835 branch before PR: replaces a NotInParallel key that
protected nothing with a bare serialization, corrects the README and an
AgentHookPoster doc comment now made false by the change, folds the
duplicated 401-vs-other stderr ternary into AuthLapseNotice.VendorStderrLine
so the two call sites can't diverge, and adds coverage for the pre-flight
auth-lapse arm of ClaudeHookCommand.HandleCore that had none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Emit the re-login nudge from Cursor's own POST path too

Cursor was the one vendor the nudge missed. It does not route its
recording POST through AgentHookPoster — it POSTs directly and uses the
poster only for the IsAuthLapsed predicate — so a 401 returned
false/DrainOutcome.Drop in silence, leaving Cursor users with exactly the
unexplained failure this change exists to remove.

TryPostHookAsync (the live path) now writes the same stderr line. The
spool-drain lambda stays silent on purpose: it replays many entries per
pass and would repeat the line for each one.

Found by the Codex PR reviewer. The design doc had asserted Cursor shared
the poster; corrected there with a note rather than a silent rewrite.

Also trims two comment blocks flagged as over-narrated, keeping the
load-bearing rationale (why exit 0, why stop-only, why this is the arm's
only stdout write).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fold AuthLapseNotice into AuthRejectionNotice

#516 landed AuthRejectionNotice on main while this branch was in review,
carrying a StoredCredentialState vocabulary whose LooksValid case is
exactly this branch's "the server rejected a locally-valid credential".
Two near-identically-named auth-notice types is the drift the original
type existed to prevent, so there is now one.

The surfaces keep different renderings of the shared states, because
their length budgets genuinely differ: Render() stays the MCP form
(several sentences in a tool result), RecordingNotice() is the one-line
form a Claude systemMessage and a vendor stderr line can carry.
FromAuthStatus() maps the AuthStatus the hook already holds onto the
states, so the per-turn hook path pays none of the disk reads
ForPersistentUnauthorizedAsync makes to classify.

No wording changes: every rendered string is byte-identical, including
WrongServer still rendering as the not-authenticated line in the short
form. Naming both servers there would be more truthful and is now a
one-line follow-up, but it is a behaviour change and not this PR's.

AuthLapseNoticeTests folded into AuthRejectionNoticeTests, plus coverage
for the AuthStatus mapping and the WrongServer short-form choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant