Chapter 4: types, one atom of state
A hook call is over in milliseconds, and everything it knew goes with it.
But the band above the prompt has to show the last decision, long after
the hook that made it returned. So the mod keeps one piece of state: an
atom, Claude Code's name for a single named value a plugin can read and
update from any hook. jevhooks has exactly one, jevhooks.last, and this
folder says what shape it is.
export type Decision = { verdict: 'allow' | 'ask' | 'pass'; line: string; ms: number; session_usd: number }
export type Shown = Decision & { kind: 'command' | 'turn end' }
declare module 'claude-code' {
interface PluginState {
jevhooks: { last: Shown | null }
}
}
Decision is what the daemon sends back for a deciding event. Shown adds
which kind of thing was decided, which is the first word the band prints.
The declare module block is TypeScript's way of adding a field to an
interface someone else owns: Claude Code's PluginState gains a
jevhooks entry, so read($, last) and update($, last, ...) in
register.tsx are checked against it.
Aside: the same type, written twice.
Decisionhere mirrors the Rust structDecisionincrates/jevhooks-events(chapter 8), field for field, and that copy is kept by hand. Rust and TypeScript do share one definition of what to do with an event, because the routing is Rust compiled to JavaScript. The daemon's reply crosses as JSON, and nothing generates this file from the Rust. If you add a field there, add it here.
Try it.
claude plugin validate pluginreportsdeclares state: jevhooks.last, and forregister.tsx,state writes: jevhooks.lastandstate reads: jevhooks.last: one atom, written bydecide, read by the band. With the engine's types in place,tsc -p pluginchecks every use.
For the people who maintain it
In this folder
| Path | What |
|---|---|
| index.d.ts | Decision, Shown, and the plugin's entry in PluginState. The manifest points here ("types"). |
← Previous: Chapter 3, hooks/ · Up: plugin · Next: Chapter 5, tests/ →