This page is the map. Per-field shapes live in the declarations, which you get with /plugin-types, because 10,843 lines of them do not belong on a web page and a good deal of that is tool input shapes you already know from using Claude Code.
Engine events
Tools
| Event | When | Core's side effect |
|---|---|---|
tool.call |
A tool is about to run | Yes. Running it. |
tool.check |
Permission is being decided | No |
tool.describe |
The tool's description is being assembled for the model | No |
tool.call is the one with a side effect and therefore the one where not calling next is how you stop something. tool.check is where you influence the permission decision without owning the call, and it is the safer seat for a policy that only wants a say. A tool.call raised inside a subagent carries agentId, which $.agent.list() maps to a name, a parent and a type. Which plugin and which agent loop are separate axes.
Prompting and turns
| Event | When |
|---|---|
prompt.submit |
A prompt is being sent. The first point a draft is visible. |
prompt.fill |
Something is writing the prompt box |
prompt.suggest |
Dim suggestion text is being proposed into it |
prompt.section |
A section of the system prompt is being built |
prompt.context |
Context is being attached to a prompt |
turn.start |
A turn begins |
turn.step |
The model is producing a step. The only streaming event. |
turn.complete |
A turn settles. Where usage numbers become real. |
On prompt.submit, e.context is string[] | undefined and it is additive: what you add is appended rather than replacing what is there. The result carries drop. The diff mod budgets its own contribution against a character limit minus what other plugins already added, and when it will not fit it drops its own contribution rather than the prompt, telling the person through $.ui.status. That is the right shape for any mod that attaches context.
Sessions and agents
| Event | When |
|---|---|
session.start |
A session begins. The composition root for most mods. |
session.receive |
Something arrives into the session |
session.compact |
The transcript is being compacted |
session.attach |
A surface attaches |
session.detach |
A surface detaches |
agent.offer |
A subagent is being offered to the model |
agent.spawn |
A subagent is being started |
Commands, config, skills
| Event | When |
|---|---|
command.run |
A slash command runs |
command.describe |
Its description is assembled |
config.set |
A configuration row is being written |
config.describe |
A row's description is assembled |
skill.prompt |
A skill's prompt is being built |
plugin.register |
A plugin is being admitted. Where an organisation governs. |
attribution.text |
Attribution text is generated |
plugin.register is the admission seam. A hook on it sees each plugin's static capability inventory, the same one claude plugin validate prints, and may refuse the plugin. That is the whole governance story: organisations govern by admission rather than by sandboxing, because plugins are trusted code with the process's reach once they are in.
attribution.text is on none of the public sources and it has four kinds: commit, pr, exemption, remedy. It is the seam for the text Claude attaches to work it did.
The screen
| Event | When |
|---|---|
ui.render |
A component is being drawn |
ui.resolve |
Elements are being resolved for a surface |
ui.press |
A button is pressed |
ui.input |
Text is entered |
ui.select |
A selection is made |
ui.message |
A Client surface module posts |
ui.scroll |
A scroll happens |
ui.focus |
Focus moves |
Four of these are on no public source: ui.select, ui.scroll, ui.focus and attribution.text above. The cheat sheet predates them or omits them; the declarations at 2.1.272 have them. Details and examples are on the UI page.
engine.create
The thirty-fourth, and the odd one. It builds $, it is not reachable as $.noun.event, it has no budget, and .catch on it throws. Covered on the engine page.
Operation events
Every verb on $ is also an event, so anything a plugin does is hookable by another plugin. Forty-nine of them:
| Noun | Events |
|---|---|
model |
complete classify fork |
session |
cwd model turns id messages repo surface surfaces authorize usage |
tool |
list register |
command |
list register |
ui |
toast status log notice invalidate open close blit |
fs |
read write list exists stat ancestors |
store |
get set delete keys |
clock |
now sleep after every |
turn |
abort |
agent |
list |
config |
list |
audio |
play speak |
http |
fetch |
process |
run |
settings |
read |
env |
get set |
mcp |
call |
Three notes on that table. tool.check is an engine event rather than an operation, despite reading like one. ui.blit is declared here and appears on no other source, including the cheat sheet and the issue thread. And flag.value, which was asked for in the issue thread, does not exist at 2.1.272: the only flag token anywhere in the declarations is SettingsSource = 'user' | 'project' | 'local' | 'flag' | 'policy', which is about where a setting came from.
The fs names moved between 2.1.263 and 2.1.272 (readFile to read, and so on), so a hook registered on fs.readFile today registers on nothing.
classic.*
Claude Code's older hooks, the ones configured in settings.json that shell out to a command and speak JSON over stdin and stdout, are all still here. Each is wrapped one for one:
every settings hook is wrapped 1:1 as
classic.<Event>with its exact JSON in and out; the configured shell hooks are core for that seam.
So classic.PreToolUse is an event, the JSON going into it is exactly what your shell hook would have read on stdin, and the JSON coming back is exactly what it would have written. The configured command hooks are core for that chain, which means they run beneath every function hook on it and a function hook can rewrite what they see and what they said.
classic is a real namespace for globbing. sec-default's own register.ts opens with on('classic.*', ($, e, next) => next.to(e, 'append')), which is both the confirmation and a fair summary of what an organisation wants from that seam.
On a Bash call, the tool.call chain logs first and settles last, so it wraps PreToolUse entirely. The two mechanisms are not alternatives and running both is normal.
Globs
on("tool.*", hook) // every tool event
on("classic.*", hook) // every settings-hook seam
on("*", hook) // literally everything, including every $ op
on("!tool.call", hook) // everything except that one
When a pattern matches several events, e narrows to the union of their types, so a glob hook is writing against a union and needs next.is("tool.*", e) or a discriminant check to get anywhere useful. next.event tells a glob hook which event it is in.
on("*") registers on every operation too, which means it observes every other plugin's $ calls. That makes install order a trust boundary rather than a preference.
The six invalidatable events
$.ui.invalidate(event) tells the engine that what a seam previously produced is stale and should be built again. Six seams accept it:
ui.renderui.resolveprompt.sectionprompt.contexttool.describecommand.describeconfig.describe
That is seven bullets for six events because ui.render and ui.resolve are the drawing pair and the declarations count the describe seams together. Take the list as the set of things you may ask to be recomputed; the cheat sheet named three of them.
This is what lets a mod change the model's view of a tool at runtime. Rewrite what tool.describe returns, call $.ui.invalidate('tool.describe'), and the next assembly picks it up.
What no event reaches
Worth knowing before you plan around a seam that is not there.
- The composer. Nothing fires while somebody is typing. The draft is first visible at
prompt.submit.prompt.fillandprompt.suggestare about writing into the box rather than watching it. - The engine's own
<system-reminder>attachments, andisMetauser messages. They are not on any seam. - The permission dialog. It is drawn by the engine alone and
ui.renderdoes not reach it. - A quota threshold. There is no event when usage crosses a line. poteat's own summary of what is left open: "What remains of item 4 is an event when a threshold is crossed; today a quota guard has to poll." Polling means
turn.completeplus a$.clock.everytick, which is what every mod doing it today does.
Why this is worth using at all
The alternative to a hook is asking the model to report what it did. I measured both on the same work: events written by hooks arrived 161 of 161. Events the model was asked to log itself arrived 0, 0 and 5 of 53, which is 9% across three runs.
An event is a fact about the session. A model's report of an event is a request it may decline.