Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **A Claude Code card stops spinning when an API error ends the turn.** A usage
limit, an overload or a failed login ends a Claude Code turn with
`StopFailure` instead of `Stop`, which the plugin did not register, so the card
kept its spinner until the next prompt. It now reports the turn done, and a
lich that reads the limit off the transcript picks the turn up again once the
limit resets.

## [0.18.0] - 2026-10-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ swallows its own errors and never awaits a report.
| `busy` | `UserPromptSubmit`, `PostToolUse` | `UserPromptSubmit`, `PostToolUse` | `PreInvocation` | `session.status` (`busy`) | `input`, `turn_start` | — |
| `busy` + tool | `PreToolUse` | `PreToolUse` | `PreToolUse` | `tool.execute.before` | `tool_call` | — |
| `waiting` + reason | `Notification` | `PermissionRequest` | — (not measured) | any `*.asked` | — (see below) | — |
| `done` | `Stop` | `Stop` | `Stop` | `session.status` (`idle`) | `session_stop` | — |
| `done` | `Stop`, `StopFailure` | `Stop` | `Stop` | `session.status` (`idle`) | `session_stop` | — |
| title | `Stop` | `Stop` | `Stop` | `session.updated` | `session_stop`, `turn_start` | — |
| `idle` | `SessionEnd` | — (registered, never fires) | — (never fires) | — (nothing outlives it) | — (nothing outlives it) | — |
| touched | `PostToolUse` (write tools) | `PostToolUse` (write tools) | `PostToolUse` (write tools) | `file.edited` | `tool_result` (write tools) | `PreToolUse` (write tools) |
Expand Down
10 changes: 10 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@
]
}
],
"StopFailure": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/report-state.sh done"
}
]
}
],
"SessionEnd": [
{
"hooks": [
Expand Down
15 changes: 15 additions & 0 deletions tests/hooks.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,21 @@ test('every registered state argument is an accepted state', () => {
}
})

// Claude Code fires StopFailure *instead of* Stop when an API error ends the
// turn (a usage limit, an overload, a failed login), so a registration with
// Stop alone leaves the card spinning, and lich never gets to read the limit
// off the transcript (lich docs/hooks/session-state.md).
test('claude ends a turn an API error stopped like any other', () => {
const failure = REGISTRATIONS.filter(
(r) => r.provider === 'claude' && r.event === 'StopFailure' && r.script === 'report-state.sh',
)
assert.deepEqual(
failure.map((r) => [r.argument, r.matcher]),
[['done', undefined]],
'StopFailure reports done for every error',
)
})

test('every registered provider argument is a registered provider', () => {
for (const r of REGISTRATIONS.filter((r) => r.script === 'report-session-start.sh')) {
assert.ok(
Expand Down
Loading