Troubleshoot a mod
Work out why a Claude Code mod does nothing - where Claude Code reports problems, every refusal and skip message, drawing failures, lost state and the debug log.
When a mod's module or one of its hooks fails, Claude Code skips it and carries on. That keeps your session alive, but it means a broken mod usually looks like a mod that simply does nothing. This page is the checklist I work through, organised by symptom.
First two moves
1. See what Claude Code reads from the files, without starting a session:
claude plugin validate ./edit-meter
This catches misspelled events, bad manifests and unreadable modules. Check the hooks: and calls: lines are what you expect (see /docs/plugins/mods/create).
2. Find the line Claude Code wrote. When a module fails to load, a hook is skipped, or another mod refuses yours, Claude Code writes one line naming your mod. Where it goes depends on the session:
| Session | Where the line appears |
|---|---|
Interactive with --plugin-dir, or with hot reloading enabled for Claude-written mods | A dim line in the transcript |
| Any other interactive session (for example a marketplace-installed mod) | The debug log only; start with claude --debug |
claude -p with --plugin-dir | stderr, in the default text output. Refusals by other mods go to the debug log only |
Can mods load here at all?
Run claude plugin test from a folder that contains no mod. No session needed:
| Output includes | Meaning |
|---|---|
no hooks module to load | Mods can load; there was just nothing to test |
hooks modules are turned off here | A setting blocks mods: your own disableAllHooks, or organisation policy |
hooks modules are turned off in this process | Anthropic has switched installed mods off remotely; nothing local re-enables them |
An organisation's allowManagedModsOnly is not reported here. If it applies, installing a mod produces a guard message instead.
Nothing from the mod appears
No command, no drawing, no behaviour change.
| Check | Cause and fix |
|---|---|
| Version | Mods need v2.1.287+ in the terminal, v2.1.286+ inside the Desktop app. See /docs/plugins/mods/overview |
/plugin shows no mods active line naming it | The module did not load. Look in the debug log for hooks module <name> not loaded: and read the refusal reason |
claude -p prints hooks module not loaded on stderr | Same: a refusal. Read the reason after the colon |
validate passes but prints no hooks line | hooks/hooks.json has no modules key, or it is misspelled. Add "modules": ["./register.js"] |
<name>: hooks module did not load: <reason> | Your module could not load, for example top-level code threw. The reason includes file and line |
hooks module did not load: options do not fit plugin.json userConfig: | A userConfig value fails validation (say, above max) or a required one is missing. The line names the pluginConfigs entry in settings.json to fix |
| First time in this folder | The workspace trust prompt was never accepted. Run claude there interactively and accept it |
| No installed plugin loads at all | You started with --safe-mode |
Remember that some settings stop mod code but leave the rest of the plugin (skills, commands, MCP servers) working, so a half-working plugin is a clue. /docs/plugins/mods/overview lists them.
Refusal reasons
These follow hooks module <name> not loaded: in the debug log. A --plugin-dir mod appears as <name>@inline.
| Reason starts with | Meaning |
|---|---|
hooks modules are turned off for installed plugins in this process | Remote switch-off by Anthropic; nothing local helps |
disableAllHooks in managed settings | Your organisation turned off hooks from installed plugins |
only managed plugins and built-in plugins run | allowManagedHooksOnly is set, or disableAllHooks is set somewhere other than managed settings |
installed plugins that are not managed load no hooks module in this mode (--bare) | You started with --bare |
another plugin of that name loads first | Name clash. The managed one, or whichever loaded first, wins. Rename yours |
Messages from the built-in guard
On machines with managed settings, or for users signed in on Team or Enterprise, the built-in guard can refuse a mod or one of its answers. Each message names the admin option that controls the rule.
| Message contains | Meaning | Appears in |
|---|---|---|
mods are limited to your organization's by policy (allowManagedModsOnly) | Only organisation mods may run; yours was refused | Debug log, and the transcript in hot-reload sessions |
tried to lift a deny rule in your settings | Your tool.check hook approved a call a deny rule refuses. It stays denied | Transcript and debug log, once per mod per session (debug log only under -p) |
the deny rules in your settings could not be checked for this call, so it is refused | The guard failed while checking a mod-approved call, so it refused | The reason Claude reads for the denied call |
See /docs/plugins/mods/admin for the admin side.
A hook is skipped or the mod is unloaded
The mod loaded, but something later went wrong.
| Message | Cause | Fix |
|---|---|---|
<mod>: <event> hook skipped: <reason>, e.g. edit-meter: tool.call hook skipped: threw Error: boom | The hook threw, exceeded its time limit, or returned the wrong shape. Shown once per event and failure kind until reload; the debug log has every occurrence | Fix the hook. Add a .catch for guards (see /docs/plugins/mods/events) |
<mod> registered /<cmd> but no command.run hook answered it | The command reached the end of the chain unanswered: no command.run hook, a matcher naming a different command, a hook that returned next(e), or a hook that was skipped (passing focus: false to $.ui.open is one way) | Look for a hook skipped line naming command.run. A test running the command fails with the same reason |
<mod> was unloaded: it crashed the hooks worker | Installed mods share one worker thread, and this mod was traced as the cause of a hang or crash, for example a loop that never awaits | Fix the blocking code |
hooks: mods that run in the hooks worker are off for this session: it crashed 3 times | The worker stopped three times without a single culprit, so every non-built-in mod (organisation mods included) was unloaded. Shown in every interactive session | Run /reload-plugins |
A tool call is unexpectedly denied
| Message | Cause | Fix |
|---|---|---|
a hook changed this call's input after the model wrote it (auto mode) | A mod's tool.call or turn.step hook, or a PreToolUse settings hook, changed the input after the server-side classifier reviewed it, so the review no longer covers it. The message does not say which | Claude is told to retry as recorded. If it is denied again, the hook rewrites every time: disable the mod or hook, or leave auto mode and approve manually. See /docs/permission-modes |
| Messages about deny rules in your settings | The built-in guard | See the guard table |
A drawing is missing or unresponsive
| Symptom | Cause | Fix |
|---|---|---|
| Pane or band empty, or shows Claude Code's normal content | Your tree failed validation. With --plugin-dir you get ui.render (Pane) refused: <reason>, e.g. Box prop "flexDirection" must be one of row, column, row-reverse, column-reverse; the engine drew its own. The debug log says a hook returned a tree that does not validate | Fix the named prop or element. Usual suspects: an unsupported prop, or an element the app lacks |
$.ui.open runs but no pane appears | Opened without user action in a terminal narrower than 144 columns (110 once the user has opened it) | Open from a command or button, and check isPlaced on the result. See /docs/plugins/mods/interface |
| Hotkeys do nothing | Pane lacks keyboard focus | Ctrl+X then Tab, click it, or open with focus: true from a command |
| Works in terminal, not Desktop | Site or element not available there (for example Raster) | Check the render sites and elements tables; branch on e.surface |
Edits or values go missing
| Symptom | Cause | Fix |
|---|---|---|
| Your code changes have no effect | You are editing source but running an installed copy, which is cached by version | Develop with claude --plugin-dir ./your-mod, which hot-reloads on save |
| A value resets on every save | Module-level variables reinitialise on reload | Keep it in $.state or $.store (/docs/plugins/mods/interface) |
A value resets after /clear, /resume or /branch, or a stored value is overwritten with a default | Those commands reset $.state, and session.start does not fire again | Reload from the store in a classic.SessionStart hook (how) |
The debug log
The debug log records every module loaded or refused, every hook failure and every refused result, so it is the place to look when the transcript is silent.
claude --debug-file ./mods.log --plugin-dir ./edit-meter
In a second terminal:
tail -f ./mods.log | grep edit-meter
A healthy load looks like this (note @inline for --plugin-dir mods):
hooks module edit-meter@inline loaded (worker, environment 2, tier user); events: session.start,tool.call,command.run,ui.render
Invalid drawings are logged as refused results too. To write your own lines into the log, pass a second argument to $.ui.log:
$.ui.log('pins loaded: ' + pins.length, { to: 'debug' })
Without it, $.ui.log writes a dim transcript line instead.
While editing a --plugin-dir mod, each reload adds a transcript line naming the mod and its hooks. If a save breaks the module you see reload failed, the previous version stays loaded: plus the reason, and the last good version keeps running, which is why "my change did nothing" is often really "my change failed to load".
Problems with installing or loading the plugin itself, rather than its mod code, are covered in /docs/plugins/troubleshooting.