Skip to content

Tell the user to run kcap login when a hook is rejected with HTTP 401 - #513

Merged
alexeyzimarev merged 11 commits into
mainfrom
alexeyzimarev/ai-1835-hook-401-login-nudge
Aug 10, 2026
Merged

Tell the user to run kcap login when a hook is rejected with HTTP 401#513
alexeyzimarev merged 11 commits into
mainfrom
alexeyzimarev/ai-1835-hook-401-login-nudge

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Closes #509

AI-1835

The bug

A credential lapsed mid-session and Claude Code showed only this:

⏺ Ran 2 stop hooks (ctrl+o to expand)
  ⎿  Stop hook error: Failed with non-blocking status code: HTTP 401

Nothing said recording had stopped, and nothing said kcap login. Finding out took a hunch and a manual kcap whoami.

There are two distinct auth lapses, and only one was handled:

Lapse How it's detected Behaviour before
The local token store knows the credential is dead (Expired / NotAuthenticated / WrongServer) pre-flight, before any POST exit 0 + a systemMessage nudge — but only on session-start
The store thinks it's usable; the server rejects it (401) only from the response no nudge on any event

This was the second. CreateClientWithAuthStatusAsync reports AuthStatus.Ok for any locally-valid token, and the hook path builds its client with autoRetryUnauthorized: false, so the 401 was final — it fell to the shared failure arm, which wrote a bare HTTP 401 to stderr and returned 1. Claude Code renders any non-zero, non-2 hook exit as that opaque banner. The other events were worse than opaque: session-start, session-end and subagent-stop class a 401 as permanent, drop the payload, and return 0 without a word.

The fix

A 401 becomes a recognized outcome instead of a generic failure.

  • stop and session-start exit 0 and write {"systemMessage": "[kcap] The server rejected your credentials (HTTP 401) — session recording is paused. Run 'kcap login' to resume."}. Exit 0 is load-bearing: it's what replaces the hook-error banner with a clean notice.
  • Other events on the shared path (notification, subagent-start) exit 0 silently. notification fires on every permission prompt, so nudging there would stack duplicate notices inside one turn. No throttle state on disk is needed as a result.
  • Non-401 failures are untouched — a 500 keeps its bare-status stderr line and exit 1, with a characterization test guarding that.
  • The seven non-Claude vendors (Codex, Cursor, Gemini, Copilot, Pi, Kiro, OpenCode — and Antigravity, which shares the seam) can't carry a systemMessage: their stdout is a strict handshake contract the vendor parses. Their stderr line now names the fix instead. Both AgentHookPoster entry points were edited, so it applies whichever one a vendor uses.
  • All notice wording moved into one Core type, AuthLapseNotice, so the pre-flight nudge and the server-rejection nudge can't drift apart.

decision: "block" was deliberately not used. It's the only way to hand text to the agent from a Stop hook, but it stops Claude from stopping and costs an extra model turn per lapsed turn. The user is the one who runs kcap login, so the notice goes to the user.

401 only — a 403 is an authorization decision, not a dead credential, and kcap login wouldn't fix it.

Deliberately out of scope

Tests

New coverage: stop/401 → exit 0 + notice; session-start/401 → exit 0 + notice; notification/401 → exit 0, no notice; stop/500 → exit 1, no notice; the vendor 401 stderr line (and its non-401 bare-status counterpart); and the pre-flight lapse arm, which turned out to have had no test at all despite the design assuming one — including the WrongServer → "not authenticated" mapping.

ClaudeHookCommandTests 44/44, AgentHookPosterTests 9/9, AuthLapseNoticeTests 5/5, integration 217/217. dotnet publish -c Release shows no IL3050/IL2026.

The wider unit suite has ~63 failures on main itself in a local macOS environment (Codex config.toml, kcap uninstall, CLI-runner flood tests, daemon teardown/quarantine timing), churning in both directions run-to-run; this branch measured 62-66 with none in hook, 401, or auth-lapse tests. CI is the authority on those.

One thing a reviewer should know

The design rests on Claude Code honouring systemMessage for the Stop event. That's documented in the installed CLI bundle ("systemMessage — Display a message to the user (all hooks)", with a worked Stop example), and kcap already ships a systemMessage write on the session-start path. It has not been confirmed end-to-end interactively, because headless -p doesn't fire Stop hooks at all. If the contract doesn't hold for Stop, the failure is silent — the user sees nothing where they previously saw an opaque banner. The session-start half is an improvement either way, since it was previously silent.

🤖 Generated with Claude Code

alexeyzimarev and others added 8 commits August 10, 2026 16:18
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>
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>
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>
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

AI-1835

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Show 'kcap login' guidance when hooks get HTTP 401 (Claude + vendors)

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Treat hook HTTP 401 as an auth-lapse outcome, not a generic failure
• For Claude stop/session-start, emit a systemMessage and exit 0 to avoid opaque banners
• For non-Claude vendors, keep outcomes but make stderr actionable; document and test wording
Diagram

graph TD
  A["Agent hook event"] --> B["ClaudeHookCommand"] --> C{"HTTP 401?"}
  C -->|"yes"| D["AuthLapseNotice"] --> E["Claude stdout systemMessage"] --> F["Claude UI notice"]
  C -->|"no"| G["stderr + exit 1"]
  A --> H["AgentHookPoster"] --> I{"HTTP 401?"} -->|"yes"| D --> J["Vendor stderr guidance"]
  I -->|"no"| K["Vendor stderr status"]
  subgraph Legend
    direction LR
    _cmd["Command/module"] ~~~ _dec{"Decision"} ~~~ _srv[("Remote server")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Retry with token recovery on 401
  • ➕ Could self-heal in cases where local token was stale but refreshable
  • ➕ May avoid user interruption if recovery succeeds
  • ➖ Risks burning single-use refresh tokens on per-turn hook traffic
  • ➖ Adds complexity and potentially changes failure semantics beyond “message only”
  • ➖ Does not help for revoked sessions/org mismatch (common causes of server-side 401)
2. Persisted throttle/dedup state for notices
  • ➕ Could safely nudge on more event types without spamming the UI
  • ➕ More control over frequency across turns/sessions
  • ➖ Introduces new on-disk state and edge cases (cleanup, concurrency, corruption)
  • ➖ Not needed if nudges are limited to once-per-turn events (stop/session-start)
3. Use Claude 'decision: block' with a reason
  • ➕ Routes message to the agent channel with stronger visibility in some flows
  • ➖ Alters Claude stop semantics (extra model turn / loop hazard)
  • ➖ Behaviorally riskier than a user-facing notice with exit 0

Recommendation: The PR’s approach is the best tradeoff: treat only HTTP 401 as a recognized auth-lapse outcome, keep non-401 failures unchanged, and use the least invasive channel per agent (Claude systemMessage+exit 0; vendor stderr guidance). The alternatives either change semantics/risk token usage, add unnecessary state, or introduce problematic Claude blocking behavior.

Files changed (9) +1079 / -12

Bug fix (2) +39 / -9
AgentHookPoster.csMake vendor hook 401 stderr output actionable +11/-5

Make vendor hook 401 stderr output actionable

• Rewords stderr output on hook failures by routing all status printing through AuthLapseNotice.VendorStderrLine. Keeps HookPostOutcome and exit behavior unchanged while adding explicit 'run kcap login' guidance for HTTP 401.

src/Capacitor.Cli/Commands/AgentHookPoster.cs

ClaudeHookCommand.csEmit Claude systemMessage on server-side 401 for stop/session-start +28/-4

Emit Claude systemMessage on server-side 401 for stop/session-start

• Uses AuthLapseNotice for pre-flight auth lapse messages to avoid string drift. Adds explicit handling for HTTP 401 responses: stop and session-start emit a systemMessage and exit 0 (preventing Claude’s opaque hook-error banner), while other events on the shared path exit 0 silently to avoid duplicate notices.

src/Capacitor.Cli/Commands/ClaudeHookCommand.cs

Refactor (1) +41 / -0
AuthLapseNotice.csCentralize auth-lapse notice strings and vendor stderr formatting +41/-0

Centralize auth-lapse notice strings and vendor stderr formatting

• Creates a Core type that holds the existing pre-flight notice strings plus the new server-rejection (401) notice, and provides a single formatter for vendor stderr lines to prevent drift between call sites.

src/Capacitor.Cli.Core/AuthLapseNotice.cs

Tests (3) +212 / -0
AgentHookPosterTests.csTest vendor stderr guidance on HTTP 401 without changing outcomes +34/-0

Test vendor stderr guidance on HTTP 401 without changing outcomes

• Adds a serialized unit test that captures Console.Error and asserts that a 401 still reports HookPostOutcome.Failed but prints the AuthLapseNotice vendor guidance line.

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

AuthLapseNoticeTests.csLock auth-lapse notice wording and vendor stderr line format +46/-0

Lock auth-lapse notice wording and vendor stderr line format

• Adds unit tests asserting the exact user-facing notice strings (expired, not-authenticated, rejected) and verifying vendor stderr formatting behavior for both 401 and non-401 codes.

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

ClaudeHookCommandTests.csAdd coverage for Claude 401 notice behavior and 500 regression guard +132/-0

Add coverage for Claude 401 notice behavior and 500 regression guard

• Extends the suite to verify stop and session-start on HTTP 401 exit 0 and emit the AuthLapseNotice.Rejected systemMessage, while notification remains silent. Adds/retains a regression test ensuring non-401 failures (e.g., 500) still exit non-zero without a notice.

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

Documentation (3) +787 / -3
README.mdDocument in-session 401 re-login guidance behavior +8/-3

Document in-session 401 re-login guidance behavior

• Updates the getting-started section to explain that mid-session server-side token rejection (HTTP 401) now produces an explicit re-login notice in Claude (and stderr guidance for other agents) rather than an opaque hook error.

README.md

2026-08-10-hook-401-login-nudge.mdAdd implementation plan for HTTP 401 login nudge work +631/-0

Add implementation plan for HTTP 401 login nudge work

• Introduces a detailed step-by-step implementation plan covering constraints (401-only, AOT rules), file touch points, test strategy, and manual verification guidance for Claude rendering.

docs/superpowers/plans/2026-08-10-hook-401-login-nudge.md

2026-08-10-hook-401-login-nudge-design.mdAdd design spec for actionable 401 auth-lapse messaging +148/-0

Add design spec for actionable 401 auth-lapse messaging

• Adds the approved design describing the two auth-lapse modes (pre-flight vs server-rejected), the Claude systemMessage mechanism with exit 0, vendor stderr behavior, and explicit non-goals (no spooling/retry semantics changes).

docs/superpowers/specs/2026-08-10-hook-401-login-nudge-design.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99af1235d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md
Comment on lines +117 to +118
error, so you no longer have to run `kcap whoami` to work out why recording stopped. Other agents'
hooks print the same advice to stderr instead of an in-session notice, since not every agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit the login advice from Cursor hooks

When a locally valid credential is rejected with HTTP 401 during a Cursor session, this promise does not hold: CursorHookCommand bypasses AgentHookPoster, and its direct POST paths only convert non-success responses into DrainOutcome.Drop or false without writing the new advice to stderr. Cursor users therefore still receive no explanation or kcap login recovery step—the exact failure this change is intended to address—so the Cursor POST path should recognize 401 as well (or the documentation and claimed vendor scope must exclude Cursor).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99af1235d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md
Comment on lines +117 to +118
error, so you no longer have to run `kcap whoami` to work out why recording stopped. Other agents'
hooks print the same advice to stderr instead of an in-session notice, since not every agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit the login advice from Cursor hooks

When a locally valid credential is rejected with HTTP 401 during a Cursor session, this promise does not hold: CursorHookCommand bypasses AgentHookPoster, and its direct POST paths only convert non-success responses into DrainOutcome.Drop or false without writing the new advice to stderr. Cursor users therefore still receive no explanation or kcap login recovery step—the exact failure this change is intended to address—so the Cursor POST path should recognize 401 as well (or the documentation and claimed vendor scope must exclude Cursor).

Useful? React with 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. AI-1835 in docs 📘 Rule violation ⚙ Maintainability
Description
New documentation includes Linear issue identifiers (AI-1835), which the checklist disallows in
repo comments/docs. This can leak internal tracking references and reduces long-term stability of
public-facing docs.
Code

docs/superpowers/specs/2026-08-10-hook-401-login-nudge-design.md[R5-6]

+**Issue:** #509 / AI-1835
+**Siblings:** #510 (a 401'd lifecycle payload is dropped, not spooled), #511 (the daemon does not
Evidence
Rule 3 prohibits Linear issue identifiers in comments; the added docs explicitly include AI-1835
alongside #509.

CLAUDE.md: Avoid Linear issue numbers in code comments
docs/superpowers/specs/2026-08-10-hook-401-login-nudge-design.md[5-6]
docs/superpowers/plans/2026-08-10-hook-401-login-nudge.md[11-13]

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

## Issue description
New markdown docs reference Linear IDs (e.g., `AI-1835`), which violates the rule to avoid Linear identifiers in comments/docs.

## Issue Context
Use GitHub issue/PR references (e.g., `#509`) when an issue reference is necessary.

## Fix Focus Areas
- docs/superpowers/specs/2026-08-10-hook-401-login-nudge-design.md[5-6]
- docs/superpowers/plans/2026-08-10-hook-401-login-nudge.md[11-13]

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



Remediation recommended

2. Overly verbose new comments ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Several new/modified code comments are very long and narrate behavior that is already clear from
code/tests, adding maintenance overhead. This conflicts with the guideline to keep comments minimal
and prefer self-explanatory code.
Code

src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[R799-802]

+            // A rejected credential is not a transient fault: exit 0 so Claude renders a clean
+            // notice instead of its opaque hook-error banner, and nudge from `stop` only — the
+            // one once-per-turn event on this path. `notification` fires per permission prompt,
+            // so nudging there would stack duplicates within a single turn.
Evidence
Rule 4 asks for minimal, necessary comments; the highlighted additions are multi-line narrative
explanations that could be reduced to a short intent note (or removed) without losing clarity.

CLAUDE.md: Keep code comments minimal; prefer self-explanatory code
src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[799-806]
src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[615-620]
src/Capacitor.Cli/Commands/AgentHookPoster.cs[51-56]

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

## Issue description
New/modified comment blocks are verbose and add ongoing maintenance burden.

## Issue Context
Prefer concise comments that capture non-obvious intent/constraints; rely on naming and tests for the rest.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[799-806]
- src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[615-620]
- src/Capacitor.Cli/Commands/AgentHookPoster.cs[51-56]

ⓘ 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 +5 to +6
**Issue:** #509 / AI-1835
**Siblings:** #510 (a 401'd lifecycle payload is dropped, not spooled), #511 (the daemon does not

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

1. ai-1835 in docs 📘 Rule violation ⚙ Maintainability

New documentation includes Linear issue identifiers (AI-1835), which the checklist disallows in
repo comments/docs. This can leak internal tracking references and reduces long-term stability of
public-facing docs.
Agent Prompt
## Issue description
New markdown docs reference Linear IDs (e.g., `AI-1835`), which violates the rule to avoid Linear identifiers in comments/docs.

## Issue Context
Use GitHub issue/PR references (e.g., `#509`) when an issue reference is necessary.

## Fix Focus Areas
- docs/superpowers/specs/2026-08-10-hook-401-login-nudge-design.md[5-6]
- docs/superpowers/plans/2026-08-10-hook-401-login-nudge.md[11-13]

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

Comment thread src/Capacitor.Cli/Commands/ClaudeHookCommand.cs Outdated
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>
@alexeyzimarev

Copy link
Copy Markdown
Member Author

Bot review responses

✅ Codex — "Emit the login advice from Cursor hooks" (P2): valid, fixed in 0e0d150

This was a real gap and the reasoning behind it was mine, not the implementation's. CursorHookCommand uses AgentHookPoster only for the IsAuthLapsed predicate (:128) and POSTs directly via TryPostHookAsync (:893) plus its own spool-drain lambda (:373), both of which turned a non-success status into false/DrainOutcome.Drop with nothing on stderr. So Cursor would have been the single vendor left with no explanation and no recovery step — precisely the failure this PR exists to remove.

For the record on how it got through: an earlier task review raised exactly this as a "cannot verify from diff" item and asked for a spot-check of the vendor call sites. I dismissed it on the grounds that both poster entry points were edited so the wiring didn't matter — which only holds if the vendor calls one of them. Cursor doesn't.

Fix: TryPostHookAsync now emits the same AuthLapseNotice.VendorStderrLine(...). The drain lambda deliberately stays silent — it replays many entries per pass and would repeat the line for each one, the same duplicate-stacking reason notification doesn't nudge on the Claude path. Two tests added (server_rejected_credential_names_kcap_login_on_stderr, plus a 500 control asserting the bare status line is unchanged, so the 401 assertion is non-vacuous). CursorHookCommandTests 42/42.

The design doc asserted Cursor shared the poster; corrected there with a dated note rather than a silent rewrite.

❌ Qodo — "Linear issue identifiers in docs": not applicable in this repo

The rule this cites is CLAUDE.md's "DO NOT use Linear issue numbers in comments", and it is enforced by the No Linear issue IDs in C# source CI check — which passes on this PR. It is scoped to code, not to design docs, and deliberately so: 43 existing committed specs under docs/superpowers/specs/ carry AI- identifiers, several in the identical **Issue:** AI-nnnn header form (e.g. 2026-06-30-token-store-corruption-resilience-design.md:4, 2026-07-09-reviewer-auto-approve-design.md:3).

There is a reason for the asymmetry: the team convention is that the reviewed spec lives on the Linear issue as the durable record, with the repo copy riding the implementation PR. A spec that cannot name its own Linear issue cannot be traced back to that record. The GitHub number #509 is present too, so nothing depends on a reader having Linear access.

No change.

◐ Qodo — "Overly verbose new comments": partly fair, trimmed in 0e0d150

Trimmed both flagged blocks in ClaudeHookCommand.cs, keeping only what isn't recoverable from the code: why exit 0 rather than non-zero (a future editor "correcting" that exit code silently reverts the whole feature), why stop only, and that the failure arm is the only stdout write because the envelope below is built from a 2xx body.

Left AgentHookPoster.cs:50-56 as-is. Its previous, shorter form actively contradicted the code — it said "no re-login nudge is surfaced here", which stopped being true in this PR — and the current length is what it takes to distinguish a pre-flight lapse (still silent) from a server-returned 401 (now nudges). It also matches the register of the surrounding docs in that file.


Verification after the fix: CursorHookCommandTests 42/42, ClaudeHookCommandTests 44/44, AgentHookPosterTests 9/9, AuthLapseNoticeTests 5/5, AOT publish clean (no IL3050/IL2026).

The caveat in the PR description still stands and is the one thing worth a human's attention: the Stop-renders-systemMessage contract is documented in the installed CLI bundle but has not been confirmed interactively, because headless -p does not fire Stop hooks.

alexeyzimarev and others added 2 commits August 10, 2026 20:15
#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

Copy link
Copy Markdown
Member Author

Folded into AuthRejectionNotice (755f15e), and a note on the CI failure

The fold

#516 landed AuthRejectionNotice on main while this branch was in review. Its StoredCredentialState vocabulary already has a LooksValid case meaning "the server rejected a credential that looks perfectly valid locally" — which is exactly what this branch's Rejected string said in prose. Shipping a second, near-identically-named auth-notice type would have been precisely the drift the type was introduced to prevent, so there is now one type.

The two surfaces keep different renderings of the same states, because their length budgets genuinely differ:

Member Surface Form
Render(state, stored, target) MCP tool results several sentences (unchanged)
RecordingNotice(state) Claude systemMessage, vendor stderr one line
VendorStderrLine(tag, endpoint, code) vendor stderr status line, enriched only on 401
FromAuthStatus(status) hook pre-flight arm maps the AuthStatus the hook already holds

Sharing the enum rather than the prose is deliberate: the states are what must not drift, and a five-sentence paragraph inside a systemMessage would be unreadable. FromAuthStatus matters for a second reason — the hook path is per-turn and budget-bounded, so it must not pay the two disk reads ForPersistentUnauthorizedAsync makes just to name a state it already knows.

No wording changed. Every rendered string is byte-identical, including WrongServer still rendering as the not-authenticated line in the short form. Naming both servers there (as Render does) would be more truthful and is now a one-line change, but it is a behaviour change and not this PR's to make.

AuthLapseNoticeTests folded into AuthRejectionNoticeTests, plus new coverage for the AuthStatus mapping and for the WrongServer short-form choice so it can't be changed silently.

Verification: AuthRejectionNoticeTests 12/12, ClaudeHookCommandTests 44/44, AgentHookPosterTests 9/9, CursorHookCommandTests 42/42, AOT publish clean.

The CI failure is not from this branch

NotAuthenticated_MakesNoRequest_AndReturnsZero (added by #517 / AI-1283, not touched here) fails on main's own CI at tip commit 032f477d9 with the identical assertion, so it is red independently of this PR.

It's order-dependent rather than broken logic: it asserts no HTTP request is made when unauthenticated, but AppConfig.SetResolvedState and the token store are process-global statics and the test carries no NotInParallel. Any test running concurrently that seeds auth makes it issue the request it asserts never happens. It passes 10/10 in isolation. Worth someone's attention as a separate issue — it will redden every PR until it's isolated.

The ubuntu leg also showed McpTelemetryTests.No_argument_data_is_carried (Single() on an empty sink) once — same species, a static TelemetryState.PathOverride guarded only by a keyed NotInParallel. It did not appear in any of four local full-suite runs, nor on main's ubuntu leg. Watching whether it recurs.

Current main is merged in as of 3d7caff.

@alexeyzimarev
alexeyzimarev merged commit a51a555 into main Aug 10, 2026
4 of 6 checks passed
@alexeyzimarev
alexeyzimarev deleted the alexeyzimarev/ai-1835-hook-401-login-nudge branch August 10, 2026 19:07
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.

Hook HTTP 401 surfaces as an opaque hook error and never says kcap login

1 participant