# Claude Code mods spec

- **Edition:** CMS-WB/1.0
- **Companion HTML:** CMS-TK/1.0 (`claude-mods-spec.html`)
- **Prepared for:** Karolis Valickas
- **Date:** 4 October 2026
- **Date format:** D Month YYYY, Europe/Vilnius.
- **Status:** Formative extract of official docs fetched 4 October 2026. Not a validated control sign-off. Not a complete API catalogue.
- **Scope:** Only events, methods, and controls in that fetch. Nothing was added from the folder's survey pages or from training memory.
- **Spelling:** British English in this edition's prose. Quotes and identifiers keep the docs' spelling, including behavior in the changelog sentence.
- **Related, not this edition:** `claude-mod-landscape.md` (CML-WB/1.0) is a survey. Do not treat its gaps or star counts as facts of this spec.

## Status banner

Where a page was not fetched, a return shape is missing, or a number is not defined, the row is unverified (`??`) or partial (`~`). This edition does not fill those holes.

## How to read badges

| Badge | Meaning in this edition |
| --- | --- |
| ✓ | Stated in the 4 October 2026 official-docs fact pack, and tied to the sources below. |
| ~ | Partial: name only, an older page, a changelog line that is not in the reference method list, or a shape the doc does not finish. |
| ?? | Unverified gap. Not a fact to build on, and not evidence that the missing thing is false. |


There is no `✓✓` or `✓✓✓`. This pass did not re-test a running Claude Code.

## What this contains

| ID | Instrument | What it is not |
| --- | --- | --- |
| S-DEF | Mod definition and the v2.1.287 line | Not a settings hook, and not the whole plugin |
| S-SURF | Where hooks run and where drawing runs | Not a product catalogue |
| S-TRUST | What a loaded mod can do, in the docs' words | Not a sandbox, and not a stronger warning than the quotes |
| S-HAN | Handler contract: watch, rewrite, answer | Not a permission to invent return fields |
| S-EVT | Event return shapes, complete for this fetch | Not events outside the list |
| S-ORD | Load order, tool.call order, and failure | Not the guard's event names (GAP-09) |
| S-UI | Render sites, elements, and $.ui names | Not $.ui.selection() as a reference method (GAP-02) |
| S-PKG | Packaging, install, validate, test | Not a Node bundler guide |
| S-ORG | User mods beside organisation controls | Not the unfetched allowManagedHooksOnly page (GAP-07) |
| S-GAP | Gaps | A gap is unverified, not a claim that the thing does not exist |


# Part 1 — Definition and version

A mod is a plugin that changes how Claude Code looks and behaves. It is JavaScript or TypeScript event handlers. Claude Code calls one when an event happens. The handler can watch, change, or take over. ✓

On these pages, hook means a mod handler. A settings-file hook is a settings hook. Both are called. ✓

Require v2.1.287 or newer. Mods are on by default. Check with `claude --version`. ✓

GitHub `CHANGELOG.md` section `## 2.1.287` says: Added Claude Mods: plugins may now modify deeper behavior. The same sentence is on the docs changelog, which says it is generated from that file. The current top section is 2.1.288. ✓ The reference says as of v2.1.287 (GAP-01, ??).

```mermaid
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
```


Caption: a mod handler and a settings hook are different, and both are called.

# Part 2 — Where hooks run and where drawing runs

`e.surface` is `terminal` or `desktop`. ✓

```mermaid
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
```


Caption: each branch is a host, not a sequence inside one turn.

| Surface | Hooks | Drawing | Badge |
| --- | --- | --- | --- |
| claude in a terminal | yes | yes | ✓ |
| editor integrated terminal | yes | yes | ✓ |
| JetBrains plugin | yes | yes | ✓ |
| Desktop Code tab except WSL | yes | yes, except elements marked terminal-only | ✓ |
| Desktop WSL | no (plugins unavailable) | no | ✓ |
| VS Code extension chat | yes | no | ✓ |
| claude -p and Agent SDK | yes | no | ✓ |
| Remote Control from claude.ai or mobile | yes, in the session on your machine | in the terminal on your machine | ✓ |
| Cloud session | yes, for a plugin that reaches the cloud session | no | ✓ |


Desktop Code tab drawing is yes except elements marked terminal-only. Those elements are in Part 6. ✓

# Part 3 — Trust

The three sentences below are quotes. They are not paraphrased into a stronger claim. ✓

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

```mermaid
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
```


Caption: once loaded, a mod can do all of these. The diagram is the fact list, not an extra threat model.

Once loaded a mod can: act as you; read secrets in env and settings including an API key; see every prompt and tool call; rewrite a prompt or tool call, submit a prompt, or message another session; approve a tool call before you are asked; spend usage. ✓

A mod that approves tool calls can approve one an ask rule would prompt for, or that one of your own PreToolUse hooks blocked. Where the built-in guard loads, a user's mod cannot approve a call a deny rule refuses, unless `allowModsToOverrideDenyRules` is set. A PreToolUse block in managed settings is final. Neither deny rules nor managed PreToolUse apply to a mod's own `$.fs` and `$.process` calls. ✓

In auto mode, a call the mod approves runs without a classifier check. ✓

```mermaid
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
```


Caption: AskUserQuestion is a render site. The permission prompt is not. A mod can restyle much of the interface, but not the permission prompt. It can't change what a prompt shows you. ✓

Trust prompts come first. In an untrusted directory, no mod loads until the prompt is answered. ✓

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

# Part 4 — Handler contract

The handler receives `$`, `e`, and `next`. `e` is deeply frozen. Assigning to it throws. ✓

`next(e)` observes. `next({...e, field})` rewrites. A return without `next` answers and short-circuits. ✓ That general contract is not a licence to invent a rewrite field an event does not spell. Part 5 says not spelled where the fetch did not spell it.

`on` takes an optional matcher. `on` returns a registration with `.catch`. `turn.step` and `process.spawn` are async generators. Other hooks are async functions. ✓ `process.spawn` has no return shape in the event list (GAP-11, ~).

`'*'` matches every event except telemetry. `'classic.*'` matches every settings-hook event. Registering the same event twice with no matcher fails to load. ✓

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. Outside its own code, a hook must call the mods API. Claude Code refuses to load a mod that uses the mods API in a way `claude plugin validate` cannot read. ✓

```mermaid
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
```


Caption: three exits. An event that does not spell one of them stays not spelled in Part 5.

### Subset rule

The HTML tab Watch, rewrite, answer shows a subset: 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 table below, and the HTML Events tab, are the full set from this fetch. The subset is not a shorter truth.

| Event | Watch spelled | Rewrite spelled | Answer or other return | Notes | Badge |
| --- | --- | --- | --- | --- | --- |
| tool.call | next(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.check | next(e) resolves allow, ask, or deny | not spelled | {decision} | Can approve a call a non-managed PreToolUse blocked. | ✓ |
| prompt.submit | not 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.fill | not spelled as a separate shape | next(e) with changed text | no separate answer shape spelled | Do not invent an answer object. | ✓ |
| prompt.suggest | not spelled as a separate shape | next(e) with changed text | no separate answer shape spelled | Same spelling as prompt.fill. | ✓ |
| prompt.edit | next(e) | doc does not say | doc does not say | Observe is the only spelled path. | ✓ |
| command.run | next(e) | not spelled | {text}, or {} | {} prints nothing. | ✓ |
| config.set | not spelled as bare next(e) | next with a changed value | {deny: reason} | Rewrite or deny. | ✓ |
| turn.start | next(e). Guide says observe | doc does not say | doc does not say | Observe only, as spelled. | ✓ |
| turn.step | yield* next(e) | next with model or effort | Guide: 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.complete | next(e) | not spelled | {text}, a line under the answer | e.isAborted, e.answer, e.durationMs, e.usage. | ✓ |
| session.start | next(e) | not spelled | not spelled | Once per loaded mod before the first prompt, and again after reload of that mod. Not after /clear, /resume, or /branch. | ✓ |
| session.append | not spelled as bare next(e) | next with changed message content | not spelled | Rewrite is the spelled path. | ✓ |
| agent.spawn | not spelled | {model} is a return, not described as next | {deny: reason} | Do not recast {model} as a next() rewrite. | ✓ |
| ui.render | next(e) leaves the drawing | next with changed props changes a detail | a tree without next replaces the drawing | For AskUserQuestion, a tree must hold the engine reference exactly once. | ✓ |
| plugin.register | not spelled | not 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.log | next(e) | not spelled | {deny: reason} | An installed mod must filter {to: 'collector'} or validate fails. * does not match telemetry. | ✓ |


### Expected user story

| Step | What happens |
| --- | --- |
| 1 | You install a mod only from an author and marketplace you trust. |
| 2 | tool.call runs, counts, and calls next(e). |
| 3 | ui.render for Spinner rewrites props.suffix. |
| 4 | The permission prompt is unchanged, because it is not a render site. |


### Baseline

The baseline is a settings hook, not a second mod. Both are called. Settings hooks are not deprecated. This fact pack does not describe a drawing API for settings hooks. ✓ for both-are-called and not-deprecated. The drawing sentence is the absence of a claim (~), not a claim that drawing is impossible from a settings file.

```mermaid
flowchart LR
  settings["Settings-file hook"]
  modh["Mod handler, called a hook on these pages"]
  both["Both are called"]
  settings --> both
  modh --> both
```


```mermaid
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
```


### Useful example

The smallest overview example is the useful case: watch tool.call, then rewrite the spinner. The declaration of `calls` was not in the quoted lines (GAP-13, ??).

```mermaid
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
```


```js
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`, and `hooks/register.js`. ✓

### Where it fails

Where the built-in guard loads, a user's mod cannot approve a call a deny rule refuses, unless `allowModsToOverrideDenyRules` is set. A managed PreToolUse block is final. Those two controls do not cover the mod's own `$.fs` and `$.process` calls. ✓

```mermaid
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
```


```mermaid
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
```


Returning from tool.call without next keeps later PreToolUse hooks from running. ✓

If the hooks worker crashes three times, Claude Code unloads every non-built-in mod until `/reload-plugins` or a new session. ✓

```mermaid
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
```


Desktop WSL is the host where plugins are unavailable, so hooks do not run and drawing does not run. ✓

# Part 5 — Events

Only the rows below. Do not add an event because it would be useful.

```mermaid
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"]
```


Caption: families, not a call order. classic.* is incomplete (GAP-04).

| Event | Watch spelled | Rewrite spelled | Answer or other return | Notes | Badge |
| --- | --- | --- | --- | --- | --- |
| tool.call | next(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.check | next(e) resolves allow, ask, or deny | not spelled | {decision} | Can approve a call a non-managed PreToolUse blocked. | ✓ |
| tool.describe | not spelled | not spelled | {description}, optionally isDeferred | No other return is spelled. | ✓ |
| prompt.submit | not 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.fill | not spelled as a separate shape | next(e) with changed text | no separate answer shape spelled | Do not invent an answer object. | ✓ |
| prompt.suggest | not spelled as a separate shape | next(e) with changed text | no separate answer shape spelled | Same spelling as prompt.fill. | ✓ |
| prompt.edit | next(e) | doc does not say | doc does not say | Observe is the only spelled path. | ✓ |
| prompt.compose | not spelled | not spelled | {sections}, a list of {id, text, scope} | Answer shape only. | ✓ |
| prompt.section | not spelled | not spelled | {text} or {text: null} | Answer shape only. | ✓ |
| prompt.context | not spelled | not spelled | {blocks} | Answer shape only. | ✓ |
| prompt.attachment | not spelled | not spelled | {text} or {text: null} | Answer shape only. | ✓ |
| skill.prompt | not spelled | not spelled | {text} | Answer shape only. | ✓ |
| attribution.text | not spelled | not spelled | {text} | Answer shape only. | ✓ |
| command.run | next(e) | not spelled | {text}, or {} | {} prints nothing. | ✓ |
| command.describe | not spelled | not spelled | {description, argumentHint, isHidden} | Answer shape only. | ✓ |
| config.set | not spelled as bare next(e) | next with a changed value | {deny: reason} | Rewrite or deny. | ✓ |
| config.describe | not spelled | not spelled | {label, description, isHidden} | Answer shape only. | ✓ |
| turn.start | next(e). Guide says observe | doc does not say | doc does not say | Observe only, as spelled. | ✓ |
| turn.step | yield* next(e) | next with model or effort | Guide: 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.complete | next(e) | not spelled | {text}, a line under the answer | e.isAborted, e.answer, e.durationMs, e.usage. | ✓ |
| session.start | next(e) | not spelled | not spelled | Once per loaded mod before the first prompt, and again after reload of that mod. Not after /clear, /resume, or /branch. | ✓ |
| session.end | next(e) | not spelled | not spelled | e.reason is clear, resume, logout, prompt_input_exit, or other. /branch reports resume. | ✓ |
| session.compact | not spelled | not spelled | {skip: reason} | Answer shape only. | ✓ |
| session.receive | not spelled | not spelled | {consumed: reason}, to keep it from Claude | origin.kind: peer, peer-send-message, task-notification, scheduled-trigger. | ✓ |
| session.send | not spelled | not spelled | {isDelivered: false, reason} | origin.kind: model or plugin. | ✓ |
| session.append | not spelled as bare next(e) | next with changed message content | not spelled | Rewrite is the spelled path. | ✓ |
| session.attach | next(e) | doc does not say | doc does not say | Observe only, as spelled. | ✓ |
| session.detach | next(e) | doc does not say | doc does not say | Observe only, as spelled. | ✓ |
| session.measure | next(e) | doc does not say | doc does not say | Observe only, as spelled. | ✓ |
| agent.offer | not spelled | not spelled | {isOffered: false} | Answer shape only. | ✓ |
| agent.spawn | not spelled | {model} is a return, not described as next | {deny: reason} | Do not recast {model} as a next() rewrite. | ✓ |
| ui.render | next(e) leaves the drawing | next with changed props changes a detail | a tree without next replaces the drawing | For AskUserQuestion, a tree must hold the engine reference exactly once. | ✓ |
| ui.resolve | doc does not say watch, rewrite, or take over | doc does not say | the result is what $.ui.resolve(e) reads | Take-over words are not in the doc. | ~ |
| ui.press | no return shape in the table | no return shape in the table | no return shape in the table | Do not invent one. | ?? |
| ui.input | no return shape in the table | no return shape in the table | no return shape in the table | Do not invent one. | ?? |
| ui.select | no return shape in the table | no return shape in the table | no return shape in the table | Do not invent one. | ?? |
| ui.focus | no return shape | no return shape | no return shape | Do not invent one. | ?? |
| ui.scroll | no return shape | no return shape | no return shape | Do not invent one. | ?? |
| ui.close | no return shape | no return shape | no return shape | e.origin.kind is plugin, person, or unload. That is event data, not a return shape. | ?? |
| ui.message | no return shape | no return shape | no return shape | Do not invent one. | ?? |
| plugin.register | not spelled | not 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.create | not spelled | not spelled as next | a hook can return a changed mods API | The doc spells a changed API, not watch or rewrite. | ✓ |
| telemetry.log | next(e) | not spelled | {deny: reason} | An installed mod must filter {to: 'collector'} or validate fails. * does not match telemetry. | ✓ |
| telemetry.mark | next(e) | not spelled | {deny: reason} | Same collector rule as telemetry.log. * does not match. | ✓ |
| classic.<settings hook name> | not enumerated | not enumerated | not enumerated | Examples named: classic.Stop, classic.PostToolUse, classic.SessionStart. 'classic.*' matches every settings-hook event. The full list was not fetched. | ?? |
| namespace.method | next(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. | ✓ |


### tool.call order

```mermaid
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
```


Caption: managed PreToolUse runs before the first mod's tool.call, and a block is final. Other PreToolUse hooks run after the last mod calls next. Answering without next keeps those from running. After next(e): permission check, then the tool. ✓

# Part 6 — Load order and failure

Order: (1) `sec-default@builtin` where it loads, then `prependPlugins`, then other organisation mods not in `appendPlugins`; (2) mods you install; (3) `appendPlugins`; (4) other built-in mods. Within dependencies, a mod runs before the mods it lists. ✓

```mermaid
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
```


Caption: four groups. Group 1 is three steps, not three equal peers.

A hook with no `.catch` that throws, times out, or returns the wrong shape: if that happens before next, the hook is skipped; if it happens after next has resolved, that result stands. `.catch` is limited to 1 second. The hook's own time is 10 seconds, not counting next or a mods API call other than `$.clock.sleep`. All `session.end` hooks together have 1.5 seconds. Time inside `$.ui.ask` does not count. Time awaiting your own promise does. ✓

```mermaid
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
```


```mermaid
stateDiagram-v2
  [*] --> Check
  Check --> Refused: return refuse with a reason
  Check --> Loads: check passes
  Check --> LoadsOpen: throw or timeout with no catch fails open
```


Caption: plugin.register is the exception. If it throws or times out with no `.catch`, the check fails open and the mod loads. A `{refuse: reason}` return refuses it. ✓

# Part 7 — Interface

Drawing follows Part 2. `e.surface` is terminal or desktop. The mod never reads the keyboard itself. It cannot bind Tab or arrow keys. ✓

```mermaid
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"]
```


```mermaid
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"]
```


| Render site | Drawing note | Badge |
| --- | --- | --- |
| Pane | not marked terminal-only | ✓ |
| AbovePrompt | not marked terminal-only | ✓ |
| UserMessage | not marked terminal-only | ✓ |
| AssistantMessage | not marked terminal-only | ✓ |
| ToolUse | not marked terminal-only | ✓ |
| ToolResult | not marked terminal-only | ✓ |
| ToolGroup | not marked terminal-only | ✓ |
| CommandOutput | not marked terminal-only | ✓ |
| AskUserQuestion | not marked terminal-only. A replacement tree must hold the engine reference exactly once. | ✓ |
| Spinner | not marked terminal-only | ✓ |
| SessionMode | not marked terminal-only | ✓ |
| PromptHint | not marked terminal-only | ✓ |
| ToolProgress | terminal only | ✓ |
| TurnDuration | terminal only | ✓ |
| InfoNotice | terminal only | ✓ |


| Element | Where | Limit spelled | Badge |
| --- | --- | --- | --- |
| Box | named | no limit spelled | ✓ |
| Text | named | one string child, 10000 characters | ✓ |
| Button | named | no limit spelled | ✓ |
| Link | named | no limit spelled | ✓ |
| Code | named | up to 10000 characters | ✓ |
| Markdown | named | up to 10000 characters | ✓ |
| Input | named | no limit spelled | ✓ |
| Select | named | no limit spelled | ✓ |
| Svg | Desktop only | source up to 131072 characters | ✓ |
| Client | named | no limit spelled | ✓ |
| Raster | terminal only | columns up to 512, rows up to 256 | ✓ |
| Image | terminal only | PNG or RGBA up to 2 MiB, or a file path | ✓ |


| $.ui name | What is spelled | Badge |
| --- | --- | --- |
| resolve | result of the ui.resolve hook is what $.ui.resolve(e) reads. Guides: resolve elements this app can draw. | ✓ |
| invalidate | Redraw. Throttled to 10 a second, or 30 a second in the terminal, for a visible pane, the expanded band, and the hint line. | ✓ |
| open | focus, 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. | ✓ |
| close | Named in the method list. No further behaviour is spelled here. | ~ |
| panes | Name only. | ~ |
| focus | Name only. | ~ |
| scroll | Name only. | ~ |
| toast | 4 seconds unless timeoutMs. | ✓ |
| status | One line under the prompt. | ✓ |
| log | A dim transcript line Claude does not read. log to debug writes the debug log. | ✓ |
| notice | Name only. | ~ |
| ask | Question 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. | ✓ |
| copy | The test stub returns isCopied true. Guides do not describe it beyond the name. | ~ |
| blit | Repaints one Raster. | ✓ |
| selection | Not 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. | ~ |


`$.ui.selection()` is not in that method list (GAP-02). The changelog sentence is the only behaviour spelled for it.

### Names that appear

This is not an API index. A name absent from this table was not in the fetch (GAP-10, ??).

| Name | What is spelled | Badge |
| --- | --- | --- |
| $.http.fetch | Network policy covers this call only. | ✓ |
| $.process.run | Not covered by that network policy. | ✓ |
| $.fs and $.process | Deny rules and managed PreToolUse do not apply to a mod's own calls. | ✓ |
| $.fs.write | Not atomic. No other write contract is spelled. | ✓ |
| $.fs.read | Validate example lists it. Packaging says write the call in full, as $.fs.read(...). Arguments are not spelled. | ✓ |
| $.store | JSON under ~/.claude/plugins/store/. 4 MiB total. Not atomic. | ✓ |
| $.store.set | Named 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.resolve | Reads the ui.resolve result. | ✓ |
| $.ui.invalidate | Used in the smallest example as $.ui.invalidate('ui.render'), and described as redraw. | ✓ |
| $.ui.ask | Ask behaviour and the timing exception are spelled. | ✓ |
| $.ui.selection | Changelog 2.1.288 only. Absent from the reference method list. | ~ |
| $.clock.sleep | Named only because its time counts toward the 10 second hook limit. Other mods API calls do not. | ✓ |
| $.state | Named only as a reason to ship types/index.d.ts, or when adding a namespace. No methods are spelled. | ~ |


# Part 8 — User mods and organisation controls

None of these controls sandboxes a mod. The third quote in Part 3 is the limit. ✓

```mermaid
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
```


Caption: allowManagedModsOnly blocks user-brought mods. Organisation mods still load. Users cannot undo it. ✓

An organisation mod counts as yours only when all three hold: managed `enabledPlugins` sets it true, managed settings name the marketplace as a directory by absolute path, and the marketplace lists the plugin by a relative path so it loads in place. A plugin copied into the cache from GitHub, git, URL, or npm counts as a user's even if managed `enabledPlugins` enables it. ✓

```mermaid
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
```


The guard loads when the machine has managed settings, or the user is signed in with Team or Enterprise. API-key, Bedrock, Agent Platform, or Foundry users get the guard only on a machine with managed settings. If the guard cannot read managed settings, it refuses every user's mod. If it cannot check deny rules for a call a user's mod approved, it refuses the call. ✓ Guard event names: GAP-09, ??.

```mermaid
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
```


Debug line, kept as written: 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. ✓

| Control | Where | User-brought mods | Organisation mods | Settings hooks | Built-ins | Also stated | Badge |
| --- | --- | --- | --- | --- | --- | --- | --- |
| allowManagedModsOnly | pluginConfigs 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 settings | managed settings, true | Stops 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.json | overview of the user settings file | Stops 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-mode | one session | Every 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. | ~ |
| allowManagedHooksOnly | Admin 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. | ~ |
| disableSideloadFlags | named control | Rejects --plugin-dir and --plugin-url, and keeps mods Claude writes from loading. | Not stated. | Not stated. | Not stated. | Also rejects --agents and --mcp-config. | ✓ |
| allowModsToOverrideDenyRules | unset, or true | Unset: 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 appendPlugins | managed 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. | ✓ |
| --bare | one process flag | Installed 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_HOOKS | ignored | v2.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 off | Anthropic, not a local setting | Installed 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. | ✓ |


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

Side by side, the two `disableAllHooks` placements are not the same switch. Managed settings stop mods in every installed plugin and turn settings-file hooks off, so a managed PreToolUse no longer blocks. In `~/.claude/settings.json`, what the organisation manages keeps running. `--safe-mode` includes organisation mods and turns them off for one session. ✓

# Part 9 — Packaging, install, validate

Required: `.claude-plugin/plugin.json` (no required mods fields). `hooks/hooks.json` with a `modules` array of one relative path. The hooks module exports `register(on, options)`. Extensions: js, mjs, cjs, jsx, ts, mts, cts, tsx. It is an ES module. `options.userConfig`. `pluginConfigs` keyed by plugin id. `types/index.d.ts` when using `$.state` or adding a namespace. Optional `*.test.ts` via `claude plugin test`. ✓

No Node bundler. Import only inside the plugin by relative path. The bare import that is allowed is `claude-code`. Dynamic `import()` fails validation. No `require`. Event names are string literals. API calls are written in full, as `$.fs.read(...)`. Do not assign `$` or a namespace to a variable. Do not shadow `on` inside `register`. ✓

```mermaid
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
```


Install: `/plugin install name@marketplace` or `claude plugin install`. `/reload-plugins` if a session is open. `claude --plugin-dir` is repeatable. `CLAUDE_CODE_PLUGIN_DIRS` is absolute paths separated by `:` , or by `;` on Windows. `CLAUDE_CODE_PLUGIN_DIR_WATCH=1` reloads `--plugin-dir` mods on save in a long-running non-interactive session. ✓

A mod Claude writes lands in `~/.claude/dev-mods/<session-id>/<name>/`. It loads only in that session after you approve hot reload. The folder is deleted once older than `cleanupPeriodDays`. That number is not defined on the mods pages (GAP-06, ??). ✓ for the path and the approval. ?? for the number.

```mermaid
stateDiagram-v2
  [*] --> Written: Claude writes under dev-mods for that session
  Written --> ThisSession: you approve hot reload
  ThisSession --> Removed: folder older than cleanupPeriodDays
```


`$.store` is JSON under `~/.claude/plugins/store/`. 4 MiB total. Not atomic. `$.fs.write` is not atomic either. ✓

### Built-ins and samples

| Name | What is spelled | Badge |
| --- | --- | --- |
| cc-plugin-agents-md | Loads AGENTS.md. | ✓ |
| cc-plugin-diff | Takes over /diff. | ✓ |
| cc-plugin-plugin-authoring | Skill only, no mod code. Off when Anthropic turns installed mods off remotely. | ✓ |
| cc-plugin-sec-default | Users cannot turn it off. | ✓ |
| cc-plugin-telemetry | Named. No further behaviour is spelled here. | ~ |
| cc-plugin-you-should-know | Disabled by default. Enable with /plugin enable cc-plugin-you-should-know@builtin. The changelog says it is for first-party sessions with telemetry on. | ✓ |


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

### Validate and test

`claude plugin validate <dir>` does not run the code. `--strict` treats warnings as errors. `--json` is a flag. Example lines: `hooks: session.start, tool.call, ui.render{component=Pane}` and `calls: $.fs.read, $.http.fetch, $.store.set, $.ui.open`. ✓

```mermaid
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
```


`claude plugin test`. One test is 5 seconds unless `timeoutMs`. ✓

| Message | Means | Badge |
| --- | --- | --- |
| no hooks module to load | mods can load | ✓ |
| hooks modules are turned off here | disableAllHooks | ✓ |
| hooks modules are turned off in this process | Anthropic turned installed mods off remotely | ✓ |
| (not reported) | This command does not report allowManagedModsOnly | ✓ |


# Part 10 — Decisions

| ID | Decision |
| --- | --- |
| DEC-S01 | On these pages, hook means a mod handler. A settings-file hook is a settings hook. Both are called. |
| DEC-S02 | No event, method, or control is added beyond the 4 October 2026 fetch. |
| DEC-S03 | A gap stays unverified. A missing page is not evidence the control does nothing. |
| DEC-S04 | The June 2026 what's-new safe-mode list is older than mods. It is not the mods-page list. |
| DEC-S05 | The YouTube video was offered and is not a source. Its contents are not described. |


# Part 11 — Gaps

```mermaid
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"]
```


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


# Part 12 — Sources

```mermaid
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
```


Caption: the YouTube edge is labelled not a source. The title, confirmed via oEmbed, is Claude Mods Is The Biggest Claude Code Upgrade Since Skills by Chase AI. The transcript was not downloaded (HTTP 429).

| ID | Tier | Status | Name | URL |
| --- | --- | --- | --- | --- |
| SRC-001 | T1 | ✓ Fetched 4 October 2026 | Mods overview | https://code.claude.com/docs/en/plugins/mods/overview.md |
| SRC-002 | T1 | ✓ Fetched 4 October 2026 | Mods admin | https://code.claude.com/docs/en/plugins/mods/admin.md |
| SRC-003 | T1 | ✓ Fetched 4 October 2026 | Mods reference | https://code.claude.com/docs/en/plugins/mods/reference.md |
| SRC-004 | T1 | ✓ Fetched 4 October 2026 | Mods events | https://code.claude.com/docs/en/plugins/mods/events.md |
| SRC-005 | T1 | ✓ Fetched 4 October 2026 | Mods API | https://code.claude.com/docs/en/plugins/mods/api.md |
| SRC-006 | T1 | ✓ Fetched 4 October 2026 | Mods interface | https://code.claude.com/docs/en/plugins/mods/interface.md |
| SRC-007 | T1 | ✓ Fetched 4 October 2026 | Mods gallery | https://code.claude.com/docs/en/plugins/mods/gallery.md |
| SRC-008 | T1 | ✓ Fetched 4 October 2026 | Create a mod | https://code.claude.com/docs/en/plugins/mods/create.md |
| SRC-009 | T1 | ✓ Fetched 4 October 2026 | Test a mod | https://code.claude.com/docs/en/plugins/mods/test.md |
| SRC-010 | T1 | ✓ Fetched 4 October 2026 | Troubleshoot mods | https://code.claude.com/docs/en/plugins/mods/troubleshoot.md |
| SRC-011 | T1 | ✓ Fetched 4 October 2026 | Docs changelog, generated from the GitHub file | https://code.claude.com/docs/en/changelog.md |
| SRC-012 | T1 | ✓ Fetched 4 October 2026 | GitHub CHANGELOG.md | https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md |
| SRC-013 | T3 | ?? Not used | YouTube, Chase AI. Title via oEmbed only | https://youtu.be/Rn4nmFRPe0s |


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

## 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). allowManagedHooksOnly's detailed 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, DEC-S05).
- The survey twins in this folder are a different edition and were not used as facts.

