Chapter 1: plugin, what Claude Code loads
Claude Code does a lot on its own: it reads files, edits them, runs shell
commands, and decides when it is done. At each of those moments it raises
an event (PreToolUse just before a tool runs, Stop just before a
turn ends, thirty-three kinds in all) and lets plugins have a say. A plugin
is a folder Claude Code is pointed at. This is that folder: it is the whole
of jevhooks as far as Claude Code can tell.
It brings two things.
- A mod (
hooks/, chapter 3): TypeScript that Claude Code runs inside its own process, with a function for each event it cares about. It is where a command is held, judged, and let go or put to you. - An MCP server (
.mcp.json): a program Claude Code starts once per session. MCP, the Model Context Protocol, is how a program offers tools to the assistant. This one offers two read-only ones (statusandrecent), but its real job is to make sure the daemon is running before the first command needs it (chapter 11).
flowchart LR
CC["Claude Code session"]
subgraph plugin["plugin/"]
manifest[".claude-plugin/plugin.json<br/>the name"]
hooks["hooks/register.tsx<br/>the mod"]
mcp[".mcp.json<br/>starts bin/jevhooks mcp"]
end
daemon["jevhooks serve<br/>one per machine"]
CC -- "reads" --> manifest
CC -- "runs in-process" --> hooks
CC -- "starts per session" --> mcp
mcp -- "makes sure it runs" --> daemon
hooks -- "HTTP on a Unix socket" --> daemon
Notice that the mod does not go through the MCP server. It talks to the daemon directly, over a Unix socket, in a few milliseconds. The MCP server is the doorman, not the hallway.
Aside: why a mod and not a settings hook? Claude Code can also run a shell command for each event, configured in your settings. That starts a new process every time, and every Bash command would pay for one. A mod is already loaded: a plainly read-only command is waved through in well under a millisecond, without leaving Claude Code at all. And a mod can draw: the band above the prompt is one.
Try it.
claude plugin validate pluginreads this folder the way Claude Code does and tells you what it found: the hooks the mod registers (session.start,command.run{command=jev},classic.PreToolUse,classic.Stop,classic.*,ui.render{component=AbovePrompt}), every host call it makes, and the one piece of state it keeps. It needs no Rust and no daemon.
For the people who maintain it
To run a session with it: claude --plugin-dir ./plugin from the
repository root. The MCP server is bin/jevhooks, which is not committed:
just daemon builds it and copies it here (chapter 11). Without it the mod
still loads, finds no daemon, and passes on everything.
tsconfig.json extends .claude-plugin/types/tsconfig.json, which Claude
Code lays down itself (it is gitignored, so it appears only once the engine
has loaded the plugin), and adds allowImportingTsExtensions, because a
mod's imports name the .ts file.
In this folder
| Path | What |
|---|---|
| .claude-plugin/ | Chapter 2: the manifest. |
| hooks/ | Chapter 3: the mod, its bridge to the Rust, and the generated JavaScript. |
| types/ | Chapter 4: the plugin's state, typed. |
| tests/ | Chapter 5: the plugin's tests, against a stand-in daemon. |
| .mcp.json | Starts ${CLAUDE_PLUGIN_ROOT}/bin/jevhooks mcp once per session. |
| tsconfig.json | For tsc -p plugin: the engine's config plus .ts imports. |
bin/jevhooks | Built by just daemon; not committed. |
← Previous: Prologue · Up: jevhooks · Next: Chapter 2, .claude-plugin/ →