DEV Community

Pawel
Pawel

Posted on

I Built My First Claude Code Mod Without Writing Code: Claude Wrote It, Validated It, and Hot-Reloaded It

I wanted one thing: to see my context window without typing /context.

Not a dashboard. Not a plugin I install and configure for twenty minutes. Just a small bar above the prompt that tells me how full the window is, the way the /context command does, but always on screen. One sentence of a prompt later, it was there.

This is the story of the first Claude Code mod I built without writing a single line of code. Claude wrote it, validated it, tested it, and hot-reloaded it into my session. I just watched.

What a mod actually is

Claude Code mods (shipped in v2.1.287, on by default) are small JavaScript or TypeScript modules that live inside a plugin. They see every event in your session as it happens: tool calls, the prompt you submit, turns starting and ending, and every piece of the interface as it is drawn.

That last part is the new bit. Mods can rewrite what Claude Code does and draw their own UI: a pane, a band above the prompt, a status line entry, a toast. Everything the engine draws is a component you can hook into.

And here is the part that still feels strange: you do not need to learn the API to try one. You describe the mod you want, and Claude Code builds it.

Step 1: ask for it

Open any project, run claude, and paste a prompt like this:

Create a mod that draws my context window as a stacked bar above the prompt, one colour per category like /context, toggled with /context-bar.

Claude Code loads its own plugin-authoring skill, reads the engine's type declarations, and writes three or four files into a session folder under ~/.claude/dev-mods/. Then it asks one question: allow hot reloading for this session?

Say yes. When the turn ends, the bar appears above your prompt.

That is the moment that sold me. I did not open an editor. I did not read the docs. I described a UI in plain English, and it showed up.

Step 2: make Claude validate its own homework

Before I even looked at the code, I asked for two things while the session was still warm:

  • Run claude plugin validate on the folder. The validator reads the module the way the engine will and names anything the engine would refuse. Think of it as a compiler for mods: non-negotiable.
  • Write a tests/ file and run claude plugin test. That exercises the hooks against the real engine. No TypeScript toolchain needed on your machine.

This is the difference between "Claude wrote something plausible" and "the engine will actually load this". An LLM will happily produce a mod that looks right and fails at load time. Validate and test are what separate the magic from the garbage.

Step 3: peek under the hood (optional, but worth it)

Curiosity won, so I read the code. A mod is one module exporting register:

export function register(on) {
  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
    const { Box, Text } = $.ui.resolve(e);
    return Box({
      paddingX: 1,
      children: [Text({ children: "hello from my mod" })],
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

The shape is middleware. Your hook runs, then next(e) hands the event to the next plugin in the chain, then to Claude Code itself. Every hook does one of three moves:

  • Observe: call next(e), then look at the result. Record every file edit, take a reading after each turn.
  • Rewrite: call next with a modified event. Change what the rest of the chain sees.
  • Answer: skip next entirely and serve the event yourself. Refuse a tool call, handle a slash command.

One detail I stole from Addy Osmani's guide: do not keep history in a module-level let. Hot reload is a fresh load, register runs again, and module variables start over. Keep readings in $.state, declared in a small types contract, and they survive reloads. If you skip the contract, claude plugin validate stops you with an error that names the fix. The tooling here is genuinely good.

Step 4: make it permanent

The session folder dies with the session. Copy the mod somewhere stable:

mkdir -p ~/.claude/mods
cp -R ~/.claude/dev-mods/<session-id>/context-bar ~/.claude/mods/context-bar
Enter fullscreen mode Exit fullscreen mode

Keep the manifest in .claude-plugin/plugin.json, the hooks/ folder with its module, and the types/ contract if the mod keeps state. Copy tests/ too, so you can rerun the tests after an update.

Then tell Claude Code where it lives. The one place that reaches every session, including the ones the desktop app starts, is the env block of your user settings file. Open ~/.claude/settings.json and add:

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "/Users/you/.claude/mods/context-bar"
  }
}
Enter fullscreen mode Exit fullscreen mode

Use the full path, not ~. Several mods are separated with a colon. For a one-off terminal session, claude --plugin-dir ~/.claude/mods/context-bar does the same without touching settings.

Open a new session in any project. The bar is above the prompt before you type anything, and /context-bar hides it.

Two things that bit me

Early access means churn. The API can change between releases, so a mod written today may need a touch-up after an update. Rerun claude plugin validate after every upgrade. It is cheap insurance.

Check which claude you are running. The desktop app bundles its own engine, and the one on your PATH may be older. Mine was, and its validator did not recognize the hooks module at all. Validate with the app's bundled binary, or update the CLI first. This cost me twenty confused minutes staring at an error that was not mine.

Why this feels different

I have used hooks, slash commands, and skills. A settings hook runs a shell command per event and passes JSON over stdin and stdout. A mod loads once, keeps state, draws UI that updates as events happen, and can call back into Claude Code: open a pane, run a process, register a slash command, register a tool the model can call. Some of Claude Code's own features are built as mods, including AGENTS.md support and the /diff pane. Their source is public, so you can read how the team builds them.

But the real shift is the loop. The skill for writing mods ships inside Claude Code, so the agent modifies its own runtime, in the same session, and hot reload shows you the result on the next turn. I kept asking for tweaks ("add a legend line with per-category token counts", "show a toast when context crosses 85%") and watched the bar change. It felt less like configuring a tool and more like pair programming with the tool itself.

If you could describe a mod in one sentence, what would it do?

Top comments (6)

Collapse
 
piekwerk profile image
Piekwerk •

The observe / rewrite / answer taxonomy is the useful part, and it also ranks the blast radius. Observe can only lose your display, rewrite changes what the engine sees, answer can quietly disable built-in behavior. So an observe mod like your context bar is the right first project, and the tweak loop stays in the safe tier.

One addition to the churn section. A mod that fails to load after an upgrade throws no banner. The UI just never appears and everything else works fine. I have had a tool report success while the live artifact stayed empty, so after every upgrade I open one session and confirm the bar still draws.

Collapse
 
pawel_nowak profile image
Pawel •

I like the blast radius framing, it is a cleaner way to say what I was fumbling toward in that section. And you are right about the silent failure mode: no banner, no error, the bar just never shows up. I had validate-after-upgrade in the post but your open-one-session-and-confirm-it-draws check is the real proof. Stealing that for the next upgrade.

Collapse
 
piekwerk profile image
Piekwerk •

Thanks, glad it landed. One split worth making when you steal it: validate proves the engine can still load the module, the open session proves the render side. I have had a mod pass validate and draw nothing, the load was fine and the component side had changed. So I keep the check cheap: new session, bar there before the first prompt, one toggle of /context-bar to confirm the command still binds. Two seconds, and it covers the half validate cannot see.

Thread Thread
 
pawel_nowak profile image
Pawel •

That is a sharp split and I am stealing it. Validate saying green while the render side silently breaks is exactly the failure class that hurts most, because everything looks fine until you actually look. I ran into a milder version of this with the hot reload loop: load was clean, command was bound, and the bar just was not there. Your two second check (fresh session, bar visible before the first prompt, one toggle) is going into my workflow permanently.

Collapse
 
rulestack profile image
Rulestack •

Hot-reloading a UI you only described in one sentence is a wild loop, and leaning on validate and test to separate plausible from actually loadable makes a lot of sense to me. The PATH mismatch would have got me too.

Collapse
 
pawel_nowak profile image
Pawel •

Yeah, the PATH thing was the most annoying part of the whole experiment. The error message looked exactly like broken code, so I burned twenty minutes re-reading my mod before I thought to check which binary was actually running. Your instinct to lean on validate and test first is the right one, it just works better when the validator itself is current.