Skip to main content
When a mod’s module or one of its hooks fails, Claude Code skips it and the session continues, so a broken mod can look like one that does nothing. Start by checking what Claude Code read from your mod and where it reports a problem, then find the symptom or message you have.

Find out why a mod does nothing

When a mod does nothing, check what Claude Code reads from the mod’s files, and the line it writes when it skips something. For the first, in your shell run claude plugin validate with the mod’s directory, as in claude plugin validate ./first-mod. It catches a misspelled event, a bad manifest, and a module Claude Code can’t read, without starting a session. When a module doesn’t load, a hook is skipped, or another mod refuses yours, Claude Code writes one line that names your mod. Where you read that line depends on the session:
  • A session that hot-reloads a plugin directory: a dim line in the transcript. That’s an interactive session you started with --plugin-dir, or one where you enabled hot reloading for mods Claude wrote.
  • Any other interactive session, such as one that runs a mod you installed from a marketplace: the debug log only. To get one, start the session with claude --debug.
  • A claude -p run with --plugin-dir: stderr, in the default text output format. A refusal by another mod goes to the debug log only.

Check whether mods can load

To check whether your setup lets mods load at all, without installing one, run claude plugin test in your shell, from a directory that doesn’t hold a mod. You don’t need a session. The message it prints tells you the state: An organization can also set allowManagedModsOnly to allow only its own mods, which this command doesn’t report. In that case Claude Code refuses a mod you install, and a message says why.

The mod doesn’t load

Nothing the mod adds appears: no command, no drawing, and no change in behavior.

Your version is too old

See which version to use and how to check yours.

The mods active line doesn’t name the mod

Nothing the mod adds appears, and the mods active line in /plugin doesn’t name it. The hooks module didn’t load. When Claude Code refused it, the debug log has a line that starts with hooks module, the mod’s name, and not loaded:, as in hooks module first-mod@inline not loaded: disableAllHooks in managed settings for a mod loaded with --plugin-dir. Read the reason after the colon. The refusal messages section lists each one. If the log has no such line, work through the other entries in this group. Some settings stop a mod and leave the rest of its plugin working. Turn mods on or off names them.

A claude -p run prints hooks module not loaded

The line starts with the mod’s name and goes to stderr. The hooks module was refused. A non-interactive run has no transcript, so the message goes to stderr. Read the reason after the colon. The refusal messages section lists each one.

Refusal messages

Each of these follows hooks module, the mod’s name, and not loaded: in the debug log.

Messages from the built-in guard

On a machine with managed settings, or in a session whose credential is a Team or Enterprise /login, the built-in guard can refuse a mod or one of its answers. Each message names the option your organization’s administrator sets to change the rule.

validate passes and lists no hooks line

hooks/hooks.json has no modules key, or the key is misspelled. Add "modules": ["./register.js"].

hooks module did not load

The line starts with the mod’s name, then hooks module did not load: and a reason, which gives the file and line when the problem is in your code. Claude Code couldn’t load the module, for example because its top-level code threw. Fix the error the reason names.

options do not fit plugin.json userConfig

The line starts with the mod’s name, then hooks module did not load: options do not fit plugin.json userConfig: and a reason. An option fails validation against its userConfig field, such as a number above the field’s max, or a required field has no value. Set or change the value. The end of the line names its pluginConfigs entry in settings.json.

code nested too deep to scan: more than 2000 scopes

The line starts with the mod’s name, then hooks module did not load:, the file, and code nested too deep to scan: more than 2000 scopes. A file in a hooks module can’t nest scopes, such as functions, blocks, and loops, more than 2,000 deep. claude plugin validate reports the same reason. Rewrite the code so its scopes nest less deeply.

No mod loads in a directory you opened for the first time

You haven’t answered the trust prompt for the directory. Start an interactive session in that directory with claude, and accept the trust prompt it opens with.

No installed plugin loads at all

You started Claude Code with --safe-mode. Start without the flag.

Claude Code stops asking to enable hot reloading

Claude writes a mod in an interactive session, nothing loads, and Claude Code doesn’t ask again whether to enable hot reloading. If the question ends three times without an answer picked, hot reloading stays off. For example, the question ends that way when you set askUserQuestionTimeout and the time passes before you answer. That setting applies here because Claude Code asks in the same question dialog that AskUserQuestion uses. A question you dismiss yourself doesn’t count toward the three. To run the mod, copy its directory out of the mods folder, then in your shell start a new session with --plugin-dir, as in claude --plugin-dir ~/mods/git-branch.

A hook is skipped or a mod is unloaded

The mod loaded, and then Claude Code skipped one of its hooks or unloaded it.

hook skipped

The line names the mod and the event, then says hook skipped: and a reason, as in first-mod: tool.call hook skipped: threw Error: boom. A hook threw, exceeded its time limit, or returned a result of the wrong shape. The line appears once for each event and kind of failure until the mod reloads. Fix the error. The debug log has a line for every occurrence.

no command.run hook answered it

You run a command your mod added, and the reply names the mod and the command, as in first-mod registered /tally but no command.run hook answered it, then tells you to add a hook. Claude Code prints that reply when the command reaches the end of the chain with no answer, which happens in two cases:
  • No hook answered the command: the module has no command.run hook, the hook’s filter names a different command, or the hook returned next(e)
  • Claude Code skipped the hook: hook skipped lists the reasons. Passing focus: false to $.ui.open is one way to get there.
If the module already has the hook the reply describes, look for a hook skipped line that names command.run, which gives the reason. A test that runs the command fails with the same reason.

it crashed the hooks worker

The line starts with the mod’s name, as in first-mod was unloaded: it crashed the hooks worker. Installed mods share one worker thread. The worker stopped responding or crashed, and Claude Code traced that to this mod and unloaded it. A hook that blocks the thread, such as a loop that never awaits, is one cause. Fix the hook.

its session.start ran again in a fresh copy

The line starts with the mod’s name and names a $.prompt.submit, $.command.run, or $.agent.spawn call, as in first-mod: its session.start ran again in a fresh copy; the $.prompt.submit call it had already made was not made again. Claude Code loaded the mod’s module again, for example after the hooks worker crashed and was replaced, and the fresh copy’s session.start hook ran. The call the line names resolved with the result of its first run instead of running again, so your mod doesn’t submit the prompt, run the command, or start the subagent twice. The rest of the hook ran as usual. There’s nothing to fix. Before v2.1.292, the call ran a second time, so the prompt was submitted, the command run, or the subagent started twice.

$.agent.register refused: the hooks module that made the call is no longer loaded

The line starts with your mod’s name, as in first-mod: $.agent.register refused: the hooks module that made the call is no longer loaded (it was reloaded or removed), and the agent isn’t registered. Your mod was reloaded or unloaded before the call. A reload loads a fresh copy of the hooks module, and this call came from code still running in the old copy, such as a hook that hadn’t returned yet. If that hook doesn’t catch the rejection, it fails and Claude Code skips it. To register the agent from the copy that stays loaded, make the call in your session.start hook, which runs again in each fresh copy after a reload.

mods that run in the hooks worker are off for this session

The line reads hooks: mods that run in the hooks worker are off for this session: it crashed 3 times. The worker stopped three times and Claude Code couldn’t trace the stops to one mod, so it unloaded every mod that isn’t built in, including mods your organization installs. This line reaches the transcript in every interactive session. Run /reload-plugins to load them again.

A tool call is denied

The mod loaded and its hooks run, and a tool call it touched is refused.

a hook changed this call's input after the model wrote it

In auto mode, a denied tool call gives this reason. A hook changed the tool call’s input after the server-side classifier reviewed it, so that review doesn’t cover what would run. The hook can be a mod’s tool.call or turn.step hook, or a PreToolUse settings hook. The message doesn’t say which. The message tells Claude to issue the call once more as recorded. If that’s denied too, the hook changes the input every time, so turn off the mod or hook, or leave auto mode and approve the call yourself.

A message about the deny rules in your settings

tried to lift a deny rule in your settings and the deny rules in your settings could not be checked for this call, so it is refused both come from the built-in guard. Look them up in Messages from the built-in guard.

A toast appears after a turn is stopped

Stopped: type a message to continue; a mod cannot resume work after Stop

This toast means that after you stopped the turn, for example with Esc, a mod tried to start work on its own and Claude Code didn’t let it. Nothing needs fixing. After your next message, mods can start work again and some of that work comes back on its own. If what you expected from the mod doesn’t come back, ask for it again or run the mod’s command again. If you wrote the mod, see Calls and background work after the user interrupts a turn.

A drawing doesn’t appear or respond

The mod loaded, and its pane, band, toast, or controls don’t behave as you expect.

Another mod’s band shows and yours doesn’t

There is one band above the prompt, and every mod shares it. A mod that runs before yours replaces your band when it returns a tree without the result of next(e) in it. To show both bands, change that mod’s hook, or ask its author to:
  • Keep the bands after it: put the result of await next(e) among the children of a Box in its tree, as in the Band above the prompt tab of Pick where to draw
  • Keep the tree valid: don’t give that Box, or any Box around it, a width, height, minWidth, minHeight, position, top, left, right, bottom, display, or overflow prop
To find that mod, write a debug log and search it for answered ui.render without next(). A line such as other-mod (user) answered ui.render without next() names a mod that returned its tree without calling next(e). It doesn’t name the render site, so a pane-only mod can appear too. A mod that calls next(e) and drops the result gets no line, so check the hooks before yours too. After the change, you see nothing in the band if any of the mods returns a tree that doesn’t validate. To read the reason, look for a ui.render (AbovePrompt) refused: line in the transcript of a --plugin-dir session, or search the debug log for does not validate. With several mods in the band, neither line names a mod, so check each mod’s tree.

A pane or band is empty or shows Claude Code’s usual content

The tree your hook returned didn’t validate. With --plugin-dir, the transcript says ui.render (Pane) refused: with the reason, as in first-mod: ui.render (Pane) refused: Box prop "flexDirection" must be one of row, column, row-reverse, column-reverse; the engine drew its own. The debug log has a hook returned a tree that does not validate with the same reason. Read the reason on that line. Common causes are a prop the element doesn’t take and an element the app doesn’t have.

A ui.render line says threw while drawn

The line names the render site, then says threw while drawn: and the error, as in first-mod: ui.render (ToolUse) threw while drawn: <error>; the engine drew its own. Claude Code hit that error while drawing the tree your ui.render hook returned, or while drawing the site from the props your hook passed to next. The ending the engine drew its own means the site shows Claude Code’s usual content. Read the error and fix the value in your hook that caused it. Before v2.1.289, this error in a transcript row ended the session with Claude Code exited after an unrecoverable interface error.

the module failed without a message

A Client failed with an error that has no message, such as throw new Error(). The line in its place reads like my-mod: Client client/spinner.js: the module failed without a message. Find the throw in your Client’s code and give the error a message. The line then shows that message. Before v2.1.289, the line showed Error as the reason instead.

$.ui.open runs and no pane appears

The call didn’t come from something the user did, and the terminal is narrower than the width that pane needs. Open the pane from a command or a button, or check the call’s isPlaced result. See Open a pane at the right time.

A toast doesn’t appear

Your mod calls $.ui.toast in an interactive terminal session and you don’t see the toast. To confirm that the call ran, look in the debug log for a line with your mod’s name and the toast’s text, as in $.ui.toast (first-mod): build finished. Then check for causes such as these:
  • The line for the call is missing: look for one that says why Claude Code refused the call, as in first-mod: $.ui.toast dropped: timeoutMs is a whole number of ms, 1 to 60000.
  • A pane is holding toasts: your mod or another one passed holdToasts when it opened the pane that’s showing. Close the pane to end the hold. If the pane is yours and is meant to stay open, remove holdToasts from its $.ui.open call and open the pane again.
  • The toast is under the prompt: in the classic renderer, look at the right under the prompt. A toast there is one line that starts with the mod’s name, rather than a box at the top right.
  • Your mod raised a newer toast: in the classic renderer, a newer toast from your mod can take the place of one that’s showing or waiting to show. The debug log has another line for the older toast, which ends with gave way, cut short when it was showing, or gave way, unseen when it never appeared. To show both messages, put them in one toast.
  • The toast ran out of time undrawn: in fullscreen rendering, Claude Code draws at most three toasts at a time, so a toast can run out of time before it’s drawn. The debug log has another line for that toast, which ends with left the stack, never drawn. When your mod raises several at once, put the messages in one toast.
Before v2.1.290, Claude Code dropped a toast raised within two seconds of the last one it showed for your mod, and the debug log line for the dropped toast said within 2000ms of the last; dropped.

Hotkeys do nothing

Your pane doesn’t have keyboard focus, or the drawing is in the VS Code extension, where buttons have no hotkeys. Do the one that matches:
  • No keyboard focus: press Ctrl+X then Tab, or click the pane. To have the pane take focus when it opens, open it with focus: true from a command.
  • In the VS Code extension: click the button, or press Enter on it once it has focus

A drawing works in the terminal and not in the Desktop app or VS Code

Either that app doesn’t draw the site or element, or the session is one where the VS Code extension draws nothing for mods. The mod’s other hooks still run. Check for these causes:
  • The site or element isn’t drawn in that app: look up your site in the render sites table and your elements in the elements table. For an element the app lacks, check e.surface in the hook and return a tree the app can draw.
  • The VS Code extension is older than 2.1.299: the chat panel draws for mods from extension version 2.1.299. Check the version in VS Code’s Extensions view and update the extension there.
  • The extension is older than 2.1.304 and the session doesn’t fetch feature flags: update the extension, or run claude in VS Code’s integrated terminal instead. Sessions on Amazon Bedrock, Google Cloud’s Agent Platform, or Microsoft Foundry skip the fetch. For the others that skip it, such as a Claude apps gateway session, see Features that need feature-flag fetching.

An edit or a value is lost

The mod runs, and a change you made or a value it kept isn’t there.

Your edits don’t take effect

You’re editing a plugin you installed. Claude Code runs the cached copy for the installed version. Develop with --plugin-dir pointed at your working copy, as in claude --plugin-dir ./first-mod, which reloads when you save.

A value resets when the module reloads

Module-level variables are re-initialized on each reload. Keep the value in $.state or $.store.

A value resets after /clear, /resume, or /branch

A value resets, or a saved value is replaced by its default. Each of those commands resets $.state to its defaults, and session.start doesn’t fire again. Load the saved value again in a classic.SessionStart hook.

Read the debug log

The debug log has a line for every module Claude Code loads or refuses, every hook that fails, and every result it refuses, so it’s where to look when the transcript shows nothing. To write one, in your shell start Claude Code with --debug, or with --debug-file <path> to choose where it goes:
In another terminal, follow the file and filter for your mod’s name:
A mod that loaded has a line that names it and lists the events it handles. A mod loaded with --plugin-dir appears under its name followed by @inline:
A drawing that didn’t validate counts as a refused result and gets a line too. To write your own lines in the log, call $.ui.log with a second argument, as in $.ui.log('message', { to: 'debug' }). Without the second argument, $.ui.log adds a dim line to the transcript. While you edit a mod loaded with --plugin-dir, the transcript shows a line for each reload that names the mod and lists its hooks. If a save breaks the module, the line says reload failed, the previous version stays loaded: with the reason, and the last working version keeps running until Claude Code next reloads plugins, such as when you run /reload-plugins.

Next steps

  • Test a mod: catch problems before they reach a session
  • Troubleshoot plugins: problems with installing and loading a plugin that aren’t specific to mods