Apply auth-expiry hook handling to the non-Claude agent hooks#260
Conversation
Follow-up to #183, which taught the Claude hook to skip the doomed POST and stop nagging when auth has lapsed. The other agent hooks all still built their client with CreateAuthenticatedClientAsync and POSTed blindly, so an expired/absent token meant a guaranteed-to-401 POST plus a misleading per-turn "[kcap] <agent>-hook ...: HTTP 401" stderr line. - New AgentHookPoster shared helper: builds the client via CreateClientWithAuthStatusAsync and returns HookPostOutcome { Posted, AuthLapsed, Failed }. On AuthLapsed it skips the POST (no request, no stderr); Failed keeps the prior stderr line + exit code; Posted is unchanged. - Codex, Gemini, Copilot, Pi, Kiro, OpenCode: PostHookAsync delegates to the helper. Session-start handlers skip the watcher and exit cleanly on AuthLapsed (Codex still emits its required {"continue":true} stdout); Failed/Posted behaviour is unchanged. - Gemini/Copilot per-turn Notification paths and Cursor's HandleInternal use IsAuthLapsed to skip (Cursor also skips the spool drain, so a 401 can't Drop the backlog — it replays after re-login). - No user-facing re-login nudge: none of these agents has a safe stdout notice channel (all emit nothing or a JSON decision/context channel), so per the issue we soft-exit silently; the expired state is surfaced by `kcap status`. No-op for the None provider; unchanged when authenticated. Adds AgentHookPosterTests. Closes #184 Linear: AI-993 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by QodoSkip non-Claude agent hook POSTs when auth has lapsed
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
Context used 1. Response not disposed
|
Independent review (Codex)No actionable findings — the PR looks sound against the stated auth-lapse contract. Key checks:
Coverage gaps (non-blocking, worth a follow-up): Verification: full TUnit suite |
PostWithRetryAsync's HttpResponseMessage was stored in a local and never disposed; wrap it in `using var` so response streams/connections are released on every path (these hooks run per turn/session). Matches the disposal pattern used elsewhere for PostWithRetryAsync responses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What & why
Follow-up to #183. That PR taught the Claude hook to skip the doomed POST and stop nagging when auth has lapsed (expired refresh credential, or never logged in). The other 7 agent hooks all still built their client with
CreateAuthenticatedClientAsyncand POSTed blindly, so an expired/absent token meant (a) a guaranteed-to-401 POST, and (b) a misleading per-turn[kcap] <agent>-hook <endpoint>: HTTP 401stderr line — for the fail-open agents it also drained the Cursor spool into 401→Drop, discarding the backlog.How
AgentHookPostershared helper — builds the client viaCreateClientWithAuthStatusAsyncand returnsHookPostOutcome { Posted, AuthLapsed, Failed }. OnAuthLapsedit skips the POST entirely (no request, no stderr);Failedkeeps the prior stderr line + exit code;Postedis unchanged. Single tested seam for the "skip the doomed POST" decision.PostHookAsyncdelegates to the helper. Session-start handlers skip the watcher and exit cleanly onAuthLapsed(would only 401 too); Codex still emits its required{"continue":true}stdout.Failed/Postedbehaviour is unchanged (real failures keep their exit codes; Kiro/OpenCode keep their fail-open0).Notificationand Cursor'sHandleInternaluseAgentHookPoster.IsAuthLapsedto skip. Cursor additionally skips the spool drain on a lapse, so a 401 can'tDropthe backlog — it replays afterkcap login.kcap statusalready reports✗ token expired (run: kcap login).Acceptance criteria
HTTP 401stderr line (or error banner) solely because auth lapsed.Noneauth provider and unchanged when authenticated.dotnet publish -c Releaseshows no IL3050/IL2026 warnings; unit tests pass.Testing
AgentHookPosterTests(WireMock + injected(client, status)factory): auth-lapse skips the POST (server sees 0 requests),Ok/NoAuthRequiredpost the body, server error →Failed.Known follow-up (out of scope, pre-existing)
On session-end during a lapse, the pre-POST drain path (
InlineDrainAsync,GeminiSubagentTeardown.DrainAsync,SpawnCopilotFinalizeDrain) still usesCreateAuthenticatedClientAsyncinternally, so it emits the "expired" stderr line and fires a doomed drain POST. This lives in the watcher/drain layer (not the hooks), is unchanged by this PR, and fires once per session (not per-turn). Worth gating on auth status in a follow-up.No README/CLI-surface change: internal hook behaviour only (no new command, flag, default, or prerequisite).
Closes #184
Linear: AI-993