jevhooks.git / plugin / types / README.md

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. Decision here mirrors the Rust struct Decision in crates/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 plugin reports declares state: jevhooks.last, and for register.tsx, state writes: jevhooks.last and state reads: jevhooks.last: one atom, written by decide, 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, Shown, and the plugin's entry in PluginState. The manifest points here ("types").

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