Summary: Claude Code mods are plugins with a hooks.json that names a TypeScript or JavaScript file, and the functions in that file run inside Claude Code’s own process rather than as a separate script. As of Claude Code v2.1.287 (per a Firecrawl post by Richard Oliver Bray, published October 6, 2026), the early-access feature flag is gone and mods are on by default. That changes what a plugin can do: instead of just watching lifecycle events from outside, your code can rewrite events and draw React-style UI above the prompt, with no build step.
The difference that matters: same process vs. subprocess
Regular Claude Code hooks — now called settings hooks — run outside Claude Code entirely. A command hook launches a program, HTTP and prompt hooks call out, and Claude Code waits for the result. That isolation is the safety property: if your script crashes, Claude Code doesn’t.
Function hooks trade isolation for reach. A mod’s code runs inside Claude Code’s process, so it can intercept a tool call before it continues, wrap later mods like Express middleware (each hook gets a next function), and hand the interface a tree of elements to render. People have drawn Tetris and terminal browsers above the prompt this way, and since it all runs locally, none of it costs tokens — relevant if you’re already tracking where your agent spend goes, as in my earlier piece on cutting an agent’s token bill.
If you set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS during early access, remove it. Versions 2.1.287 and later ignore the variable, and setting it to 0 does not disable mods. To actually turn them off, disable the plugin, start with --safe-mode, or set "disableAllHooks": true.
The smallest useful mod is three files
No bundler, no package install. Claude Code loads TypeScript directly. The structure:
demo/
├── .claude-plugin/
│ └── plugin.json
├── hooks/
│ ├── hooks.json
│ └── register.tsx
└── tsconfig.json
The manifest is a standard plugin manifest — mods add no required fields. What turns the plugin into a mod is the modules key in hooks/hooks.json, pointing at your code file. That file exports a register function:
import type { Register } from 'claude-code'
let showing = false
export const register: Register = on => {
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
if (!showing) return next(e)
const { Box, Text } = await $.ui.resolve(e)
return (
<Box flexDirection="column">
<Text color="red">Hello from a Claude Mod</Text>
{await next(e)}
</Box>
)
})
}
Every hook gets three arguments: $ (the mods API — the only door to files, processes, the network, commands, and drawing), e (the event as frozen data), and next. Registering /hello in a session.start hook and flipping showing in a matched command.run hook gets you a toggleable banner. The whole demo is about 37 lines.
Hot reload and the validation loop
Claude Code watches mod folders loaded with --plugin-dir and reloads them on save, printing a line listing the hooks it registered. That line is your first debugging tool: a missing hook usually means a misspelled event name. One gotcha — reload runs register from scratch, so module-level variables reset. Use $.state for anything that should survive.
claude plugin validate ./demo prints what Claude Code reads from your mod without loading it: every event, matcher, and mods API call. That output exists because of the constraint that shapes everything else.
Why you can’t import npm packages
Claude Code has to read the whole mod and list everything it does before it runs. A hooks module may import only its own relative files plus Claude Code’s module for types. No ambient fs or fetch — you go through $.fs, $.http, $.process, so static analysis can name every side effect for review. WebAssembly is out too. The practical exception is a small, dependency-free ES module copied into the plugin folder.
If writing by hand feels slow, describe the mod in a Claude Code session — it ships a plugin-authoring skill, writes files under a protected dev-mods path, and points it at the built-in mods in the claude-code repository for examples.
The honest limitation: mods that Claude writes in-session only load in that session, so copy the folder somewhere permanent. Start with something like a git-branch display above the prompt — small enough to debug in one reload cycle, useful enough to keep.
