Claude Code Mods in TypeScript: Production Guardrails for Next.js Teams (2026)
Anthropic shipped Claude Code mods on October 1, 2026. Build a TypeScript plugin that blocks force-pushes, redacts secrets from tool output, and asks before destructive shell commands, without replacing human review.
By Mussawar Hayat
Claude Code Mods Are the New Control Plane
On October 1, 2026 Anthropic shipped mods for Claude Code: small TypeScript or JavaScript functions that sit on the events the agent already emits. A mod can rewrite a prompt, deny a tool call, redact a secret from tool output, or draw extra UI. They ship inside plugins and run in the CLI and the desktop app, starting with Claude Code v2.1.287.
That is the practical answer to the conversation dominating developer timelines this week. Coding agents are no longer a novelty. Teams are arguing about how to keep them from pushing to main, leaking .env values back into the model, or running rm -rf because a prompt looked plausible. Hooks could observe. Mods can rewrite and answer. This guide builds one production-shaped mod for a Next.js and TypeScript repo, using only the shapes documented by Anthropic.
What You Will Learn
- How a mod differs from settings hooks, skills, and MCP servers
- The plugin layout Claude Code loads with no bundler
- A TypeScript module that denies force-pushes, asks before destructive Bash, and redacts secrets from tool results
- How matchers,
next, andtool.checkfit together - Security limits: mods are not sandboxed, and managed settings still win
- How to load, hot-reload, and share the plugin without weakening review
Table of Contents
- The problem mods actually solve
- How a mod works
- Plugin layout
- Step-by-step implementation
- Best practices for Next.js teams
- Security considerations
- Performance notes
- Common mistakes
- Real use cases
- FAQ
- Summary
1. The Problem
Claude Code already has permission rules, auto mode, and settings hooks. Those layers still leave a gap teams hit every day on a Next.js codebase:
- A Bash allow rule cannot easily say “git push is fine, except on main, except force.”
- A settings hook can block a call. It cannot rewrite the tool result so a leaked
DATABASE_URLnever re-enters the model context. - Skills describe how the agent should behave. They do not enforce that behavior when the model ignores the skill.
- MCP servers add tools. They do not intercept Edit, Write, or Bash.
Mods close that gap with a middleware chain. Official docs describe three moves: observe (call next(e) after your work), rewrite (call next with a changed copy), or answer (return a result and never call next). Events are frozen. You copy them; you do not mutate them.
2. How a Mod Works
A mod is a plugin whose hooks/hooks.json lists a module under modules. Claude Code calls the exported register(on, options) function once on load and again on hot reload. Inside register, on(event, matcher?, hook) subscribes to an event.
Every hook receives three arguments:
$— the mods API ($.ui,$.command,$.process,$.state, and others)e— the event payloadnext— the rest of the chain, ending in Claude Code itself
The events this guide uses are documented on the mods events page:
tool.callfires before a tool runs, including subagent and MCP tools.e.toolis the name. Bash exposese.command. Edit and Write exposee.file_path. Returning{ deny: 'reason' }skips the tool. Returning{ result: '...' }answers the call yourself.tool.checkfires after permission rules and settings hooks.next(e)resolves toallow,ask, ordeny. A mod can return a different decision. Managed-settings blocks still win.prompt.submitseese.textbefore the turn. You can pass a trimmed copy or add privatecontext.ui.renderwith a matcher such as{ component: 'Spinner' }can append text the user sees.
Matchers compare fields. A string matches one value, an array matches any value, a regular expression matches a pattern. Register each event and matcher pair once. A second bare on('session.start') fails to load.
Mods are not sandboxed. Anthropic’s launch note is explicit: a mod has the same access to the machine as Claude Code. Install only code you have read. On Team and Enterprise plans a built-in mod named sec-default loads first and stops user mods from overriding permission deny rules. If you replace that load order, keep sec-default in the list.
3. Plugin Layout
Claude Code loads .js, .mjs, and .ts directly. You do not need Node, a bundler, or a build step for the mod itself. This layout matches the official create-a-mod tutorial:
next-guard/
.claude-plugin/plugin.json
hooks/hooks.json
hooks/register.tsCheck the CLI before you start. Mods require Claude Code v2.1.287 or later:
claude --version4. Step-by-Step Implementation
4.1 Manifest
Save this as next-guard/.claude-plugin/plugin.json. The manifest is an ordinary plugin manifest. The modules entry in hooks.json is what makes it a mod.
{
"name": "next-guard",
"version": "0.1.0",
"description": "Blocks force-pushes, asks before destructive shell commands, and redacts secrets from tool output.",
"author": { "name": "Your Team" }
}4.2 Point Claude Code at the module
Save this as next-guard/hooks/hooks.json:
{
"description": "Next.js repo guardrails",
"modules": ["./register.ts"]
}4.3 The hooks module
Save this as next-guard/hooks/register.ts. The patterns below are taken from the official events guide: matchers, { deny }, $.ui.ask, and post-tool result rewriting. The secret scan is local string replacement. It is a backstop, not a guarantee that a secret never existed on disk.
const FORCE_PUSH = /git push\b[\s\S]*--force|git push\b[\s\S]*\s-f\b/
const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bprisma\s+migrate\s+reset\b|\bdropdb\b/
const SECRET = /(sk-[A-Za-z0-9_\-]{20,}|postgres(?:ql)?:\/\/\S+|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]+PRIVATE KEY-----)/g
function scrub(value: string): string {
return value.replace(SECRET, '[redacted]')
}
export function register(on: Function) {
on('tool.call', { tool: 'Bash' }, async ($: any, e: any, next: Function) => {
const command = String(e.command ?? '')
if (FORCE_PUSH.test(command)) {
return {
deny: 'Force pushes are blocked by next-guard. Push a new branch and open a pull request.'
}
}
if (!RISKY.test(command)) return next(e)
let answer = 'Refuse'
try {
answer = await $.ui.ask('Run this command? ' + command, ['Run it', 'Refuse'])
} catch {
// Dismissed dialog, or a headless claude -p run with nobody to ask.
}
if (answer !== 'Run it') {
return {
deny: 'The user declined this command. Propose a narrower command or ask first.'
}
}
return next(e)
})
on('tool.call', { tool: ['Edit', 'Write'] }, async ($: any, e: any, next: Function) => {
const filePath = String(e.file_path ?? '')
if (/(^|\/)\.env(\..*)?$/.test(filePath) || filePath.includes('secrets/')) {
return {
deny: 'next-guard blocks writes to env and secrets paths. Edit those files yourself.'
}
}
return next(e)
})
on('tool.call', async (_$: any, e: any, next: Function) => {
const result = await next(e)
if (!result || typeof result !== 'object') return result
const text = typeof result.result === 'string' ? result.result : ''
if (!text || !SECRET.test(text)) return result
SECRET.lastIndex = 0
return { ...result, result: scrub(text) }
})
on('tool.check', { tool: 'Bash' }, async ($: any, e: any, next: Function) => {
const decided = await next(e)
const command = String(e.input?.command ?? '')
if (!command.includes('git push')) return decided
const branch = await $.process.run(['git', 'branch', '--show-current'])
if (String(branch.stdout ?? '').trim() !== 'main') return decided
return { decision: 'deny', reason: 'Push from a branch other than main.' }
})
}What each hook does:
- The Bash
tool.callhook answers force-pushes itself, so the command never runs and no permission prompt appears. Forrm -rf,git reset --hard,prisma migrate reset, anddropdb, it waits on$.ui.ask. Official docs say time spent inside a mods API call does not count against the hook time limit. A custom promise does. Default the answer to refuse so a headlessclaude -prun cannot click through. - The Edit/Write hook refuses paths that look like env files. Do this in the mod and also keep those files out of the worktree the agent can see.
- The unfiltered
tool.callhook runs afternextand rewrites string results that match a small secret pattern. Later mods see the event first if they loaded earlier; stack order is load order. Put the redaction mod early if you want it to see raw output, and remember that a deny from an earlier hook never reaches this one. - The
tool.checkhook refusesgit pushwhile the current branch ismain. This is a reminder for the agent. Protectmainon the Git host as well. A text match missesgit -C ../other pushand shell wrappers.
4.4 Load it
Start a session with the plugin directory. Claude Code watches that directory and hot-reloads when the module changes. Module-level variables reset on reload; use plugin state if you need a counter to survive a save.
claude --plugin-dir ./next-guardAsk the agent to run git push --force on a throwaway branch. The command should not run, and the transcript should show the deny text. Ask it to print a fake sk- token from a fixture file and confirm the model’s next turn sees [redacted]. Then open /plugin, select Installed, and confirm you can disable the mod.
4.5 Share it
A mod Claude writes into ~/.claude/dev-mods/ loads only in that session, and Claude Code deletes the folder after cleanupPeriodDays. Copy the directory into the repo, for example tools/next-guard, and document the launch flag. To publish it, package the plugin and submit it to the Claude plugin directory. Teammates should review the module in the same pull request as any other executable.
5. Best Practices
- Keep the mod small and boring. One plugin for guardrails, another for UI. A 2,000-line mod is as hard to review as the agent output you are trying to control.
- Write deny text as an instruction the model can follow. “Blocked” is worse than “Push a new branch and open a pull request.”
- Prefer matchers over a giant
ifon every tool. A Bash matcher does not run for Read. - Do not duplicate policy that a permission rule already expresses.
Bash(npm test)needs no code. Usetool.checkwhen the decision depends on live state, such as the current branch. - Pair the mod with the project rules you already keep for Claude Code. The mod enforces a few sharp edges.
CLAUDE.mdstill has to describe the App Router tree, the Prisma client, and the test command. - Work on a branch. A mod that blocks pushes to
mainis not a substitute for branch protection. - Version the plugin next to the app. When Next.js or Prisma conventions change, the path denylist should change in the same commit.
6. Security Considerations
- Mods run with Claude Code’s privileges. A malicious mod can read the same files and call the same tools the agent can. Review source before
--plugin-diror a marketplace install. - Managed settings hooks run before a mod’s
tool.callhook, and a block from them is final. Do not assume a user mod can weaken an org deny. sec-defaultexists so user mods cannot override permission deny rules on Team and Enterprise. Leave it in the load list.- Redaction in the tool result does not delete the secret from disk, shell history, or a transcript line you already printed with
$.ui.log. Keep production credentials off the agent machine. - Regex denylists miss obfuscation.
git push --forceis easy.bash -cwith a constructed string is not. Treat the mod as one layer beside host-side sandboxing and GitHub branch protection. - Headless
claude -pruns have nobody to answer$.ui.ask. The sample defaults to refuse. Do not default to allow. - Hot reload re-runs
register. A bug that throws during register disables the guard until you fix it. Watch the transcript line that lists hooks after a reload.
7. Performance Notes
A hook runs on the events you subscribe to. An unfiltered tool.call hook runs for every tool, including Read. Keep that hook to a string scan and an early return. Do not shell out on the hot path.
$.process.run in the tool.check sample spawns git only when the command text contains git push. That is one process per push attempt, not per keystroke. Avoid network calls inside tool.call. Official docs warn that a hook which exceeds its time limit is skipped, and a skipped guard means the tool runs.
UI invalidation and spinner rewrites are cheap if you only change a suffix. Do not fetch status from CI on every spinner frame.
8. Common Mistakes
- Mutating
e. The event is frozen. Assigning a field throws. Copy it. - Registering the same event twice without a matcher. The module fails to load.
- Calling
nextand also returning{ deny }. If you callednext, the tool already ran. Answer by returning withoutnext. - Forgetting
SECRET.lastIndex = 0after a global regex test. The next call can miss a match. - Assuming the mod replaces auto mode, permission rules, or code review. It does not.
- Shipping the plugin only in
~/.claude. Session mods are deleted. Commit the directory. - Trusting a marketplace listing you have not read. Same rule as an npm install on a laptop that holds production tokens.
9. Real Use Cases
- App Router refactors. Allow Edit under
app/andcomponents/, deny writes underprisma/migrationsunless a human runs the migrate command. - Preview deploys. A
tool.checkhook can denyvercel --prodwhile the branch is not a release branch, and leave preview deploys to the existing permission rule. - Secret hygiene. Scrub tool output before the model plans the next edit, so a dumped
.env.localdoes not get copied into a Server Action. - Review support. A spinner suffix that counts Bash calls makes a long auto-mode session visible without adding tokens to the prompt.
- Team policy. Admins can load an org mod before user mods and record tool names. Anthropic’s launch post lists audit logging as an intended team use. Log names and paths, not raw command output.
FAQ
Do I need Claude Code 2.1.287?
Yes. Mods load on v2.1.287 or later, in the CLI and the desktop app. Check with claude --version. They are on by default; --safe-mode, --bare, and disableAllHooks stop them.
Is a mod the same as a settings hook?
No. Settings hooks can observe and block. Mods can also rewrite the event, replace the result, draw UI, and register commands. Managed settings hooks still run before a mod’s tool hook, and their block is final.
Can I write the mod in TypeScript?
Yes. Claude Code loads .ts and .tsx modules directly. The official tutorial uses JavaScript; the same register export works in TypeScript. You do not bundle it.
Will this stop every dangerous command?
No. Matchers and regexes miss wrapped shells, aliases, and tools you did not list. Use the mod with branch protection, a sandbox, and human review of the diff.
Where should the plugin live?
In the repo, loaded with claude --plugin-dir, or installed from a marketplace you trust. Session mods under ~/.claude/dev-mods are temporary.
Summary
Claude Code mods, released October 1, 2026, are the first supported way to rewrite agent events in TypeScript without waiting for a product feature. The useful production shape is small: deny force-pushes, ask before destructive shell commands, refuse env-file writes, and scrub secrets from tool results before the model reads them.
Keep the module in the repository, load it with --plugin-dir, and treat it as code. It does not replace permission rules, sec-default, branch protection, or review. It makes those layers harder to skip on an ordinary Next.js session.
Key Takeaway
Ship a reviewed TypeScript mod that answers the dangerous calls itself, and leave everything else to next.
Need a production agent workflow on Next.js?
I design and build React, Next.js, Node.js, and TypeScript systems, including the guardrails around coding agents. Get in touch or see the services page.
Related reading: Claude Code auto mode production guide and Postgres write guardrails for AI agents.
References
Frequently Asked Questions
Do I need Claude Code 2.1.287?
Yes. Mods load on v2.1.287 or later, in the CLI and the desktop app. Check with claude --version. They are on by default; --safe-mode, --bare, and disableAllHooks stop them.
Is a mod the same as a settings hook?
No. Settings hooks can observe and block. Mods can also rewrite the event, replace the result, draw UI, and register commands. Managed settings hooks still run before a mod tool hook, and their block is final.
Can I write the mod in TypeScript?
Yes. Claude Code loads .ts and .tsx modules directly. The official tutorial uses JavaScript; the same register export works in TypeScript. You do not bundle it.
Will this stop every dangerous command?
No. Matchers and regexes miss wrapped shells, aliases, and tools you did not list. Use the mod with branch protection, a sandbox, and human review of the diff.
Where should the plugin live?
In the repo, loaded with claude --plugin-dir, or installed from a marketplace you trust. Session mods under ~/.claude/dev-mods are temporary.
Related guides
How to run Octomind AI agents against a Next.js 16 app in production. Covers agent-generated Playwright tests, TypeScript config, CI integration on preview deploys, auth handling, flake control, and when autonomous e2e agents beat hand-written suites.
Production Evals for Coding Agents in TypeScript and Next.js (2026)Code generation is cheap. Knowing the agent is right is not. This guide shows how to score TypeScript and Next.js coding agents with fixture tasks, deterministic checks, LLM judges used only where needed, merge gates, and a CI harness you can run without a human watching every diff.
Claude Fable 5.1 in Next.js 16: Messages API, Tool Use, and Preserved Thinking (2026)Production guide for calling Claude Fable 5.1 from Next.js 16 with the official Anthropic TypeScript SDK. Covers the Messages API, streaming, tool loops, max_tokens budgets, and the Fable 5.1 breaking changes around forced tool use and preserved thinking blocks.
