{"slug": "claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look", "title": "Claude Code mod not loading or rendering with no error? Where to look", "summary": "A developer documented the three failure modes that cause Claude Code mods to silently fail without errors: static analysis rejection that prevents the whole plugin from loading, hooks skipped for using unavailable runtime APIs like process or setTimeout, and component rule violations that make Claude Code render its default UI. The writeup recommends running `claude plugin validate` and searching the debug log for the mod's name, and notes that the desktop app bundles an older Claude Code (2.1.286) than the terminal, so events such as ui.fault require v2.1.289 or later.", "body_md": "When a mod does nothing and shows no error, the cause almost always falls into one of three groups: **(1) it failed static analysis and the whole mod was not loaded, (2) it used something the hook runtime does not have and the hook was skipped, or (3) the tree it returned broke the component rules, so Claude Code drew its own UI instead**. Start with `claude plugin validate`, then search the debug log for the mod's name.\n\nThe official troubleshooting page opens with this: when a mod fails to load or a hook fails, **Claude Code skips it and keeps the session going**. A broken mod looks the same as a mod that does nothing.\n\nThat design keeps Claude Code itself from going down, but it leaves the author with few clues. So decide in advance where to look.\n\n| Symptom | Where to look | Common causes | \n|---|---|---|\n| No command, no band, nothing | 1. Static analysis | Put `$` in a variable, passed`$` to a function in another file, dynamic import, an event not in this version | \n| Loaded, but some hooks do not run | 2. Runtime | Used `process` or`setTimeout` , ran over 10 seconds | \n| The band or panel shows the default UI | 3. Component rules | Passed `undefined` or`false` , missing a required field, a component not available on that surface | \n\n`claude plugin validate`\n\n```\nclaude plugin validate .\\my-mod\n```\n\nWithout starting a session, this runs the same static analysis Claude Code runs at load time. It catches misspelled events, broken manifests, and unreadable modules.\n\n`hooks:` line appears: `hooks/hooks.json` has no `modules`, or it is misspelled\nAt load time, the module source is analyzed, and if it fails, **the whole plugin** is not loaded.\n\n| Code | Fix | \n|---|---|\n| `const s = $.store; s.get(k)` | Write `$.store.get(k)` in full | \n| `const { store } = $` | Do not destructure | \n| `helper($)` (where`helper` is in another file) | You can pass `$` only to top-level functions in the same file | \n| Passing `$` to a function created inside a hook | Move the function to the top level | \n| `on(eventName, ...)` (name in a variable) | Write the event name as a string literal | \n| Declaring another variable named `on` inside`register` | Rename it | \n| `await import('./x.mjs')` | Dynamic import is not allowed. Use a top-level `import` statement | \n| Importing a file outside the plugin | Copy it into the plugin. The only outside import allowed is `claude-code` | \n| `require(...)` | Use ES module `import` | \n\nThree things are easy to miss:\n\n`on()` calls in files you import.`AbovePrompt`, register once and dispatch inside\nThat last one hits the desktop app in particular. The desktop app bundles its own Claude Code, which can be older than your terminal version. I had a mod that registered `ui.fault` (the event for `Client` component failures). It worked in the terminal but vanished entirely on the desktop (bundled 2.1.286). The official reference now says `ui.fault` requires **v2.1.289 or later**. When you use a new event, compare the version note in the reference with the version shown by `/status` on the desktop.\n\nHooks run in an environment that is neither Node nor a browser. The official API page also says there are no Node.js APIs and no timers like `setTimeout`.\n\n`process`, `setTimeout`, `setInterval`, `Buffer`, the DOM, `require`\n`URL`, `TextEncoder`/` TextDecoder`, `AbortController`, `crypto` (including `crypto.subtle`), `atob`/` btoa`, `structuredClone`, `performance`\n| What you want | Use instead | \n|---|---|\n| Wait | `$.clock.sleep(ms)` | \n| Run once later | `$.clock.after(ms, fn)` | \n| Run periodically | `$.clock.every(ms, fn)` (create it in`session.start` ) | \n| Current time | `await $.clock.now()` | \n| Files, network, processes | `$.fs` ,`$.http` ,`$.process` | \n\nIn your own code, you just rewrite these. The trouble comes when you copy in an npm package: **it reads `process.env` the moment it loads, throws, and the whole mod silently disappears**. Putting local stand-ins at the top of that package's file, scoped to that file only, got it through.\n\n``` js\nconst process = { env: {} }\nconst setTimeout = () => 0\n```\n\nThe other issue is time. These are the limits from the official reference that people hit most often.\n\n| Thing | Limit | \n|---|---|\n| A hook's own run time per call (waiting inside `next` and APIs does not count;`$.clock.sleep` does) | 10 seconds | \n| A `.catch` handler | 1 second | \n| All `session.end` hooks | 1.5 seconds combined (configurable) | \n| `$.process.run` | 30 seconds by default, 10 minutes max | \n\n**Hooks that run longer than 10 seconds are skipped.** If you do heavy work in a guard that blocks tool calls (`tool.check`), the guard itself gets bypassed, so be careful.\n\nIf the tree returned from `ui.render` breaks the rules, **Claude Code draws its own UI without any error**. Your band just disappears and the default comes back, which makes this the hardest one to notice.\n\n| Code | Result | \n|---|---|\n| Passing a field whose value is `undefined` | May be treated as invalid | \n| `autoFocus: false` ,`focus: false` | Throws. Pass only `true` , or omit it | \n| `plain: false` on a`Button` | Cannot be drawn on the desktop. Use `true` or omit it | \n| `Svg` without`source` or`alt` | Not drawn (both are required) | \n| `undefined` in`Client` props | Invalid (JSON values only) | \n| A component not available on that surface ( `Raster` on the desktop, etc.) | Invalid | \n| Props a component does not accept (including typos) | Invalid | \n\nConditional fields naturally let `undefined` slip in, so I wrap the component factories in one place and strip those values.\n\n```\n// Do not pass fields that have no value to components\nfunction strip(raw) {\n  const out = {}\n  for (const k of Object.keys(raw)) {\n    const make = raw[k]\n    out[k] = typeof make === 'function'\n      ? (props = {}) => make(Object.fromEntries(Object.entries(props).filter(([, v]) => v !== undefined)))\n      : make\n  }\n  return out\n}\n\nconst t = strip($.ui.resolve(e))\nt.Button({ key: 'pdf', label: 'PDF', plain: true, dimColor: busy ? true : undefined, onPress })\n```\n\nWriting `dimColor: busy ? true : undefined` avoids ever passing `false`.\n\nWhen a module fails to load, a hook is skipped, or a tree is rejected, Claude Code writes **one line containing the mod's name**. Where it goes depends on the session (official troubleshooting page).\n\n| Session | Where the line appears | \n|---|---|\n| Interactive session loaded with `--plugin-dir` | A faint line in the conversation | \n| Normal session with a mod installed from a marketplace | Debug log only | \n| `claude -p` with`--plugin-dir` | Standard error | \n\nIn other words, **when an installed mod is silent, no amount of looking at the conversation will show anything**. Capture a debug log.\n\n```\nclaude --debug-file .\\mod-debug.log --plugin-dir .\\my-mod\n```\n\nWatch the log in another window (PowerShell example).\n\n```\nGet-Content .\\mod-debug.log -Wait | Select-String my-mod\n```\n\nThese are the strings to look for:\n\n| Log text | Meaning | \n|---|---|\n| `hooks module my-mod@inline loaded ... events: ...` | Loaded, with the list of registered events | \n| `hooks module my-mod ... not loaded:` | Rejected by settings or policy. The reason follows the colon | \n| `hooks module did not load:` | Top-level code threw. File and line are shown | \n| `hook skipped:` | A hook threw, timed out, or returned the wrong shape | \n| `ui.render (Pane) refused:` /`a hook returned a tree that does not validate` | The tree broke the component rules. Reason included | \n| `it crashed the hooks worker` | The mod was removed, for example because of a loop that never yields | \n\nA line like `ui.render (Pane) refused: Text prop \"bogusProp\" is not allowed; the engine drew its own` tells you exactly which prop on which component is wrong. Problems in group 3, component rules, are almost always solved in one look at this line.\n\nTo write your own markers, use `$.ui.log('got here', { to: 'debug' })` to write to the log.\n\nThe desktop app cannot take launch flags, so the approach above does not work as is. How to capture a debug log from the desktop app is not described on the official pages, so it is **not verified**. My routine is to load the same mod in the terminal with `--debug-file`, find the reason line, fix it, and then check in the desktop app. However, things that fail **only on the desktop**, like an event missing from that version, do not reproduce in the terminal, so check the desktop version with `/status` first.\n\nBefore blaming your mod, mods may be turned off by configuration. Run `claude plugin test` in a folder with no mod to see the state.\n\n| Output | Meaning | \n|---|---|\n| `no hooks module to load` | Mods can load (there is just nothing to test in this folder) | \n| `hooks modules are turned off here` | Turned off by `disableAllHooks` or an organization policy | \n| `hooks modules are turned off in this process` | Turned off remotely by Anthropic | \n\nOther causes of \"nothing loads at all\" include not having answered the trust prompt for a newly opened folder, and starting with `--safe-mode` or `--bare`.\n\n`undefined`, `false`, required fields, and components not available on that surface\nTo see how other mods are written so they pass static analysis, read them in the [modscode reviewed list](https://modscode.com/gallery/). Every mod listed there is code that `claude plugin validate` can read.\n\n*This article was written with AI assistance (Claude) and checked against the official Claude Code docs; anything marked \"not verified\" could not be confirmed there.*", "url": "https://wpnews.pro/news/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look", "canonical_source": "https://dev.to/nakadadev/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look-3bfg", "published_at": "2026-10-09 14:14:25+00:00", "updated_at": "2026-10-09 14:21:28.590834+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Claude Code", "Anthropic"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look", "markdown": "https://wpnews.pro/news/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look.md", "text": "https://wpnews.pro/news/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look.txt", "jsonld": "https://wpnews.pro/news/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look.jsonld"}}