Halfway through building this mod, I used its handoff. A new terminal tab opened on my second account and resumed the same conversation. I finished the mod in that tab, on a different subscription, with the context intact.

That is the pitch for Claude Code mods, which Anthropic shipped on October 1 in 2.1.287. The harness is now something you can program from inside.

What a Mod Is

A mod is a plugin whose hooks.json names a TypeScript or JavaScript module. The module exports register(on), and every hook has the shape ($, e, next). Return without next to answer the event yourself. Call next({ ...e, x }) to rewrite it for everything below. Await next(e) to act on the result. It is middleware, and the events cover tool calls, prompts, the system prompt, turns and what the interface draws.

A settings hook runs a shell command for each event and passes JSON over stdin and stdout. A mod is loaded once and stays in the session. It can keep state, draw UI that updates as events happen, and call back into Claude Code: open a pane, run a process, register a slash command, or register a tool the model can call.

— Anthropic, Getting started with Claude Code mods

The shell hooks I use for guardrails were a fence around the agent. A mod is in the room with it. It can draw a pane, a band above the prompt, toasts and status lines. It can restyle the spinner, messages and tool rows. It can register slash commands, add tools for the model and spawn processes. It cannot change the permission prompt, which is the right call.

Launch day went to the fun end of that surface first:

I made one that connects you to a multiplayer Doom server while Claude is busy. Every other player in the match is a person waiting for their Claude to finish.

— Jarrod Watts, on X

The Sample: ccseats in a Pane

ccseats runs several Claude accounts on one machine. Until now, its budget lived in a statusline and a table in another terminal. The mod moves both into the session:

  • /seats pane: 5h, weekly and Fable meters for every seat, drawn as a Raster of eighth-block cells that shade green to red. It shows the reset times, marks this session’s seat and stars the seat the picker would choose.
  • Band above the prompt: appears only when this seat crosses a picker threshold.
  • Seat handoff: runs ccseats <seat> --resume <id> --fork-session in a new tab. The seats share projects/, so the transcript is already there. The fork gives the new seat its own session ID, so two processes never write one file.
  • Codex and Grok handoff: for the day every Claude seat is low.

It reads only the local usage caches, so it adds nothing to an endpoint that answers fast polling with a four-minute 429. It is 305 lines, plus 236 lines of tests.

The Best Primitive Is the Conversation

Codex and Grok cannot read a Claude transcript. The obvious fix is to dump the last few hundred messages into a file. The better fix is one call:

const brief = await $.model.fork({ prompt: BRIEF_PROMPT })

$.model.fork reruns the main thread’s last request with one more user message and every tool denied. The API serves that prefix from its cache. So the model that did the work writes the brief: the goal, the files touched, the constraints I set, the open problems and the next steps. It writes this from full context, usually at the price of a cache read. The mod saves the brief outside the repo and starts codex 'Read <brief> and continue the work it describes.'.

That is the part of mods I did not expect. The harness exposes the conversation as a value you can spend.

The Validator Reads Your Code

claude plugin validate does not trust a manifest. It parses the module and refuses idioms it cannot follow. It rejected my first draft because I passed $ to a nested helper:

$ is passed to "load", which is not a function declared at the top of this file

$.env.get names must be string literals. Atoms in $.state must be declared in a type contract, keyed by plugin name. In return, the validator prints exactly what the mod touches:

calls: $.env.get (via handoff, load), $.fs.read (via load), $.process.run (via handoff), ...

That is a capability list derived from source, not declared by the author. It makes a stranger’s mod reviewable in five minutes, if you run it yourself. Nothing else shows it.

Reviewable is not sandboxed

Anthropic is explicit: a mod “runs inside Claude Code on your machine, with the same access Claude Code has, and it’s written by its publisher, not Anthropic.” The docs add that a process a mod starts runs outside the sandbox, even with sandboxing on. The validator tells you what a mod calls. It does not stop it.

The design depends on users seeing a mod’s capabilities before they trust it. The normal install and inspection paths do not show them.

— Ehud Melzer, Pluto Research

Pluto tested the September preview. It still holds on 2.1.288. claude plugin install printed “Successfully installed” with no prompt. claude plugin details ccseats@ccseats reports Hooks (0) and roughly zero tokens, for a mod that hooks four events and starts processes in a new terminal tab. Run claude plugin validate on the plugin’s cache folder before you trust one.

What Broke

The test kit is strict in a useful way. Nothing sits beneath your hooks in a test, so every engine call needs an answer. My first run failed on no implementation for command.register. Stubbing each call made the mod’s whole dependency list concrete.

Then I ran two reviews, /code-review and Codex. Together they found nine bugs in 305 lines. These five taught me the most:

  • A home path with an apostrophe broke the shell command for every terminal backend.
  • A missing kitty binary threw before the clipboard fallback, after the pane had already closed. The handoff did nothing.
  • A missing config at startup skipped the 30-second refresh timer for the rest of the session.
  • One dismissal hid the warning band for good, even after the seat recovered and ran low again.
  • A digit hotkey on the band fires from an empty prompt. The docs say so. I had bound 1 to handoff, so any prompt that began with “1” would fork the session. That is now a focus-only button.

The last one is the API’s sharpest edge. A band is always on screen, and its digits are global keystrokes.

What Mods Don’t Solve

  • The API moves. The skill’s own reference calls it early access. My session went from 2.1.287 to 2.1.288 mid-build, and the type folder moved with it.
  • Hot reload belongs to one session. The handoff starts a new session, and the dev copy stopped loading. That was the push to publish it properly.
  • Prompt caches belong to one account. The first turn on a new seat sends the whole context at full input price.
  • Terminals are not standard. New tabs work in herdr, tmux, WezTerm and kitty. Everything else gets the command on the clipboard, or in the transcript when no clipboard tool exists.

Try It

claude plugin marketplace add paddo/ccseats
claude plugin install ccseats@ccseats

The source is in plugin/. If you write your own mod, start with the validator output, not the docs: it tells you what your code actually does.