CMS-TK/1.0 · companion of CMS-WB/1.0 · 4 October 2026

Claude Code mods spec

Official handler, drawing, and organisation controls, from docs fetched 4 October 2026. For Karolis Valickas. British English. If a shape was not fetched, it stays unverified.

Use this first. Version, then where drawing works, then the trust quotes, then the control that matches your file.

Markdown twin: claude-mods-spec.md

Diagram sources: diagrams/cms-*.mmd

flowchart TB
  plugin["Plugin"]
  mod["Mod: JS or TS event handlers"]
  event["Claude Code calls a handler when an event happens"]
  watch["Watch"]
  change["Change"]
  takeover["Take over"]
  settingsHook["Settings-file hook"]
  bothCalled["Both are called"]
  plugin --> mod
  mod --> event
  event --> watch
  event --> change
  event --> takeover
  mod --> bothCalled
  settingsHook --> bothCalled
A mod is a plugin that changes how Claude Code looks and behaves. On these pages, hook means a mod handler. A settings hook is the settings-file kind. Both are called.

1. Check the version2. See where drawing works3. Read the trust quotes4. Open controls before you install

Start here

Version

2.1.287

Required. On by default. Check claude --version. ✓

Changelog

Added Claude Mods: plugins may now modify deeper behavior.

Same sentence in GitHub CHANGELOG.md and the docs page generated from it. Top section is 2.1.288. ✓ Reference still says as of v2.1.287. ?? GAP-01

Words

Handler can watch, change, or take over. JavaScript or TypeScript. ✓

This page does not add events or methods.

Five decisions

1

DEC-S01

Hook means the mod handler

A settings-file hook is a settings hook. Both are called.

2

DEC-S02

No extra APIs

If the 4 October 2026 fetch did not name it, it is not here.

3

DEC-S03

Gaps stay unverified

A missing page is not evidence the control does nothing.

4

DEC-S04

June 2026 list is older

That safe-mode list is not the mods-page list.

5

DEC-S05

Video was not a source

Offered, title confirmed, transcript not used.

flowchart TB
  host["Where the session runs"]
  term["Terminal, editor terminal, JetBrains: hooks yes, drawing yes"]
  desk["Desktop Code tab except WSL: hooks yes, drawing yes except terminal-only"]
  wsl["Desktop WSL: hooks no, drawing no"]
  vsc["VS Code extension chat: hooks yes, drawing no"]
  pipe["claude -p and Agent SDK: hooks yes, drawing no"]
  remote["Remote Control: hooks on your machine, drawing in that terminal"]
  cloud["Cloud: hooks if a plugin reaches the session, drawing no"]
  host --> term
  host --> desk
  host --> wsl
  host --> vsc
  host --> pipe
  host --> remote
  host --> cloud
Each branch is a host. e.surface is terminal or desktop. Desktop drawing skips elements marked terminal-only.

Hooks beside drawing

Surface Hooks Drawing Badge
claude in a terminalyesyes✓
editor integrated terminalyesyes✓
JetBrains pluginyesyes✓
Desktop Code tab except WSLyesyes, except elements marked terminal-only✓
Desktop WSLno (plugins unavailable)no✓
VS Code extension chatyesno✓
claude -p and Agent SDKyesno✓
Remote Control from claude.ai or mobileyes, in the session on your machinein the terminal on your machine✓
Cloud sessionyes, for a plugin that reaches the cloud sessionno✓

Hooks, no drawing

  • VS Code extension chat
  • claude -p and the Agent SDK
  • A cloud session, and only if the plugin reaches it

Neither

  • Desktop WSL. Plugins are unavailable, so hooks do not run and drawing does not run.

Remote Control is not this row. Hooks run in the session on your machine. Drawing is in the terminal on your machine.

flowchart TB
  loaded["Once loaded, the mod runs as you"]
  files["Read and write files"]
  proc["Start processes"]
  net["Make network requests"]
  secrets["Read secrets in env and settings, including an API key"]
  see["See every prompt and tool call"]
  rewrite["Rewrite a prompt or tool call, submit a prompt, or message another session"]
  approve["Approve a tool call before you are asked"]
  spend["Spend usage"]
  loaded --> files
  loaded --> proc
  loaded --> net
  loaded --> secrets
  loaded --> see
  loaded --> rewrite
  loaded --> approve
  loaded --> spend
Once loaded, the mod runs as you. This is the fact list, not a stronger warning than the quotes.
A mod is code that runs with your permissions. It can read and write your files, start processes, and make network requests. Install mods only from authors and marketplaces you trust.
Mods aren't sandboxed. If you turn on sandboxing, the sandbox isolates the Bash commands Claude runs, and a process that a mod starts runs outside it.
None of these controls sandboxes a mod. A mod you allow runs as the user, with the user's access to files, processes, and the network.
flowchart TB
  face["Interface"]
  much["Much of the interface can be restyled"]
  ask["AskUserQuestion is a render site"]
  perm["The permission prompt is not, and a mod cannot change what it shows"]
  face --> much
  face --> ask
  face --> perm
AskUserQuestion is a render site. The permission prompt is not. A mod cannot change what that prompt shows you.

Approve

A mod can approve a call an ask rule would prompt for, or that one of your own PreToolUse hooks blocked. In auto mode that call runs without a classifier check.

Where the guard loads, a user mod cannot approve a call a deny rule refuses, unless allowModsToOverrideDenyRules is set. A managed PreToolUse block is final.

Not covered

Neither deny rules nor managed PreToolUse apply to a mod's own $.fs and $.process calls.

Network policy covers $.http.fetch only. $.process.run is not covered.

Trust prompts come first. An untrusted directory loads no mod until you answer.

flowchart LR
  handler["Handler receives the mods API, e, and next"]
  watch["next of e: watch"]
  rewrite["next with a changed field: rewrite"]
  answer["Return without next: answer and short-circuit"]
  handler --> watch
  handler --> rewrite
  handler --> answer
next(e) watches. next with a changed field rewrites. A return without next answers and short-circuits. An event that does not spell one of these stays not spelled.

Subset on this tab: tool.call, tool.check, prompt.submit, prompt.fill, prompt.suggest, prompt.edit, turn.start, turn.step, turn.complete, command.run, config.set, session.start, session.append, agent.spawn, ui.render, plugin.register, telemetry.log. The Events tab is the full set. The subset is not a shorter truth.

Event Watch spelled Rewrite spelled Answer or other return Notes Badge
tool.callnext(e)change args via next; retry by calling next(e) again{deny: reason} or {result}Includes subagent and MCP. {result} answers without a permission prompt and without running the tool. After next(e): permission check, then the tool. A result may have deny or isError.✓
tool.checknext(e) resolves allow, ask, or denynot spelled{decision}Can approve a call a non-managed PreToolUse blocked.✓
prompt.submitnot spelled as bare next(e)rewrite text or context{drop: reason}Rewrite is spelled. The field names inside the rewrite are not spelled beyond text or context.✓
prompt.fillnot spelled as a separate shapenext(e) with changed textno separate answer shape spelledDo not invent an answer object.✓
prompt.suggestnot spelled as a separate shapenext(e) with changed textno separate answer shape spelledSame spelling as prompt.fill.✓
prompt.editnext(e)doc does not saydoc does not sayObserve is the only spelled path.✓
command.runnext(e)not spelled{text}, or {}{} prints nothing.✓
config.setnot spelled as bare next(e)next with a changed value{deny: reason}Rewrite or deny.✓
turn.startnext(e). Guide says observedoc does not saydoc does not sayObserve only, as spelled.✓
turn.stepyield* next(e)next with model or effortGuide: answer without calling the model. No separate object shape is spelled for that answer.Async generator. result.usage fields: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, plus model.✓
turn.completenext(e)not spelled{text}, a line under the answere.isAborted, e.answer, e.durationMs, e.usage.✓
session.startnext(e)not spellednot spelledOnce per loaded mod before the first prompt, and again after reload of that mod. Not after /clear, /resume, or /branch.✓
session.appendnot spelled as bare next(e)next with changed message contentnot spelledRewrite is the spelled path.✓
agent.spawnnot spelled{model} is a return, not described as next{deny: reason}Do not recast {model} as a next() rewrite.✓
ui.rendernext(e) leaves the drawingnext with changed props changes a detaila tree without next replaces the drawingFor AskUserQuestion, a tree must hold the engine reference exactly once.✓
plugin.registernot spellednot spelled{refuse: reason}e.tier is prepend, user, append, or builtin. e.uses lists events, API calls, env, state. If this hook throws or times out with no .catch, the check fails open and the mod loads.✓
telemetry.lognext(e)not spelled{deny: reason}An installed mod must filter {to: 'collector'} or validate fails. * does not match telemetry.✓

Who calls whom

sequenceDiagram
  participant Managed as Managed PreToolUse
  participant Mod
  participant Other as Other PreToolUse
  participant Perm as Permission check
  participant Tool
  Managed->>Mod: Runs before the first tool.call. A block is final
  alt Return without next
    Mod-->>Mod: Answer. Later PreToolUse hooks do not run
  else next of e
    Mod->>Other: Other PreToolUse after the last next
    Other->>Perm: Permission check
    Perm->>Tool: Then the tool
  end
Managed PreToolUse runs before the first tool.call and a block is final. Other PreToolUse hooks run after the last next. Answering without next skips them. Then permission, then the tool.

User story

flowchart LR
  dev["Person who trusts the author"]
  goal["See a tool-call count on the spinner"]
  mod["Handler on tool.call and ui.render"]
  dev --> goal
  goal --> mod
Expected story: someone who trusts the author wants a tool-call count on the spinner.

1

Install only from an author you trust.

2

tool.call counts and calls next(e).

3

ui.render for Spinner rewrites props.suffix.

4

The permission prompt stays as Claude Code drew it.

Baseline: settings hook

flowchart LR
  settings["Settings-file hook"]
  modh["Mod handler, called a hook on these pages"]
  both["Both are called"]
  settings --> both
  modh --> both
Both are called. Settings hooks are not deprecated. This fetch does not describe a drawing API for settings hooks. That absence is partial, not a proof that drawing from a settings file is impossible.

Useful

flowchart LR
  watch["tool.call calls next of e and counts"]
  match["ui.render matcher component Spinner"]
  rewrite["next with props.suffix changed"]
  watch --> match
  match --> rewrite
The overview example: watch tool.call, rewrite the spinner suffix. The declaration of calls was not in the quoted lines (GAP-13).
export function register(on) {
  on('tool.call', async ($, e, next) => {
    calls += 1
    $.ui.invalidate('ui.render')
    return next(e)
  })
  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
    return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
  })
}

hooks.json is { "modules": ["./register.js"] }. Files: first-mod/.claude-plugin/plugin.json, hooks/hooks.json, hooks/register.js.

Where it fails

flowchart TB
  deny["A deny rule refuses the call"]
  guard["The built-in guard is loaded"]
  unset["allowModsToOverrideDenyRules is unset"]
  noApprove["A user mod cannot approve that call"]
  allow["Set true: a user mod can approve a denied call"]
  deny --> guard
  guard --> unset
  unset --> noApprove
  guard --> allow
Unset, deny wins. true lets a user's approving mod approve a denied call. This does not sandbox the mod.
flowchart LR
  rules["Deny rules and managed PreToolUse"]
  toolCall["They can stop a tool call"]
  ownCalls["They do not apply to the mod own fs and process calls"]
  rules -->|"apply"| toolCall
  rules -->|"do not apply"| ownCalls
Deny rules and managed PreToolUse can stop a tool call. They do not apply to the mod's own $.fs and $.process calls.
flowchart LR
  crash["Hooks worker crashes three times"]
  unload["Unload every non-built-in mod"]
  back["Until reload-plugins or a new session"]
  crash --> unload
  unload --> back
Three crashes of the hooks worker unload every non-built-in mod until /reload-plugins or a new session.
flowchart TB
  sec["1 sec-default at builtin, where it loads"]
  pre["1 then prependPlugins"]
  org["1 then other organisation mods not in appendPlugins"]
  user["2 Mods you install"]
  app["3 appendPlugins"]
  builtin["4 Other built-in mods"]
  sec --> pre
  pre --> org
  org --> user
  user --> app
  app --> builtin
Group 1 is sec-default where it loads, then prependPlugins, then other organisation mods not in appendPlugins. Then mods you install, then appendPlugins, then other built-ins. Inside dependencies, a mod runs before the mods it lists.

Before the mods

Managed PreToolUse runs before the first mod's tool.call. A block is final.

After next

Other PreToolUse hooks run after the last mod calls next. Answering tool.call without next keeps them from running.

When a hook fails

stateDiagram-v2
  [*] --> Running
  Running --> Skipped: no catch, and throw, timeout, or wrong shape before next
  Running --> Stands: the same failure after next has resolved
  Running --> Unloaded: hooks worker crashes three times
  Unloaded --> Back: reload-plugins or a new session
No .catch, and a throw, a timeout, or the wrong shape: skipped if it is before next; the result stands if next has already resolved.
stateDiagram-v2
  [*] --> Check
  Check --> Refused: return refuse with a reason
  Check --> Loads: check passes
  Check --> LoadsOpen: throw or timeout with no catch fails open
plugin.register is different. A throw or a timeout with no .catch fails open, and the mod loads. {refuse: reason} refuses it.
Limit Spelled Badge
.catch1 second✓
Hook's own time10 seconds, not counting next or a mods API call other than $.clock.sleep✓
All session.end hooks together1.5 seconds✓
Time inside $.ui.askDoes not count✓
Time awaiting your own promiseDoes count✓
claude plugin test, one test5 seconds unless timeoutMs✓
Worker crashesThree times, then unload every non-built-in mod✓
flowchart TB
  brought["Who brought the mod"]
  yours["Counts as yours"]
  orgmod["Organisation mod"]
  blocked["allowManagedModsOnly: a user-brought mod does not load"]
  still["Organisation mods still load"]
  brought --> yours
  brought --> orgmod
  yours --> blocked
  orgmod --> still
allowManagedModsOnly stops user-brought mods. Organisation mods still load. Users cannot undo it. The same key in user, project, local, or --settings changes nothing.
flowchart TB
  test["Counts as organisation-owned only when all three hold"]
  en["managed enabledPlugins sets it true"]
  dir["Marketplace named as a directory by absolute path"]
  rel["Plugin listed by a relative path, so it loads in place"]
  cache["Cache copy from GitHub, git, URL, or npm"]
  userCopy["Counts as a user mod even if enabledPlugins enables it"]
  test --> en
  test --> dir
  test --> rel
  cache --> userCopy
All three must hold or a cache copy from GitHub, git, URL, or npm still counts as a user's mod.
flowchart TB
  machine["This machine"]
  managed["Has managed settings: guard loads"]
  team["Signed in with Team or Enterprise: guard loads"]
  apikey["API key, Bedrock, Agent Platform, or Foundry: guard only with managed settings"]
  unread["Guard cannot read managed settings"]
  refuseMods["Refuses every user mod"]
  unchecked["Cannot check deny rules for a call a user mod approved"]
  refuseCall["Refuses that call"]
  machine --> managed
  machine --> team
  machine --> apikey
  unread --> refuseMods
  unchecked --> refuseCall
API-key, Bedrock, Agent Platform, and Foundry users get the guard only when the machine has managed settings. If it cannot read managed settings, it refuses every user's mod.
refused by cc-plugin-sec-default: mods are limited to your organization's by policy (allowManagedModsOnly)

A hooks module can still log loaded before that refusal. Guard event names were not given (GAP-09).

User file beside managed file

disableAllHooks in managed settings

Stops mods in every installed plugin, yours included. Turns off every settings-file hook, so a managed PreToolUse no longer blocks. Custom status lines and /goal stop. Does not stop built-in mods. Skills and MCP are not mentioned on this sentence.

disableAllHooks in ~/.claude/settings.json

Stops every mod you installed, your settings hooks, and your custom status line. What the organisation manages keeps running. The plugin stays installed. Skills, commands, agents, and MCP still load. Built-ins are not affected.

--safe-mode

Every installed mod off for one session, organisation mods included, plus your other customisations. Built-ins keep running. The mods pages do not list the full set. A June 2026 what's-new page names CLAUDE.md, skills, plugins, hooks, MCP servers, and custom commands and agents. That page is older than mods. ~

--bare

Installed plugins that are not managed load no hooks module. Built-ins still run. ✓

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS is ignored at any value from v2.1.287 on. A 0 does not keep mods off. Replace it with allowManagedModsOnly. Settings hooks are not deprecated.

Control Where User-brought mods Organisation mods Settings hooks Built-ins Also stated Badge
allowManagedModsOnlypluginConfigs on cc-plugin-sec-default@builtin, managed settings only. The same entry in user, project, local, or --settings changes nothing.No user-brought mod loads: installed, --plugin-dir, or written mid-session. A GitHub or remote marketplace mod, and one the organisation turns on for members on claude.ai, counts as a user's and does not load. Users cannot undo it.Organisation mods still load.Users' settings hooks, status lines, and /goal keep working.Built-ins keep running.File or MDM policy covers Bedrock, Google Agent Platform, and Foundry. Debug line is quoted in Part 8.✓
disableAllHooks in managed settingsmanaged settings, trueStops mods in every installed plugin, yours included.Not given a separate exemption in that sentence. Do not invent one.Turns off every hook in settings files, so a managed PreToolUse no longer blocks. Custom status lines and /goal stop.Does not stop built-in mods.Skills, commands, agents, and MCP are not mentioned on this sentence.✓
disableAllHooks in ~/.claude/settings.jsonoverview of the user settings fileStops every mod you installed.What the organisation manages keeps running.Stops your settings hooks and custom status line.Built-ins not affected.Plugin stays installed. Skills, commands, agents, and MCP still load.✓
--safe-modeone sessionEvery installed mod off, and your other customisations.Organisation mods included, so they are off too.The mods pages do not give the full list.Does not stop built-in mods.Admin: start with claude --safe-mode to check whether a mod caused a problem. A June 2026 what's-new page lists CLAUDE.md, skills, plugins, hooks, MCP servers, and custom commands and agents. That page is older than mods.~
allowManagedHooksOnlyAdmin page points elsewhere. This fetch did not include that page.Only organisation mods and built-ins, so a user mod is not in that set.Organisation mods load.Also blocks hooks in users' own settings files.Built-ins load.Detailed page not fetched. Do not add controls from memory.~
disableSideloadFlagsnamed controlRejects --plugin-dir and --plugin-url, and keeps mods Claude writes from loading.Not stated.Not stated.Not stated.Also rejects --agents and --mcp-config.✓
allowModsToOverrideDenyRulesunset, or trueUnset: deny wins, so a user's approving mod cannot approve a denied call. true: it can.Not a loader switch.Not a settings-hook switch.Where the guard loads, this is the exception to deny-wins.Does not sandbox the mod.✓
prependPlugins and appendPluginsmanaged settings. User settings only on a machine with no managed settings, and only when not signed in with Team or Enterprise.Order group 2 is mods you install.prepend runs in group 1 after sec-default, where the guard loads. append is group 3. Other organisation mods not in appendPlugins stay in group 1.Not a hook switch.If you set prependPlugins, name sec-default@builtin or the guard does not load. pluginConfigs reads only cc-plugin-sec-default@builtin.Within dependencies, a mod runs before the mods it lists.✓
--bareone process flagInstalled plugins that are not managed load no hooks module.Managed plugins are the ones this sentence does not switch off. No further split is spelled.Not stated.Built-ins still run.Does not stop built-in mods.✓
CLAUDE_CODE_ENABLE_FUNCTION_HOOKSignoredv2.1.287+ ignores it at any value, so 0 does not keep mods off.Not a replacement for organisation policy.Settings hooks are not deprecated.Not a built-in switch.Replace a 0 with allowManagedModsOnly.✓
Remote offAnthropic, not a local settingInstalled mods can be turned off. No local setting turns them back on.Not stated as a separate class.Not stated.cc-plugin-plugin-authoring is off in that case. Other built-ins are not stated.claude plugin test: hooks modules are turned off in this process.✓
flowchart TB
  ev["Events named in the fetched pages"]
  ev --> tool["tool"]
  ev --> prompt["prompt, skill.prompt, attribution.text"]
  ev --> turn["turn and session"]
  ev --> ui["ui"]
  ev --> other["command, config, agent"]
  ev --> meta["plugin.register, engine.create, telemetry"]
  ev --> classic["classic dot name. Full list not fetched"]
  ev --> api["namespace.method for each mods API method"]
Families only. This is not call order. The classic.* list was not fetched.

Full set for this fetch. Do not add a row.

Event Watch spelled Rewrite spelled Answer or other return Notes Badge
tool.callnext(e)change args via next; retry by calling next(e) again{deny: reason} or {result}Includes subagent and MCP. {result} answers without a permission prompt and without running the tool. After next(e): permission check, then the tool. A result may have deny or isError.✓
tool.checknext(e) resolves allow, ask, or denynot spelled{decision}Can approve a call a non-managed PreToolUse blocked.✓
tool.describenot spellednot spelled{description}, optionally isDeferredNo other return is spelled.✓
prompt.submitnot spelled as bare next(e)rewrite text or context{drop: reason}Rewrite is spelled. The field names inside the rewrite are not spelled beyond text or context.✓
prompt.fillnot spelled as a separate shapenext(e) with changed textno separate answer shape spelledDo not invent an answer object.✓
prompt.suggestnot spelled as a separate shapenext(e) with changed textno separate answer shape spelledSame spelling as prompt.fill.✓
prompt.editnext(e)doc does not saydoc does not sayObserve is the only spelled path.✓
prompt.composenot spellednot spelled{sections}, a list of {id, text, scope}Answer shape only.✓
prompt.sectionnot spellednot spelled{text} or {text: null}Answer shape only.✓
prompt.contextnot spellednot spelled{blocks}Answer shape only.✓
prompt.attachmentnot spellednot spelled{text} or {text: null}Answer shape only.✓
skill.promptnot spellednot spelled{text}Answer shape only.✓
attribution.textnot spellednot spelled{text}Answer shape only.✓
command.runnext(e)not spelled{text}, or {}{} prints nothing.✓
command.describenot spellednot spelled{description, argumentHint, isHidden}Answer shape only.✓
config.setnot spelled as bare next(e)next with a changed value{deny: reason}Rewrite or deny.✓
config.describenot spellednot spelled{label, description, isHidden}Answer shape only.✓
turn.startnext(e). Guide says observedoc does not saydoc does not sayObserve only, as spelled.✓
turn.stepyield* next(e)next with model or effortGuide: answer without calling the model. No separate object shape is spelled for that answer.Async generator. result.usage fields: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, plus model.✓
turn.completenext(e)not spelled{text}, a line under the answere.isAborted, e.answer, e.durationMs, e.usage.✓
session.startnext(e)not spellednot spelledOnce per loaded mod before the first prompt, and again after reload of that mod. Not after /clear, /resume, or /branch.✓
session.endnext(e)not spellednot spellede.reason is clear, resume, logout, prompt_input_exit, or other. /branch reports resume.✓
session.compactnot spellednot spelled{skip: reason}Answer shape only.✓
session.receivenot spellednot spelled{consumed: reason}, to keep it from Claudeorigin.kind: peer, peer-send-message, task-notification, scheduled-trigger.✓
session.sendnot spellednot spelled{isDelivered: false, reason}origin.kind: model or plugin.✓
session.appendnot spelled as bare next(e)next with changed message contentnot spelledRewrite is the spelled path.✓
session.attachnext(e)doc does not saydoc does not sayObserve only, as spelled.✓
session.detachnext(e)doc does not saydoc does not sayObserve only, as spelled.✓
session.measurenext(e)doc does not saydoc does not sayObserve only, as spelled.✓
agent.offernot spellednot spelled{isOffered: false}Answer shape only.✓
agent.spawnnot spelled{model} is a return, not described as next{deny: reason}Do not recast {model} as a next() rewrite.✓
ui.rendernext(e) leaves the drawingnext with changed props changes a detaila tree without next replaces the drawingFor AskUserQuestion, a tree must hold the engine reference exactly once.✓
ui.resolvedoc does not say watch, rewrite, or take overdoc does not saythe result is what $.ui.resolve(e) readsTake-over words are not in the doc.~
ui.pressno return shape in the tableno return shape in the tableno return shape in the tableDo not invent one.??
ui.inputno return shape in the tableno return shape in the tableno return shape in the tableDo not invent one.??
ui.selectno return shape in the tableno return shape in the tableno return shape in the tableDo not invent one.??
ui.focusno return shapeno return shapeno return shapeDo not invent one.??
ui.scrollno return shapeno return shapeno return shapeDo not invent one.??
ui.closeno return shapeno return shapeno return shapee.origin.kind is plugin, person, or unload. That is event data, not a return shape.??
ui.messageno return shapeno return shapeno return shapeDo not invent one.??
plugin.registernot spellednot spelled{refuse: reason}e.tier is prepend, user, append, or builtin. e.uses lists events, API calls, env, state. If this hook throws or times out with no .catch, the check fails open and the mod loads.✓
engine.createnot spellednot spelled as nexta hook can return a changed mods APIThe doc spells a changed API, not watch or rewrite.✓
telemetry.lognext(e)not spelled{deny: reason}An installed mod must filter {to: 'collector'} or validate fails. * does not match telemetry.✓
telemetry.marknext(e)not spelled{deny: reason}Same collector rule as telemetry.log. * does not match.✓
classic.<settings hook name>not enumeratednot enumeratednot enumeratedExamples named: classic.Stop, classic.PostToolUse, classic.SessionStart. 'classic.*' matches every settings-hook event. The full list was not fetched.??
namespace.methodnext(e)not spelled as a field rewrite{deny} or {value}Every mods API method is also an event under this name. This is not a catalogue of methods.✓
flowchart TB
  sites["Render sites not marked terminal-only"]
  sites --> pane["Pane"]
  sites --> above["AbovePrompt"]
  sites --> user["UserMessage"]
  sites --> asst["AssistantMessage"]
  sites --> use["ToolUse"]
  sites --> result["ToolResult"]
  sites --> group["ToolGroup"]
  sites --> cmd["CommandOutput"]
  sites --> ask["AskUserQuestion"]
  sites --> spin["Spinner"]
  sites --> mode["SessionMode"]
  sites --> hint["PromptHint"]
Render sites that are not marked terminal-only. The permission prompt is not among them.
flowchart TB
  termOnly["Marked terminal-only"]
  termOnly --> prog["ToolProgress"]
  termOnly --> dur["TurnDuration"]
  termOnly --> info["InfoNotice"]
  termOnly --> raster["Raster"]
  termOnly --> image["Image"]
  deskOnly["Desktop only"]
  deskOnly --> svg["Svg"]
Terminal-only sites and elements, and Svg which is Desktop only. VS Code chat, claude -p, the Agent SDK, and cloud do not draw.

The mod never reads the keyboard itself. It cannot bind Tab or arrow keys.

Render site Drawing note Badge
Panenot marked terminal-only✓
AbovePromptnot marked terminal-only✓
UserMessagenot marked terminal-only✓
AssistantMessagenot marked terminal-only✓
ToolUsenot marked terminal-only✓
ToolResultnot marked terminal-only✓
ToolGroupnot marked terminal-only✓
CommandOutputnot marked terminal-only✓
AskUserQuestionnot marked terminal-only. A replacement tree must hold the engine reference exactly once.✓
Spinnernot marked terminal-only✓
SessionModenot marked terminal-only✓
PromptHintnot marked terminal-only✓
ToolProgressterminal only✓
TurnDurationterminal only✓
InfoNoticeterminal only✓
Element Where Limit spelled Badge
Boxnamedno limit spelled✓
Textnamedone string child, 10000 characters✓
Buttonnamedno limit spelled✓
Linknamedno limit spelled✓
Codenamedup to 10000 characters✓
Markdownnamedup to 10000 characters✓
Inputnamedno limit spelled✓
Selectnamedno limit spelled✓
SvgDesktop onlysource up to 131072 characters✓
Clientnamedno limit spelled✓
Rasterterminal onlycolumns up to 512, rows up to 256✓
Imageterminal onlyPNG or RGBA up to 2 MiB, or a file path✓

$.ui names

$.ui name What is spelled Badge
resolveresult of the ui.resolve hook is what $.ui.resolve(e) reads. Guides: resolve elements this app can draw.✓
invalidateRedraw. Throttled to 10 a second, or 30 a second in the terminal, for a visible pane, the expanded band, and the hint line.✓
openfocus, closeOnEscape, and holdToasts accept only true. false throws. Resolves isPlaced true, or false with a reason. A pane opened without the user asking waits until 144 columns, or 110 after they opened one once.✓
closeNamed in the method list. No further behaviour is spelled here.~
panesName only.~
focusName only.~
scrollName only.~
toast4 seconds unless timeoutMs.✓
statusOne line under the prompt.✓
logA dim transcript line Claude does not read. log to debug writes the debug log.✓
noticeName only.~
askQuestion above a numbered list, then a row for typing and Chat about this. Rejects if dismissed, if Chat about this, or under claude -p. Time inside $.ui.ask does not count toward the hook limit.✓
copyThe test stub returns isCopied true. Guides do not describe it beyond the name.~
blitRepaints one Raster.✓
selectionNot in the reference method list. Changelog 2.1.288: returns the text you last selected in fullscreen mode and, when the selection lies within one transcript row, that row.~

Other names that appear

Not an API index. A missing name is GAP-10, unverified, not a claim that the method does not exist.

Name What is spelled Badge
$.http.fetchNetwork policy covers this call only.✓
$.process.runNot covered by that network policy.✓
$.fs and $.processDeny rules and managed PreToolUse do not apply to a mod's own calls.✓
$.fs.writeNot atomic. No other write contract is spelled.✓
$.fs.readValidate example lists it. Packaging says write the call in full, as $.fs.read(...). Arguments are not spelled.✓
$.storeJSON under ~/.claude/plugins/store/. 4 MiB total. Not atomic.✓
$.store.setNamed on a validate example line. No signature is spelled.✓
$.ui.*Method names in the interface section. Not a signature list, except where a guide spells behaviour.✓
$.ui.resolveReads the ui.resolve result.✓
$.ui.invalidateUsed in the smallest example as $.ui.invalidate('ui.render'), and described as redraw.✓
$.ui.askAsk behaviour and the timing exception are spelled.✓
$.ui.selectionChangelog 2.1.288 only. Absent from the reference method list.~
$.clock.sleepNamed only because its time counts toward the 10 second hook limit. Other mods API calls do not.✓
$.stateNamed only as a reason to ship types/index.d.ts, or when adding a namespace. No methods are spelled.~
flowchart TB
  root["Plugin directory"]
  manifest[".claude-plugin/plugin.json"]
  hooks["hooks/hooks.json modules array"]
  reg["exports register of on and options"]
  esm["ES module, no Node bundler, no require"]
  root --> manifest
  root --> hooks
  hooks --> reg
  reg --> esm
plugin.json has no required mods fields. hooks.json lists one relative module. The module exports register(on, options). ES module. No Node bundler. No require.

Allowed

  • Extensions: js, mjs, cjs, jsx, ts, mts, cts, tsx
  • Relative imports inside the plugin
  • Bare import of claude-code
  • Event names as string literals
  • Calls written in full, as $.fs.read(...)

Rejected

  • Dynamic import()
  • require
  • Assigning $ or a namespace to a variable
  • Shadowing on inside register
  • A mods API use that claude plugin validate cannot read

Also ship

  • options.userConfig
  • pluginConfigs keyed by plugin id
  • types/index.d.ts when using $.state or adding a namespace
  • Optional *.test.ts

The hooks module has no Node.js APIs, no setTimeout, and no network or file access of its own. URL, TextEncoder, AbortController, and crypto.subtle are available.

Install

  • /plugin install name@marketplace, or claude plugin install
  • /reload-plugins if a session is open
  • claude --plugin-dir, repeatable
  • CLAUDE_CODE_PLUGIN_DIRS: absolute paths, separated by a colon, or by a semicolon on Windows
  • CLAUDE_CODE_PLUGIN_DIR_WATCH=1 reloads --plugin-dir mods on save in a long-running non-interactive session
stateDiagram-v2
  [*] --> Written: Claude writes under dev-mods for that session
  Written --> ThisSession: you approve hot reload
  ThisSession --> Removed: folder older than cleanupPeriodDays
A mod Claude writes loads only in that session, after you approve hot reload. cleanupPeriodDays has no number on the mods pages (GAP-06).

Path: ~/.claude/dev-mods/<session-id>/<name>/. $.store is JSON under ~/.claude/plugins/store/, 4 MiB total, not atomic. $.fs.write is not atomic either.

Built-ins

Name What is spelled Badge
cc-plugin-agents-mdLoads AGENTS.md.✓
cc-plugin-diffTakes over /diff.✓
cc-plugin-plugin-authoringSkill only, no mod code. Off when installed mods are turned off remotely.✓
cc-plugin-sec-defaultUsers cannot turn it off.✓
cc-plugin-telemetryNamed. No further behaviour is spelled here.~
cc-plugin-you-should-knowOff by default. /plugin enable cc-plugin-you-should-know@builtin. Changelog: first-party sessions with telemetry on.✓

Samples, names only (READMEs not fetched, GAP-08): token-weather, blast-radius, replay-theater, in claude-code/mods of claude-code-playground, marketplace claude-code-playground-mods. Tutorials first-mod and hello-tabs. gallery is a sample harness.

Validate

sequenceDiagram
  participant You
  participant Validate as claude plugin validate
  participant Test as claude plugin test
  You->>Validate: Point it at the directory
  Note over Validate: Does not run the code
  Validate-->>You: Hooks and calls it can read
  You->>Test: Run tests
  Note over Test: One test is 5 seconds unless timeoutMs
validate does not run the code. --strict makes warnings errors. --json is available. One test is 5 seconds unless timeoutMs.

Example lines: hooks: session.start, tool.call, ui.render{component=Pane} and calls: $.fs.read, $.http.fetch, $.store.set, $.ui.open.

Message Means Badge
no hooks module to loadmods can load✓
hooks modules are turned off heredisableAllHooks✓
hooks modules are turned off in this processAnthropic turned installed mods off remotely✓
not reportedThis command does not report allowManagedModsOnly✓

The mods active line omits built-ins. You cannot update or uninstall a built-in. disableAllHooks, --bare, and --safe-mode do not stop built-in mods.

flowchart TB
  gaps["Labelled unverified or incomplete"]
  gaps --> classic["classic star list not fetched"]
  gaps --> page["allowManagedHooksOnly page not fetched"]
  gaps --> days["cleanupPeriodDays has no number here"]
  gaps --> guard["Guard hooked event names not given"]
  gaps --> readme["Sample READMEs not fetched"]
  gaps --> shape["Several UI events have no return shape"]
  gaps --> sel["selection is not in the method list"]
A gap is unverified or incomplete. It is not proof the thing is false.
ID Badge Gap
GAP-01??The reference says as of v2.1.287. The changelog top section is 2.1.288. This page does not invent what else 2.1.288 changed.
GAP-02??$.ui.selection() is in the 2.1.288 changelog and is not in the reference method list.
GAP-03??ui.press, ui.input, ui.select, ui.focus, ui.scroll, ui.close, and ui.message have no return shape.
GAP-04??The full classic.* list was not fetched. Named examples only: classic.Stop, classic.PostToolUse, classic.SessionStart.
GAP-05~panes, focus, scroll, and notice are names only. close is a method name without further behaviour here. copy is the test stub isCopied true, and guides do not describe it beyond the name.
GAP-06??cleanupPeriodDays is not defined on the mods pages. The dev-mod folder is deleted once older than that number.
GAP-07??allowManagedHooksOnly: the admin page points elsewhere, and this fetch did not include that page.
GAP-08??Sample READMEs were not fetched. Names only: token-weather, blast-radius, replay-theater, tutorials first-mod and hello-tabs. gallery is a sample harness.
GAP-09??The guard's exact hooked event names are not given.
GAP-10??The full mods API method catalogue is not in this fact pack. Only names that appear are listed.
GAP-11~process.spawn is named as an async generator. Its return shape is not in the event list.
GAP-12~The June 2026 what's-new safe-mode list is older than mods. Its URL was not in this fact pack. The mods pages do not define the full safe-mode list.
GAP-13??The declaration of calls was not in the quoted smallest example.
GAP-14??YouTube https://youtu.be/Rn4nmFRPe0s was offered. Title confirmed via oEmbed. Transcript was not downloaded (HTTP 429). Not used as a source.

Limitations

  • Formative extract, 4 October 2026. Not a security sign-off and not a running-version test.
  • Reference text is as of v2.1.287 while the changelog top section is 2.1.288 (GAP-01).
  • classic.* is not enumerated (GAP-04). The allowManagedHooksOnly page was not fetched (GAP-07).
  • $.ui.selection() is a changelog sentence, not a reference method (GAP-02).
  • The June 2026 safe-mode list is older than mods, and its URL was not in this fetch (GAP-12).
  • No sample README was fetched (GAP-08). The YouTube transcript was not used (GAP-14).
  • The survey twins in this folder are a different edition and were not used as facts.
flowchart TB
  src["This edition"]
  docs["Mods docs fetched 4 October 2026"]
  chg["Changelog 2.1.287 and top section 2.1.288"]
  old["June 2026 what is new, older than mods"]
  yt["YouTube offered. Transcript not used"]
  src --> docs
  src --> chg
  src --> old
  src -->|"not a source"| yt
The YouTube edge is not a source. Title via oEmbed: Claude Mods Is The Biggest Claude Code Upgrade Since Skills, by Chase AI. Transcript not downloaded (HTTP 429). Contents are not described.
ID Tier Status Name URL
SRC-001T1✓ ✓ Fetched 4 October 2026Mods overviewhttps://code.claude.com/docs/en/plugins/mods/overview.md
SRC-002T1✓ ✓ Fetched 4 October 2026Mods adminhttps://code.claude.com/docs/en/plugins/mods/admin.md
SRC-003T1✓ ✓ Fetched 4 October 2026Mods referencehttps://code.claude.com/docs/en/plugins/mods/reference.md
SRC-004T1✓ ✓ Fetched 4 October 2026Mods eventshttps://code.claude.com/docs/en/plugins/mods/events.md
SRC-005T1✓ ✓ Fetched 4 October 2026Mods APIhttps://code.claude.com/docs/en/plugins/mods/api.md
SRC-006T1✓ ✓ Fetched 4 October 2026Mods interfacehttps://code.claude.com/docs/en/plugins/mods/interface.md
SRC-007T1✓ ✓ Fetched 4 October 2026Mods galleryhttps://code.claude.com/docs/en/plugins/mods/gallery.md
SRC-008T1✓ ✓ Fetched 4 October 2026Create a modhttps://code.claude.com/docs/en/plugins/mods/create.md
SRC-009T1✓ ✓ Fetched 4 October 2026Test a modhttps://code.claude.com/docs/en/plugins/mods/test.md
SRC-010T1✓ ✓ Fetched 4 October 2026Troubleshoot modshttps://code.claude.com/docs/en/plugins/mods/troubleshoot.md
SRC-011T1✓ ✓ Fetched 4 October 2026Docs changelog, generated from the GitHub filehttps://code.claude.com/docs/en/changelog.md
SRC-012T1✓ ✓ Fetched 4 October 2026GitHub CHANGELOG.mdhttps://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
SRC-013T3?? ?? Not usedYouTube, Chase AI. Title via oEmbed onlyhttps://youtu.be/Rn4nmFRPe0s

T1 is Anthropic docs, or the changelog those docs are generated from. T3 is the video that was not used. There is no T2 in this edition. There is no ✓✓.