jevhooks.git / plugin / README.md

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 (status and recent), 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 plugin reads 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

PathWhat
.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.jsonStarts ${CLAUDE_PLUGIN_ROOT}/bin/jevhooks mcp once per session.
tsconfig.jsonFor tsc -p plugin: the engine's config plus .ts imports.
bin/jevhooksBuilt by just daemon; not committed.

← Previous: Prologue · Up: jevhooks · Next: Chapter 2, .claude-plugin/ →