Claude Code Mods and When to Use One Instead of a Hook¶
A mod is plugin code running inside Claude Code's process, so it can draw panes, rewrite events, and approve a call your hook blocked.
Claude Code 2.1.287 shipped the surface on 1 October 2026 with one changelog line: "Added Claude Mods: plugins may now modify deeper behavior" (Claude Code changelog). Nothing needs switching on, because "Mods require Claude Code v2.1.287 or later, and they're on by default" (Mods overview).
What a mod is¶
A mod is a plugin "that changes how Claude Code looks and behaves. It's made of JavaScript or TypeScript event handlers" (Mods overview). Three files carry it. The first is a .claude-plugin/plugin.json manifest. The second is a hooks/hooks.json whose modules array names one path to the hooks module. The third is the module itself, which "Exports register(on, options)" (mods reference). That same hooks.json "Can also hold settings hooks under hooks". A types/index.d.ts contract becomes required once the mod uses $.state or adds a namespace to the mods API.
The naming confuses people, and the docs settle it. Settings hooks run "as a shell command, HTTP request, or prompt you configure in a settings file", while a mod's handlers "are functions that run inside Claude Code instead" (Mods overview). Both are called hooks. The docs reserve the term settings hook for the settings-file kind. One plugin can carry both, so the choice is per behavior.
Each handler takes ($, e, next). Calling next(e) "Runs the hooks after this one, then Claude Code's behavior" (mods reference). That leaves three moves. Await it and read the result. Pass a changed copy forward. Or return something like { deny: "Use the file tools." } without calling it at all.
Why it works¶
The claude.dev walkthrough draws the contrast in two sentences: "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" (Getting started with Claude Code mods). A fresh subprocess per event holds no handle on the renderer and no identity between events. It can decide and it can log. Staying in the session gets a mod three things that subprocess cannot have. Those are a place in the middleware chain, the render tree for the component being drawn, and live state across events. A mod is the only one of the four extension points whose answer to "Can it draw in the interface" is yes (Mods overview).
The same placement is the price. "Mods aren't sandboxed". The sharpest consequence lands on anyone who has already built a guard. "A mod that approves tool calls can approve one that an ask rule would prompt for, or that one of your own PreToolUse hooks blocked" (Mods overview). One carve-out survives. "A mod can restyle much of Claude Code's interface, but not the permission prompt."
The three reload paths¶
Mods get described as hot-reloading, which holds for two of the three ways one reaches a session.
| How the mod is loaded | What picks up a change |
|---|---|
--plugin-dir ./my-mod |
Claude Code "watches a directory loaded with --plugin-dir and hot-reloads the hooks module when a file in it changes" (Create a mod) |
| Written by Claude in-session | "the mods in the session's mods folder load when the turn ends, and reload at the end of each turn that changes them" (Create a mod) |
| Installed from a marketplace | "If you install or update a mod from your shell while a session is open, run /reload-plugins in that session to load it" (Mods overview) |
Reloading costs you module scope. "Each reload runs register again, so calls resets to 0" (Create a mod). Put a counter or a history in $.state instead, declared in the manifest's type contract. A directory passed to --plugin-dir is also a protected path, "so in default and acceptEdits modes you're asked to approve each of Claude's edits to the mod". Skills reload by a separate mechanism, covered in reloading skills mid-session.
When this backfires¶
- Nothing draws outside the terminal and the Desktop app's Code tab. A mod's hooks run wherever the plugin loads. But "Drawing is narrower: only the terminal and the Desktop app show a mod's panes, bands, and replaced rows" (Mods overview). The same table marks the VS Code extension's chat panel,
claude -p, the Agent SDK, and cloud sessions as hooks-yes and draws-no. A pane is dead weight in a headless CI session. - A WSL session in the Desktop app loads no plugin at all, so the handlers never fire there.
- A mod is not an enforcement point. The guard mod in the claude.dev walkthrough says so: "It's a safety net, not a permission system. [...] Use permission rules for a hard block" (Getting started with Claude Code mods). A handler also gets 10 seconds of its own time per event. That budget excludes time inside
nextand inside any mods API call other than$.clock.sleep. "Claude Code skips a hook that exceeds a time limit" (mods reference). - On a managed machine, a user's mod stops before a user's settings hook does. Under
allowManagedModsOnly, only the organization's mods and the built-in ones load, and "Users' settings hooks keep running" (mods reference). - The API is unfinished. The walkthrough warns that "The API can change between releases" (Getting started with Claude Code mods). Each load writes "TypeScript declaration files, ending in
.d.ts, into.claude-plugin/types/inside the mod's directory" (Create a mod). Those beat the published ones, because "The copy on GitHub can be older than the Claude Code version you have installed" (mods reference).
Example¶
Blast Radius, one of the three mods in the claude.dev walkthrough, hooks tool.call with a { tool: "Bash" } matcher. It matches risky commands such as rm -rf, git reset --hard, or a force push. It collects a report with $.process.run, using the tools' own dry-run commands (git status --porcelain, git clean -n). It then opens a pane with Proceed and Cancel, and returns a deny carrying the reason if you cancel (Getting started with Claude Code mods).
The pane and the held call are only possible in-process, so no settings hook could have built it. The gap it leaves is also why it is not the control. It reads command text, so a $(…) substitution or a script that calls rm walks straight through. The build that actually holds pairs it with a deny rule from parameter-level permission rules. The mod shows you the blast radius, and the rule does the blocking.
Key Takeaways¶
- Pick a mod when you want a pane, a band above the prompt, a custom command, or to rewrite an event. Pick a settings hook when you want to block, allow, or log one with a script you already have (Mods overview).
- Installing a mod is a trust decision rather than a configuration change. Run
claude plugin validateon its directory first. "Thehooks:andcalls:lines in the output list the events the mod handles and what it asks Claude Code to do" (Mods overview). That listing is possible only because "A hook has no other way to do those things" than the mods API. - A mod outranks your own
PreToolUsehook on tool approval. A hook you rely on stops being a floor once a mod is installed.
Related¶
- Claude Code Extension Points: When to Use What — the decision framework mods now extend, covering CLAUDE.md, rules, skills, hooks, subagents, MCP, and plugins
- Claude Code Hooks Lifecycle — the settings-hook events a mod's handlers sit beside
- Local Plugin Scaffolding via
claude plugin init— the manifest layer underneath a mod, and when it beats a loose skill - Reloading Skills Mid-Session in Claude Code — the adjacent reload mechanism, for skills rather than plugin code
- Plugin Background Monitors — the other way a plugin runs work for the length of a session