Skip to content

· 12 min read · Nadeera Silva

AI & Automation

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.

A teal command-line device with amber and teal modules snapped onto its sides, a robotic hand adding one more, and an amber shield gate stopping a stream of tool-call orbs before they reach a folder, showing mods guarding Claude Code tool calls

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.

ModSettings hookSkillMCP server
What it isJS or TS functions that Claude Code calls in its own processA shell command, HTTP request or prompt run on a lifecycle eventA SKILL.md file of instructionsAn external process or service that gives Claude tools
What it can changeTool calls, prompts, commands, turns and what the interface drawsWhether a tool call or prompt goes ahead, arguments, results and added contextWhat Claude knows and doesWhich tools Claude has
Can draw UIYesNoNoNo
Pick it whenYou need a pane, a custom command, or to rewrite or decide an event in codeA script you already have should block, allow or log somethingYou keep pasting the same instructionsClaude 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.

Flow diagram of one Claude Code request: prompt.submit and turn.step mod hooks, then managed PreToolUse hooks, the tool.call mod hook, other PreToolUse hooks, the permission decision, the tool.check mod hook, the permission prompt or classifier, and the tool run, with the two env-guard checks marked
Where mod hooks run around one tool call, from the Claude Code docs (v2.1.317)

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.js

The 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 calling next stops 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, and e.input holds 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-guard

On 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 passed

calls: 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.get and $.env.set in calls:, and at tool.check, which can approve calls.
  • Know your setup. With managed settings or a Team or Enterprise login, the built-in sec-default guard loads first. A mod's allow then can't lift a deny rule, skip an ask rule or pass a call a PreToolUse hook 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-mode turns off installed mods for one session, and "disableAllHooks": true in ~/.claude/settings.json stops every mod you installed. Built-in mods keep running.
  • For teams, admins can set allowManagedModsOnly so only the organisation's own mods run, and disableSideloadFlags to 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 --version and confirm 2.1.287 or later (2.1.304+ for isReadOnly and decider).
  • Try a permission rule, settings hook, skill or MCP server first.
  • Add a .catch that fails closed on every guard hook.
  • Run claude plugin validate and read hooks:, calls: and the gating lines.
  • Write claude plugin test tests for the allow, ask and deny paths.
  • Try it with --plugin-dir in 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

  1. Customize Claude Code with mods (Anthropic (Claude blog), 2026-10-01)
  2. Mods overview (Claude Code Docs)
  3. Create a mod (Claude Code Docs)
  4. React to events with a mod (Claude Code Docs)
  5. Mods reference (Claude Code Docs)
  6. Test a mod (Claude Code Docs)
  7. Manage mods for your organization (Claude Code Docs)
  8. Configure permissions (precedence among rules, hooks, and mods) (Claude Code Docs)
  9. Claude Code CHANGELOG (2.1.287 to 2.1.317) (Anthropic on GitHub, 2026-11-03)
  10. @anthropic-ai/claude-code on npm (2.1.317) (npm, 2026-11-03)
  11. Mods - make Claude 10x more extensible (design proposal, issue #91870) (Anthropic on GitHub, 2026-09-03)
  12. Built-in mods source (mods directory) (Anthropic on GitHub)
  13. Sample mods (claude-code-playground) (Anthropic on GitHub)
  14. Codex hooks (OpenAI)
SharePost on XLinkedIn

Comments

Share a question or note about this article.

Loading comments…