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' | 'hold'; line: string; ms: number; session_usd: number }
export type ModelChoice = { tier?: 'haiku' | 'sonnet' | 'opus' | 'fable' | null; model?: string | null; line: string; ms: number; session_usd: number }
export type Shown = { verdict: Decision['verdict'] | 'model'; line: string; ms: number; session_usd: number } & {
  kind: 'command' | 'turn end' | 'prompt'
}

declare module 'claude-code' {
  interface PluginState {
    jevhooks: { last: Shown | null }
  }
}

Decision is what the daemon sends back for a deciding event, and ModelChoice what it sends back for a prompt. Shown is either of them as the band prints it, with which kind of thing was decided as its first word. 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. Decision and ModelChoice here mirror the Rust structs of the same names in crates/jevhooks-events (chapter 8), field for field, and those copies are 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 plugin reports declares state: jevhooks.last, and for register.tsx, state writes: jevhooks.last and state reads: jevhooks.last: one atom, written by decide and chooseModel, read by the band. With the engine's types in place, tsc -p plugin checks every use.

For the people who maintain it

In this folder

PathWhat
index.d.tsDecision, ModelChoice, Shown, and the plugin's entry in PluginState. The manifest points here ("types").

← Previous: Chapter 3, hooks/ · Up: plugin · Next: Chapter 5, tests/ →