Background
PR #183 fixed auth-expiry handling for the Claude Code hook (ClaudeHookCommand): when the token is expired and can't be refreshed, it no longer POSTs a request the server will 401, no longer exits non-zero (which Claude Code renders as a red "Stop hook error" banner every turn), and nudges the user to re-login at most once per session via a systemMessage JSON object.
That work added a reusable seam in src/Capacitor.Cli.Core/HttpClientExtensions.cs:
enum AuthStatus { Ok, NoAuthRequired, Expired, NotAuthenticated }
CreateClientWithAuthStatusAsync(...) → (HttpClient, AuthStatus) — same as CreateAuthenticatedClientAsync but reports the outcome instead of swallowing it.
This issue is to carry the same handling to the other 7 agent hooks, which all still build their client with CreateAuthenticatedClientAsync() and POST blindly.
The shared problem
All non-Claude hook commands under src/Capacitor.Cli/Commands/ build the client via CreateAuthenticatedClientAsync() and, on a non-success response, write [kcap] <agent>-hook {endpoint}: HTTP {status} to stderr. When auth has lapsed this means: (a) a guaranteed-to-401 POST is still sent, and (b) a stderr line is logged on every affected event. How visible (b) is depends on each agent's hook system.
Goal (per hook)
- Switch the client build to
CreateClientWithAuthStatusAsync.
- When status is
Expired / NotAuthenticated: skip the POST (it will 401) and return the agent's clean/no-op exit code.
- Surface a re-login nudge at most once per session — on the session-start-equivalent event only, never on per-turn events — using that agent's own user-facing channel if one exists (see caveat).
Per-agent checklist (line numbers approximate)
Important caveats
- The
systemMessage notice channel is Claude-Code-specific. Do not copy it verbatim. Most of these agents emit nothing on stdout or treat stdout as a JSON decision/context channel — injecting a notice there would corrupt that channel or leak into the agent's context. For each agent, either find its real user-facing notice channel or, if none exists, soft-exit silently and rely on kcap status (already reports ✗ token expired (run: kcap login)) and the interactive CLI's own messaging. Surfacing the once-per-session nudge is best-effort and may be N/A for several agents.
- Severity varies. Cursor, Kiro, and OpenCode are already fail-open at the process level, so they don't produce a per-turn error banner today — for them this is mostly hygiene (don't send a doomed request, don't log a misleading
HTTP 401 line). Codex/Gemini/Copilot/Pi return 1; whether their host renders that as a user-visible error is per-agent and worth confirming.
- Consider factoring the shared decision ("given
AuthStatus + whether this is the session-start event, should I POST / what to emit / what exit code") into a small helper so the 7 hooks don't each reimplement it.
Acceptance criteria
Reference
Template: PR #183 (ClaudeHookCommand.cs skip-and-exit-0 + systemMessage, and HttpClientExtensions.CreateClientWithAuthStatusAsync). Related: #182 (proactive daemon token refresh).
Background
PR #183 fixed auth-expiry handling for the Claude Code hook (
ClaudeHookCommand): when the token is expired and can't be refreshed, it no longer POSTs a request the server will 401, no longer exits non-zero (which Claude Code renders as a red "Stop hook error" banner every turn), and nudges the user to re-login at most once per session via asystemMessageJSON object.That work added a reusable seam in
src/Capacitor.Cli.Core/HttpClientExtensions.cs:enum AuthStatus { Ok, NoAuthRequired, Expired, NotAuthenticated }CreateClientWithAuthStatusAsync(...)→(HttpClient, AuthStatus)— same asCreateAuthenticatedClientAsyncbut reports the outcome instead of swallowing it.This issue is to carry the same handling to the other 7 agent hooks, which all still build their client with
CreateAuthenticatedClientAsync()and POST blindly.The shared problem
All non-Claude hook commands under
src/Capacitor.Cli/Commands/build the client viaCreateAuthenticatedClientAsync()and, on a non-success response, write[kcap] <agent>-hook {endpoint}: HTTP {status}to stderr. When auth has lapsed this means: (a) a guaranteed-to-401 POST is still sent, and (b) a stderr line is logged on every affected event. How visible (b) is depends on each agent's hook system.Goal (per hook)
CreateClientWithAuthStatusAsync.Expired/NotAuthenticated: skip the POST (it will 401) and return the agent's clean/no-op exit code.Per-agent checklist (line numbers approximate)
PostHookAsyncbuilds client at ~L321; non-success → stderr +return 1(~L326). Session event:SessionStart→/hooks/session-start/codex. Stop is not POSTed. Stdout is a JSON decision channel ({"continue":true}) — investigate whether Codex supports a user-message field.return 1(~L246). Session event:SessionStart→/hooks/session-start/gemini; per-turn:Notification. Stdout is "a JSON decision channel; this dispatcher emits nothing" — likely no user-notice channel.return 1(~L332). Routes off--event; session eventsessionStart→/hooks/session-start/copilot; per-turnnotification/agentStop. "Copilot treats hook stdout as optional… emits nothing."return 1(~L214). Routes off--event;session-start→/hooks/session-start/pi. Fail-open contract ("a kcap/server problem must never disrupt the pi session").return 1(~L162), butHandlealready returns 0 (fail-open, ~L137).agentSpawn→/hooks/session-start/kiro, fires on every prompt with the same session id. Emits nothing on stdout. Lower priority — already exits 0; the win is skipping the doomed POST + not logging the stderr line.return 1(~L125), butHandlealready returns 0 (fail-open, ~L107). Only handlessession-start→/hooks/session-start/opencode. Same lower-priority note as Kiro.TryPostHookAsyncreturns a bool and the caller is fail-open ("never crash Cursor"); no stderr error logged. Least-affected — main win is skipping the doomed POST. Session event:sessionStartviaCursorHookEventMap.Important caveats
systemMessagenotice channel is Claude-Code-specific. Do not copy it verbatim. Most of these agents emit nothing on stdout or treat stdout as a JSON decision/context channel — injecting a notice there would corrupt that channel or leak into the agent's context. For each agent, either find its real user-facing notice channel or, if none exists, soft-exit silently and rely onkcap status(already reports✗ token expired (run: kcap login)) and the interactive CLI's own messaging. Surfacing the once-per-session nudge is best-effort and may be N/A for several agents.HTTP 401line). Codex/Gemini/Copilot/Pireturn 1; whether their host renders that as a user-visible error is per-agent and worth confirming.AuthStatus+ whether this is the session-start event, should I POST / what to emit / what exit code") into a small helper so the 7 hooks don't each reimplement it.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.Reference
Template: PR #183 (
ClaudeHookCommand.csskip-and-exit-0 +systemMessage, andHttpClientExtensions.CreateClientWithAuthStatusAsync). Related: #182 (proactive daemon token refresh).