· 12 min read · Nadeera Silva
Claude Code Mods: What They Are and How to Write One
Claude Code mods let a plugin rewrite prompts, guard tool calls and draw UI. What they are, how they differ from hooks and skills, and a tested example.

Claude Code mods arrived on 1 October 2026 in version 2.1.287, and searches for "claude code mods" have been climbing since. A mod is a plugin with a small JavaScript or TypeScript module that runs inside Claude Code and handles its events, such as a prompt being sent, a tool call about to run or a permission decision. That makes mods more powerful than settings hooks, skills or MCP servers, and it means they run code with your permissions. I build agent setups for clients, so I read the docs, installed Claude Code 2.1.317 on a test box and built a small guard mod. Here is what mods are, how they compare, a tested tool.check example, how to validate and share one, and what to check for security.
What Claude Code mods are
The mods overview defines a mod as a plugin that changes how Claude Code looks and behaves. Its code registers event handlers, which the docs call hooks. Claude Code runs your hook before it acts on the event, so the hook can do one of three things:
- Observe: note what happened and let it continue, for example counting tool calls.
- Rewrite: change the event before it continues, for example rewriting a prompt or a tool call's arguments.
- Answer: handle the event itself, for example refusing a shell command so it never runs.
Each hook gets $ (the mods API), e (the event as frozen data) and next (the rest of the chain, ending in Claude Code's own behaviour). It works like Express middleware.
Anything outside the hook's own code, such as reading a file, starting a process, calling a model or drawing a pane, goes through the mods API ($.fs, $.process, $.http, $.model, $.ui and others). That is why Claude Code can list what a mod does before you install it.
Some built-in features already ship as mods, including /diff and AGENTS.md loading, plus the sec-default guard covered below.
Mods vs hooks, skills, plugins and MCP
The naming is confusing. The docs now call the older shell-command kind "settings hooks", and "hook" on the mods pages means a mod's handler. A plugin is the package, and it can hold a mod, skills, settings hooks and MCP servers side by side.
| Mod | Settings hook | Skill | MCP server | |
|---|---|---|---|---|
| What it is | JS or TS functions that Claude Code calls in its own process | A shell command, HTTP request or prompt run on a lifecycle event | A SKILL.md file of instructions | An external process or service that gives Claude tools |
| What it can change | Tool calls, prompts, commands, turns and what the interface draws | Whether a tool call or prompt goes ahead, arguments, results and added context | What Claude knows and does | Which tools Claude has |
| Can draw UI | Yes | No | No | No |
| Pick it when | You need a pane, a custom command, or to rewrite or decide an event in code | A script you already have should block, allow or log something | You keep pasting the same instructions | Claude needs to reach an external system |
The table follows the mods overview. If a skill or an MCP server already solves the problem, use that. Skills are plain Markdown and far easier to review (see my Agent Skills guide), and MCP is still the way to reach outside systems (see the stateless MCP spec).
Where mods sit in a tool call
The order matters for guards. Per the events page, managed PreToolUse hooks run before any mod's tool.call hook. Other PreToolUse hooks run after the last mod calls next. Then tool.check fires after those hooks and before the permission prompt, auto mode's classifier or a PermissionRequest hook.

Write a minimal Claude Code mod
A mod needs three files. You don't need Node.js or a build step, because Claude Code loads .js and .ts files directly (Create a mod).
env-guard/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.jsThe manifest is an ordinary plugin manifest. Don't start the name with claude-, because claude plugin validate rejects names that look like Anthropic's own.
{
"name": "env-guard",
"version": "0.1.0",
"description": "Refuses file-tool writes to .env files and asks a person before destructive shell commands",
"author": { "name": "Nadeera Silva" },
"license": "MIT"
}The modules key in hooks/hooks.json is what makes a plugin a mod:
{
"description": "env-guard hooks module",
"modules": ["./register.js"]
}A practical guard: block .env writes, ask a person for destructive commands
My two real worries with any coding agent on client projects are an edited .env file and an rm -rf or force push I didn't approve. This module handles both:
// hooks/register.js (tested with Claude Code 2.1.317)
// .env, .env.local, .env.production ... but not .env.example or .env.sample
const ENV_FILE = /(^|\/)\.env(\.(?!example$|sample$)[\w.-]+)?$/
// Shell commands a person should approve. Text matching only: a reminder, not a sandbox.
const RISKY = /\brm\s+-[a-zA-Z]*[rf]|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*(--force|\s-f\b)|\bdrop\s+(table|database)\b/i
export function register(on) {
// 1. Refuse Edit and Write calls that target a .env file
on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
if (ENV_FILE.test(e.file_path)) {
return { deny: 'env-guard: editing ' + e.file_path + ' is blocked. Tell the user which variable to add and let them edit the file.' }
}
return next(e)
}).catch(async ($, e, next) => {
// Fail closed: if the guard throws or times out, refuse the write
if (next.called) return next(e)
return { deny: 'env-guard failed, so this write was not run: ' + next.error.kind }
})
// 2. Destructive shell commands always go to a person
on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
const decided = await next(e)
// Keep Claude Code's own deny, and leave other commands alone
if (decided.decision === 'deny' || !RISKY.test(e.input.command)) return decided
return { decision: 'ask', decider: 'person', reason: 'env-guard: destructive command, a person must approve it' }
}).catch(async () => ({ decision: 'deny', reason: 'env-guard failed while checking this command, so it was refused' }))
}How it works, based on the docs:
- `tool.call` fires when a tool is about to run. Returning
{ deny }without callingnextstops the tool, and Claude reads the reason, so write it as an instruction. - `tool.check` is where Claude Code decides allow, ask or deny.
await next(e)returns its own decision, ande.inputholds the arguments. - `.catch` matters. Without it, a hook that throws or times out is skipped and the call carries on, which is the wrong default for a guard.
- `decider: 'person'` was added in 2.1.304. Per the changelog, only the permission dialog or a host's callback can approve that ask, and where nobody can be asked the call is refused. The docs pages don't describe it yet, so check your generated types first.
Load it and check what Claude Code reads
Load it for one session with claude --plugin-dir ./env-guard, which hot-reloads on save. First run the validator, which analyses the code without running it or signing in:
claude plugin validate ./env-guardOn the box with 2.1.317, the relevant lines were:
❯ ./register.js hooks: tool.call{tool=Edit|Write}, tool.check{tool=Bash}
❯ ./register.js gating hook with .catch: tool.call{tool=Edit|Write}
❯ ./register.js gating hook with .catch: tool.check{tool=Bash}
❯ ./register.js calls: nothing on $
✔ Validation passedcalls: nothing on $ means the guard reads no files, runs no processes and makes no network requests. The validator's directory checks also flagged a missing README and licence (both block a directory submission, so I added them) and a missing icon.
Test it without a session
claude plugin test runs .test.ts files with no session, sign-in or network (Test a mod). The test's $ acts as Claude Code, and on registers stubs that answer in its place:
import { expect, test } from 'claude-code/testing'
test('a Write to .env is refused before the tool runs', async ($, on) => {
on('tool.call', () => ({ result: 'ok' }))
const r = await $.tool.call({ tool: 'Write', file_path: '/work/app/.env', content: 'KEY=1' })
expect(String(r.deny)).toContain('env-guard')
})
test('rm -rf is sent to a person', async ($, on) => {
on('tool.check', () => ({ decision: 'allow' }))
const r = await $.tool.check({ tool: 'Bash', input: { command: 'rm -rf build' } })
expect(r.decision).toBe('ask')
})My full file had six tests (also .env.example passing, npm test keeping its allow, a deny staying a deny), and all six passed in about 0.2 seconds. The answer didn't include decider, so I couldn't test what that flag does in a real dialog.
Let Claude write a mod for you
You can also describe a mod in a session ("make a mod that shows the current git branch above the prompt"). Claude uses the built-in plugin-authoring skill and writes the files to ~/.claude/dev-mods/<session-id>/, asking you to approve each file in default and acceptEdits modes. Nothing loads until you agree to hot reloading, and these mods don't load in claude -p, dontAsk mode or an untrusted workspace. They only load in that session and are deleted after cleanupPeriodDays, so copy one out and use --plugin-dir to keep it.
Share and install a mod
A mod ships like any plugin: send the folder or a zip, list it in a team marketplace, have an admin install it through managed settings, or submit it to Anthropic's directory. People install it with /plugin install name@marketplace. Put the tested Claude Code version in the README, and bump version in plugin.json, because installed plugins are cached by version.
If Claude helps you share it, the changelog says the plugin-authoring skill scans the mod's files for keys first (2.1.300) and asks public or private before creating a GitHub repository (2.1.299). Run your own secret scanner too.
Security notes before you install anyone's mod
What I check first:
- Run `claude plugin validate` on a clone. Look hard at
$.process.run,$.http.fetch,$.env.getand$.env.setincalls:, and attool.check, which can approve calls. - Know your setup. With managed settings or a Team or Enterprise login, the built-in
sec-defaultguard loads first. A mod'sallowthen can't lift a deny rule, skip an ask rule or pass a call aPreToolUsehook blocked (permissions docs). With neither (for example a Pro or Max login or an API key on your own laptop), it can do all three. - Know the off switches.
--safe-modeturns off installed mods for one session, and"disableAllHooks": truein~/.claude/settings.jsonstops every mod you installed. Built-in mods keep running. - For teams, admins can set
allowManagedModsOnlyso only the organisation's own mods run, anddisableSideloadFlagsto reject--plugin-dir(Manage mods for your organization).
The same habit applies to any agent tool that runs code as you, as with the workers in my DeepSeek Harness superagent review.
Version notes
The feature began as a public design proposal, anthropics/claude-code#91870, opened on 3 September 2026. Releases 2.1.300 to 2.1.317 keep adding hooks and UI pieces (settings.read layers, agent.spawn rewrites, $.ui.view, virtualText), so pin a version in your README.
Does Codex have something similar?
OpenAI's Codex hooks docs describe lifecycle hooks such as PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit and Stop, set in hooks.json or config.toml. Only command handlers run today, and each non-managed hook must be reviewed and trusted by its hash. That is close to Claude Code's settings hooks. The page describes nothing like in-process mods that draw UI.
Checklist
- Run
claude --versionand confirm 2.1.287 or later (2.1.304+ forisReadOnlyanddecider). - Try a permission rule, settings hook, skill or MCP server first.
- Add a
.catchthat fails closed on every guard hook. - Run
claude plugin validateand readhooks:,calls:and the gating lines. - Write
claude plugin testtests for the allow, ask and deny paths. - Try it with
--plugin-dirin a throwaway repo first. - Validate every third-party mod, and know whether your machine has managed settings.
- Ship a README with the tested version and a licence.
Frequently asked questions
What are Claude Code mods?
Mods are plugins with a JavaScript or TypeScript module that runs inside Claude Code and handles its events, such as prompts, tool calls, permission decisions and drawing the interface. A hook can observe an event, rewrite it or answer it. They arrived in Claude Code 2.1.287 on 1 October 2026.
What is the difference between Claude Code mods and hooks?
Settings hooks are shell commands, HTTP requests or prompts that Claude Code runs on lifecycle events such as PreToolUse. Mods are functions that run inside Claude Code, so they can also rewrite events, add commands and draw panes or buttons. Both can ship in the same plugin.
How are mods different from Claude skills and plugins?
A skill is a SKILL.md file of instructions Claude reads, and an MCP server gives Claude extra tools. A plugin is the package that can hold mods, skills, settings hooks and MCP servers together. A plugin becomes a mod when its hooks/hooks.json lists a hooks module under modules.
Are Claude Code mods safe?
Mods are not sandboxed and run with your permissions, so they can read files, environment variables and API keys, and approve tool calls. Install them only from sources you trust, and run claude plugin validate on a copy first to see which events and API calls the mod uses.
How do I turn Claude Code mods off?
Disable one mod in the Installed tab of /plugin, start a session with --safe-mode to skip installed mods for that session, or set disableAllHooks to true in your user settings. Built-in mods keep running, and admins can use allowManagedModsOnly.
Does Codex have hooks like Claude Code?
Yes. OpenAI's Codex docs describe lifecycle hooks such as PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit and Stop, configured in hooks.json or config.toml. Only command handlers run today, which makes them closer to Claude Code's settings hooks than to mods.
Sources
- Customize Claude Code with mods (Anthropic (Claude blog), 2026-10-01)
- Mods overview (Claude Code Docs)
- Create a mod (Claude Code Docs)
- React to events with a mod (Claude Code Docs)
- Mods reference (Claude Code Docs)
- Test a mod (Claude Code Docs)
- Manage mods for your organization (Claude Code Docs)
- Configure permissions (precedence among rules, hooks, and mods) (Claude Code Docs)
- Claude Code CHANGELOG (2.1.287 to 2.1.317) (Anthropic on GitHub, 2026-11-03)
- @anthropic-ai/claude-code on npm (2.1.317) (npm, 2026-11-03)
- Mods - make Claude 10x more extensible (design proposal, issue #91870) (Anthropic on GitHub, 2026-09-03)
- Built-in mods source (mods directory) (Anthropic on GitHub)
- Sample mods (claude-code-playground) (Anthropic on GitHub)
- Codex hooks (OpenAI)
Keep reading
- Agent Skills Guide: Write a SKILL.md That Works EverywhereWhat agent skills are, how to write a SKILL.md that loads in Claude Code, Copilot, Cursor and Codex, and how to install shared skills safely.
- MCP Stateless Update: Migrate Your Server to 2026-07-28MCP's 2026-07-28 spec drops sessions and the initialize handshake. What breaks in your MCP server, and how to migrate a TypeScript server step by step.
- DeepSeek Harness Superagent: Self-Hosted GitHub Coding AgentDeepSeek Harness now has dsh-superagent, a self-hosted agent that turns GitHub requests into pull requests. How it works, how to install it, and the risks.
Comments
Share a question or note about this article.
Loading comments…