<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Haruki Nakada</title>
    <description>The latest articles on DEV Community by Haruki Nakada (@nakadadev).</description>
    <link>https://dev.to/nakadadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4166552%2Fc9081751-4dd5-4837-879e-f69bdf970ead.jpg</url>
      <title>DEV Community: Haruki Nakada</title>
      <link>https://dev.to/nakadadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nakadadev"/>
    <language>en</language>
    <item>
      <title>Claude Code mods in the desktop app: a one-page summary and what to recheck after updates</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:09:37 +0000</pubDate>
      <link>https://dev.to/nakadadev/claude-code-mods-in-the-desktop-app-a-one-page-summary-and-what-to-recheck-after-updates-23bo</link>
      <guid>https://dev.to/nakadadev/claude-code-mods-in-the-desktop-app-a-one-page-summary-and-what-to-recheck-after-updates-23bo</guid>
      <description>&lt;p&gt;This series covered the minimum steps for using Claude Code mods in the desktop app's Code tab, and answers to the problems people search for most. Here is the whole thing on one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything on one page
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What are mods?&lt;/td&gt;
&lt;td&gt;JS/TS functions that run inside Claude Code. They draw panes and bands and can intercept tool calls&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where do they render?&lt;/td&gt;
&lt;td&gt;Only in the terminal and the desktop Code tab. Desktop from 2.1.286&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How to build one&lt;/td&gt;
&lt;td&gt;Three files: &lt;code&gt;plugin.json&lt;/code&gt;, &lt;code&gt;hooks.json&lt;/code&gt;, &lt;code&gt;register.tsx&lt;/code&gt;. Branch on &lt;code&gt;e.surface&lt;/code&gt; per surface&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Panes&lt;/td&gt;
&lt;td&gt;Open with &lt;code&gt;$.ui.open({ id })&lt;/code&gt;, draw in the &lt;code&gt;ui.render&lt;/code&gt; &lt;code&gt;Pane&lt;/code&gt; while checking &lt;code&gt;e.requestId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bands, status line, toasts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AbovePrompt&lt;/code&gt;, &lt;code&gt;$.ui.status&lt;/code&gt;, &lt;code&gt;$.ui.toast&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing renders&lt;/td&gt;
&lt;td&gt;Static check, runtime environment, component rules. Search the debug log for the mod's name&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken on desktop&lt;/td&gt;
&lt;td&gt;Auto-open from 144 columns, colors by theme name, Svg needs width and height, don't set &lt;code&gt;isInteractive&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doesn't work on Windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.process&lt;/code&gt; launches programs Windows doesn't have. Replace with the API. Sound via SoundPlayer&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asking from a button does nothing&lt;/td&gt;
&lt;td&gt;The press only queues; the clock sends&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributing and installing&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;marketplace.json&lt;/code&gt; and push. &lt;code&gt;/plugin install name@marketplace-name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is it safe?&lt;/td&gt;
&lt;td&gt;No sandbox. Read &lt;code&gt;validate&lt;/code&gt; output and the four APIs, pin with &lt;code&gt;sha&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When the version goes up
&lt;/h2&gt;

&lt;p&gt;Mods are a brand-new feature, and each version adds events, APIs and components. The behavior in this series was checked on 2.1.286 to 2.1.288. When the version goes up, recheck in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;/status&lt;/code&gt; in the desktop app to see the bundled Claude Code version&lt;/li&gt;
&lt;li&gt;Read the type definitions written out when loading with &lt;code&gt;--plugin-dir&lt;/code&gt; (&lt;code&gt;.claude-plugin/types/&lt;/code&gt;). They're more accurate than the web pages&lt;/li&gt;
&lt;li&gt;The workarounds (Windows sound, sending &lt;code&gt;$.prompt.submit&lt;/code&gt; from a clock) become unnecessary once the core fixes them. Try the straightforward way first&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  See real mods
&lt;/h2&gt;

&lt;p&gt;To see real mods, &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;the modscode reviewed gallery&lt;/a&gt; lists mods written for the desktop app that run on Windows and passed the review, each with a UI preview and code. To search them from inside a conversation with Claude, connect &lt;a href="https://modscode.com/connector/" rel="noopener noreferrer"&gt;the modscode MCP connector&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>javascript</category>
      <category>programming</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Is a Claude Code mod safe? How to check an unsandboxed mod before installing it</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:09:45 +0000</pubDate>
      <link>https://dev.to/nakadadev/is-a-claude-code-mod-safe-how-to-check-an-unsandboxed-mod-before-installing-it-1ja7</link>
      <guid>https://dev.to/nakadadev/is-a-claude-code-mod-safe-how-to-check-an-unsandboxed-mod-before-installing-it-1ja7</guid>
      <description>&lt;p&gt;A Claude Code mod is &lt;strong&gt;code that runs without a sandbox, with your permissions&lt;/strong&gt;. It can read and write files, read environment variables (including API keys), launch programs and use the network. Before installing, list the APIs the mod uses with &lt;code&gt;claude plugin validate&lt;/code&gt;, check in the code why it uses &lt;code&gt;$.http&lt;/code&gt;, &lt;code&gt;$.env&lt;/code&gt;, &lt;code&gt;$.process&lt;/code&gt; and &lt;code&gt;$.fs&lt;/code&gt;, and install it pinned to the &lt;code&gt;sha&lt;/code&gt; of the commit you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a mod can reach
&lt;/h2&gt;

&lt;p&gt;The "decide whether to trust a mod" section of the &lt;a href="https://code.claude.com/docs/en/plugins/mods/overview" rel="noopener noreferrer"&gt;official overview&lt;/a&gt; lists what an installed mod can do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs on your PC as you&lt;/td&gt;
&lt;td&gt;Reads and writes files anywhere your user can, launches programs, uses the network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads secrets&lt;/td&gt;
&lt;td&gt;Environment variables and settings files, including any API keys stored there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sees the session&lt;/td&gt;
&lt;td&gt;Every prompt you send and every tool call Claude makes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changes the session&lt;/td&gt;
&lt;td&gt;Rewrites prompts or tool calls, sends prompts as if typed by a person, messages other sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acts without asking&lt;/td&gt;
&lt;td&gt;Approves tool calls before you're asked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses your quota&lt;/td&gt;
&lt;td&gt;Calls the model with your plan or API key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And &lt;strong&gt;mods are not sandboxed&lt;/strong&gt;. Even with Claude Code's sandbox turned on, only the Bash commands Claude runs are isolated; processes started by a mod run outside it.&lt;/p&gt;

&lt;p&gt;One thing is protected: &lt;strong&gt;a mod can't redraw the permission prompt&lt;/strong&gt;. It's built so a mod can't disguise what the confirmation screen shows. However, a mod can answer "allow" before the prompt is ever shown (the "acts without asking" row above).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reading the code is enough
&lt;/h2&gt;

&lt;p&gt;For a mod's code to touch files, the network or processes, it &lt;strong&gt;must go through the &lt;code&gt;$&lt;/code&gt; API&lt;/strong&gt;. Inside a hook there's no &lt;code&gt;fetch&lt;/code&gt;, no &lt;code&gt;process&lt;/code&gt; and no &lt;code&gt;require&lt;/code&gt; (part 5). And at load time, Claude Code statically checks the source and only allows &lt;code&gt;$&lt;/code&gt; calls written out in full.&lt;/p&gt;

&lt;p&gt;So if you search for &lt;code&gt;$.http&lt;/code&gt;, &lt;code&gt;$.process&lt;/code&gt;, &lt;code&gt;$.fs&lt;/code&gt; and &lt;code&gt;$.env&lt;/code&gt;, you'll find every place the mod touches the outside. The official docs also say this is why Claude Code can list "what it does" before installing a mod.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. List it with &lt;code&gt;claude plugin validate&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Clone the mod's repo and run the check from a shell. &lt;strong&gt;It doesn't launch anything, so the mod's code doesn't run during validation.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/someone/some-mod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\some-mod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at these lines in the output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which events it intercepts (&lt;code&gt;tool.call&lt;/code&gt;, &lt;code&gt;tool.check&lt;/code&gt;, &lt;code&gt;prompt.submit&lt;/code&gt; and so on)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calls:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which APIs it calls (&lt;code&gt;$.http.fetch&lt;/code&gt;, &lt;code&gt;$.process.run&lt;/code&gt; and so on)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;env reads:&lt;/code&gt;, &lt;code&gt;env writes:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Which environment variables it reads or writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;state reads:&lt;/code&gt;, &lt;code&gt;state writes:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.state&lt;/code&gt; values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If something looks off, read that line in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check the reason for four APIs
&lt;/h2&gt;

&lt;p&gt;Most mods are one or two files of a few hundred lines. Even if you can't read all of it, search for these four and confirm "why is this needed" for each, and you can spot almost anything dangerous.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Search for&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.http&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is the destination host written in the code? What is being sent?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which names does it read? Where does it pass the values?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.process&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is the program name written in the code? Does it launch a shell (&lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;powershell.exe&lt;/code&gt;, &lt;code&gt;cmd.exe&lt;/code&gt;) or &lt;code&gt;curl&lt;/code&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.fs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where does it read and write? Does it read keys or config under your home folder?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ones to be most wary of are &lt;strong&gt;mods that both read (&lt;code&gt;$.fs&lt;/code&gt;, &lt;code&gt;$.env&lt;/code&gt;) and send (&lt;code&gt;$.http&lt;/code&gt;)&lt;/strong&gt;, because that shape can ship what it read to the outside. That's fine if the reason is clear, but if a mod that only shows a clock in the band reads environment variables and talks to the network, don't install it.&lt;/p&gt;

&lt;p&gt;Other things worth checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Does it intercept &lt;code&gt;tool.check&lt;/code&gt; or &lt;code&gt;tool.call&lt;/code&gt; and return "allow"?&lt;/strong&gt; That lets tools through without confirmation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where does it call &lt;code&gt;$.prompt.submit&lt;/code&gt; from?&lt;/strong&gt; It can start turns without the person knowing (part 8)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it call &lt;code&gt;$.model&lt;/code&gt;?&lt;/strong&gt; That uses your quota&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is there code you can't read?&lt;/strong&gt; Minified one-liners, &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;new Function&lt;/code&gt;, base64 blobs, &lt;code&gt;import()&lt;/code&gt; with a computed path. If you can't read it, don't install it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Pin the version
&lt;/h2&gt;

&lt;p&gt;What you read is the contents of one commit. A marketplace that points to the author's latest &lt;strong&gt;changes every time the author pushes&lt;/strong&gt;. With auto-update on, code you haven't read comes in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-update is off by default for marketplaces you added yourself. Keep it off for unfamiliar authors&lt;/li&gt;
&lt;li&gt;To pin to the commit you read, make your own marketplace and put that commit in the &lt;code&gt;source&lt;/code&gt; &lt;code&gt;sha&lt;/code&gt; (part 9)&lt;/li&gt;
&lt;li&gt;When updating, read the diff from the version you read last, then bump the &lt;code&gt;sha&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Keep watching after install
&lt;/h2&gt;

&lt;p&gt;If an installed mod worries you, disable it in the Installed tab of &lt;code&gt;/plugin&lt;/code&gt;. To stop everything and see what happens, start with &lt;code&gt;--safe-mode&lt;/code&gt; (part 1).&lt;/p&gt;

&lt;p&gt;In an organization, admins can set policies such as "load only the organization's mods" (&lt;code&gt;allowManagedModsOnly&lt;/code&gt;). You can also write a mod that refuses to load other mods (the &lt;code&gt;plugin.register&lt;/code&gt; event). Details are on the official "Manage mods for your organization" page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 13 checks in the modscode review
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modscode.com/review/" rel="noopener noreferrer"&gt;The modscode review&lt;/a&gt; applies the checks above mechanically to 1,383 public mods. Of the 1,383 public mods on GitHub it read, 49 were written for the desktop app, work on Windows, and passed the review (as of 2026-10-06).&lt;/p&gt;

&lt;p&gt;There are 13 checks, grouped into 4 by idea:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Checks&lt;/th&gt;
&lt;th&gt;Related part&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Usable on desktop&lt;/td&gt;
&lt;td&gt;Written for the desktop app / installable from the desktop app (listed in &lt;code&gt;marketplace.json&lt;/code&gt;) / works on Windows&lt;/td&gt;
&lt;td&gt;Parts 6, 7, 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readable&lt;/td&gt;
&lt;td&gt;Entry points read (every module named by &lt;code&gt;hooks.json&lt;/code&gt;) / parseable / self-contained code (no external packages or &lt;code&gt;require&lt;/code&gt;) / readable (no minified code, &lt;code&gt;eval&lt;/code&gt;, or encoded blobs)&lt;/td&gt;
&lt;td&gt;End of step 2 above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outside access is visible&lt;/td&gt;
&lt;td&gt;Calls are visible (network and program launches written in place) / fixed destinations (the https host for fetch is in the code) / no shell&lt;/td&gt;
&lt;td&gt;Table in step 2 above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keeps secrets&lt;/td&gt;
&lt;td&gt;Doesn't send what it reads (doesn't both read and send) / no keys (no API keys or private keys in the source) / open license&lt;/td&gt;
&lt;td&gt;Step 2 above and the rules for copying code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In numbers, 167 don't work on Windows and 269 can't be installed from &lt;code&gt;marketplace.json&lt;/code&gt;. "Written for the desktop app" is especially strict; many mods written for the terminal drop out there.&lt;/p&gt;

&lt;p&gt;However, as the modscode pages themselves say, &lt;strong&gt;this review does not guarantee that any mod is safe&lt;/strong&gt;. It only checks whether the code is in a machine-readable shape and whether its outside access is visible. You still need to read before installing. Use the review as a pointer to where to start reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mods run without a sandbox, with your permissions&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;claude plugin validate&lt;/code&gt; to get &lt;code&gt;hooks:&lt;/code&gt;, &lt;code&gt;calls:&lt;/code&gt; and &lt;code&gt;env reads:&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;$.http&lt;/code&gt;, &lt;code&gt;$.env&lt;/code&gt;, &lt;code&gt;$.process&lt;/code&gt; and &lt;code&gt;$.fs&lt;/code&gt; and confirm the reasons. Be especially careful with mods that both read and send&lt;/li&gt;
&lt;li&gt;Pin to the &lt;code&gt;sha&lt;/code&gt; of the commit you read, keep auto-update off, and read the diff before bumping&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you need to explain what mods are to someone else, &lt;a href="https://modscode.com/guide/" rel="noopener noreferrer"&gt;What are mods&lt;/a&gt; is a short summary.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>How to distribute and install a Claude Code mod with marketplace.json and /plugin install</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:50:43 +0000</pubDate>
      <link>https://dev.to/nakadadev/how-to-distribute-and-install-a-claude-code-mod-with-marketplacejson-and-plugin-install-1gka</link>
      <guid>https://dev.to/nakadadev/how-to-distribute-and-install-a-claude-code-mod-with-marketplacejson-and-plugin-install-1gka</guid>
      <description>&lt;p&gt;A mod is a plugin, so it's distributed and installed the same way as any plugin. &lt;strong&gt;To distribute, put &lt;code&gt;.claude-plugin/marketplace.json&lt;/code&gt; in the repo and push.&lt;/strong&gt; To install, add the marketplace with &lt;code&gt;/plugin marketplace add owner/repo&lt;/code&gt;, then install with &lt;code&gt;/plugin install name@marketplace-name&lt;/code&gt;. In the desktop app you can also install from &lt;strong&gt;+ next to the prompt → Plugins → Add plugin&lt;/strong&gt; (official install page).&lt;/p&gt;

&lt;h2&gt;
  
  
  Terms
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;A single folder holding mods, skills, commands, MCP servers and so on. A mod is one kind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketplace&lt;/td&gt;
&lt;td&gt;A JSON file (&lt;code&gt;marketplace.json&lt;/code&gt;) listing plugins and where to fetch each one. Usually kept in a GitHub repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin id&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;name@marketplace-name&lt;/code&gt;. Used to install, remove and configure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Distributing: add marketplace.json
&lt;/h2&gt;

&lt;p&gt;The simplest setup is to make the mod's own repo the marketplace. Put &lt;code&gt;marketplace.json&lt;/code&gt; next to &lt;code&gt;plugin.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello-band/
├── .claude-plugin/
│   ├── plugin.json
│   └── marketplace.json
└── hooks/
    ├── hooks.json
    └── register.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A mod that shows one line above the prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-band"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt; and &lt;code&gt;plugins&lt;/code&gt; are required. &lt;code&gt;owner&lt;/code&gt; requires &lt;code&gt;name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Each entry in &lt;code&gt;plugins&lt;/code&gt; requires &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source: "./"&lt;/code&gt; points to the repo root (one level above &lt;code&gt;.claude-plugin/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Match the entry's &lt;code&gt;name&lt;/code&gt; to the &lt;code&gt;name&lt;/code&gt; in &lt;code&gt;plugin.json&lt;/code&gt; (the official docs say to)&lt;/li&gt;
&lt;li&gt;The marketplace name uses letters, digits, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt;, and starts with a letter or digit. Official names like &lt;code&gt;claude-plugins-official&lt;/code&gt;, names that look official, and names containing non-ASCII letters are rejected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validate before pushing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It's public the moment the files are pushed.&lt;/strong&gt; There's no application process. If the repo is private, it becomes a private marketplace that only people who can clone it can install from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing several mods in one repo
&lt;/h3&gt;

&lt;p&gt;As your mods grow, you can create one repo for the marketplace and line them up under &lt;code&gt;plugins/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-band"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./plugins/hello-band"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-pane"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./plugins/hello-pane"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To list a mod that lives in another repo, make &lt;code&gt;source&lt;/code&gt; an object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-band"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-name/hello-band"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pinning a version
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;github&lt;/code&gt; source can have &lt;code&gt;ref&lt;/code&gt; (a branch or tag) and &lt;code&gt;sha&lt;/code&gt; (a 40-character lowercase commit). &lt;strong&gt;With &lt;code&gt;sha&lt;/code&gt;, only that commit's contents are distributed.&lt;/strong&gt; Even if the author changes things later, users won't get them until the marketplace's &lt;code&gt;sha&lt;/code&gt; is updated.&lt;/p&gt;

&lt;p&gt;When you list someone else's mod in your marketplace, pinning to the &lt;code&gt;sha&lt;/code&gt; of the commit you read is the safe choice (part 10).&lt;/p&gt;

&lt;h3&gt;
  
  
  Bumping the version
&lt;/h3&gt;

&lt;p&gt;If you put a &lt;code&gt;version&lt;/code&gt; in &lt;code&gt;plugin.json&lt;/code&gt;, &lt;strong&gt;bump it every time you change something&lt;/strong&gt;. If you push without bumping, users' &lt;code&gt;claude plugin update&lt;/code&gt; says "already up to date" and they keep the old copy. For git marketplaces you can also omit &lt;code&gt;version&lt;/code&gt;, in which case the commit SHA becomes the version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing: with commands
&lt;/h2&gt;

&lt;p&gt;Type these inside a Claude Code session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add your-name/hello-band
/plugin install hello-band@hello-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;your-name/hello-band&lt;/code&gt; on the first line is the GitHub &lt;code&gt;owner/repo&lt;/code&gt;. What follows &lt;code&gt;@&lt;/code&gt; on the second line is &lt;strong&gt;the &lt;code&gt;name&lt;/code&gt; in &lt;code&gt;marketplace.json&lt;/code&gt;, not the repo name&lt;/strong&gt;. Mixing these up and getting "not found" is very common.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;#&lt;/code&gt; like &lt;code&gt;owner/repo#v1.2.0&lt;/code&gt; to pin to a branch or tag&lt;/li&gt;
&lt;li&gt;For non-GitHub hosts use an &lt;code&gt;https://...git&lt;/code&gt; URL; for a local folder use a path starting with &lt;code&gt;./&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both in one step: &lt;code&gt;/plugin install hello-band --marketplace your-name/hello-band&lt;/code&gt; (Claude Code 2.1.275 and later)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;/plugin install&lt;/code&gt; inside a session doesn't install immediately; it opens a details screen. There you review the contents (commands, hooks, MCP servers and so on) and pick a scope.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Applies to&lt;/th&gt;
&lt;th&gt;Settings file written&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;All your projects&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;Everyone working in this repo&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; (committed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Only you, only this repo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/settings.local.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can do the same from a shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;marketplace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;your-name/hello-band&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hello-band&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;hello-tools&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When installed from a shell, type &lt;code&gt;/reload-plugins&lt;/code&gt; in an open session to load it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing: from the desktop app UI
&lt;/h2&gt;

&lt;p&gt;The official install page describes these steps for the desktop app's Code tab (local or SSH sessions):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;+&lt;/strong&gt; next to the prompt and choose &lt;strong&gt;Plugins&lt;/strong&gt; → &lt;strong&gt;Add plugin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A list of marketplace plugins opens; pick the one to install&lt;/li&gt;
&lt;li&gt;Choose the scope (user, project or local)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To disable or remove later, use &lt;strong&gt;+ → Plugins → Manage plugins&lt;/strong&gt;. This screen isn't available in cloud sessions.&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;the steps to add a marketplace itself from the desktop app UI aren't on the official page, so that is not verified.&lt;/strong&gt; The list above shows only plugins from marketplaces you've already added. The terminal, local desktop sessions and VS Code read the same settings files, so if you run &lt;code&gt;claude plugin marketplace add&lt;/code&gt; in a shell, it should show up in the desktop list as well. Whether typing &lt;code&gt;/plugin marketplace add&lt;/code&gt; in the Code tab prompt works the same way on desktop is also &lt;strong&gt;not verified&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After installing, open a new session in the desktop app and check whether the band or pane appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;By the user: &lt;code&gt;claude plugin update hello-band@hello-tools&lt;/code&gt;, or &lt;strong&gt;Update now&lt;/strong&gt; in the Installed tab of &lt;code&gt;/plugin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auto-update: &lt;strong&gt;off by default&lt;/strong&gt; for marketplaces you added yourself. You can turn it on per marketplace in the Marketplaces tab of &lt;code&gt;/plugin&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With auto-update on, whatever the author pushes keeps coming in. Mods are code that runs without a sandbox, so for marketplaces from unfamiliar authors it's safer to leave it off and read before updating (part 10).&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't rename
&lt;/h2&gt;

&lt;p&gt;Don't change the &lt;code&gt;name&lt;/code&gt; of a published mod. Users' installs are recorded as &lt;code&gt;name@marketplace-name&lt;/code&gt;, so renaming makes the mod disappear for people who installed it. If you really must rename, map the old name to the new one with &lt;code&gt;renames&lt;/code&gt; in &lt;code&gt;marketplace.json&lt;/code&gt;. If you only want to change the display name, use &lt;code&gt;displayName&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distributing to Windows users
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;source&lt;/code&gt; that uses &lt;code&gt;command&lt;/code&gt; (a folder produced by a local command) with &lt;code&gt;mode: "link"&lt;/code&gt; &lt;strong&gt;can't be installed on Windows&lt;/strong&gt; (official marketplace reference). If you have Windows users, use &lt;code&gt;copy&lt;/code&gt;. With ordinary &lt;code&gt;github&lt;/code&gt; or &lt;code&gt;./&lt;/code&gt; sources you don't need to worry about this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding mods you can install
&lt;/h2&gt;

&lt;p&gt;Among public mods on GitHub, fewer than you'd expect are listed in their repo's own &lt;code&gt;marketplace.json&lt;/code&gt; and installable with the steps in this article. In the modscode review, 269 of 1,383 didn't meet this condition (as of 2026-10-06).&lt;/p&gt;

&lt;p&gt;All 49 mods in &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;the modscode reviewed gallery&lt;/a&gt; can be installed from their repo's &lt;code&gt;marketplace.json&lt;/code&gt;, and each page shows the install commands. If you want to search reviewed mods inside a conversation with Claude, or have Claude read a mod's UI structure and review results, you can also connect &lt;a href="https://modscode.com/connector/" rel="noopener noreferrer"&gt;the modscode MCP connector&lt;/a&gt; to Claude (read-only, no login).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>plugin</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Claude Code mod button calling $.prompt.submit doesn't work the first time (where to call it)</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:09:41 +0000</pubDate>
      <link>https://dev.to/nakadadev/claude-code-mod-button-calling-promptsubmit-doesnt-work-the-first-time-where-to-call-it-158i</link>
      <guid>https://dev.to/nakadadev/claude-code-mod-button-calling-promptsubmit-doesnt-work-the-first-time-where-to-call-it-158i</guid>
      <description>&lt;p&gt;If you call &lt;code&gt;$.prompt.submit&lt;/code&gt; inside a mod button's &lt;code&gt;onPress&lt;/code&gt; or inside a command, Claude sometimes doesn't start. In the versions I tested, &lt;strong&gt;a &lt;code&gt;$.prompt.submit&lt;/code&gt; called inside the press event was thrown away when the event ended, with no error&lt;/strong&gt;. Have the button only store what to send in a variable, and send it from a clock created in &lt;code&gt;session.start&lt;/code&gt;. That works reliably.&lt;/p&gt;

&lt;p&gt;Versions checked: Claude Code 2.1.286 to 2.1.288 (October 2026, desktop Code tab).&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom
&lt;/h2&gt;

&lt;p&gt;Put a button in a mod's panel, and when pressed, send a request to Claude. It's the first thing you want to build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ask&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Decide now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Check the market and decide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pressing it doesn't start Claude.&lt;/strong&gt; Sometimes pressing twice works, which made the cause slow to find. Sending from inside a command (&lt;code&gt;/xxx&lt;/code&gt;) behaved the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the official docs say
&lt;/h2&gt;

&lt;p&gt;On the official API page, &lt;code&gt;$.prompt.submit&lt;/code&gt; sits in the section about &lt;strong&gt;starting a turn from background work&lt;/strong&gt;. Three key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$.prompt.submit({ text })&lt;/code&gt; waits until the session is idle, then starts a new turn&lt;/li&gt;
&lt;li&gt;It resolves when the turn starts, so &lt;strong&gt;don't &lt;code&gt;await&lt;/code&gt; it inside a handler that runs while Claude is working&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Claude reads the text after a line saying your mod sent it. With &lt;code&gt;asUser: true&lt;/code&gt;, it's sent as the person's own words without that line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The example given also sends from inside a clock (&lt;code&gt;$.clock.every&lt;/code&gt;) created in &lt;code&gt;session.start&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I observed
&lt;/h2&gt;

&lt;p&gt;The official text goes as far as "it waits, so don't &lt;code&gt;await&lt;/code&gt; it". &lt;strong&gt;It does not say that a call inside an event gets thrown away.&lt;/strong&gt; What follows is what I saw in my versions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;$.prompt.submit&lt;/code&gt; called inside a button's &lt;code&gt;onPress&lt;/code&gt; or a command hook disappeared when the event ended, and no turn started&lt;/li&gt;
&lt;li&gt;No exception, and nothing reached &lt;code&gt;.catch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Moving the same call into a clock made it work every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why it gets dropped is &lt;strong&gt;not verified&lt;/strong&gt;. But together with the fact that the official example sends from a clock, I believe the right place to start a turn is a clock, not an event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix: the press only queues, the clock sends
&lt;/h2&gt;

&lt;p&gt;The button only puts "what to send" into a variable, and a 1-second clock finds it and sends it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;toSend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="c1"&gt;// Button: just queue it&lt;/span&gt;
&lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;toSend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Check the market and decide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create the clock once in session.start&lt;/span&gt;
&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;toSend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toSend&lt;/span&gt;
  &lt;span class="nx"&gt;toSend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;            &lt;span class="c1"&gt;// clear before sending (so the next tick doesn't send it twice)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's up to a 1-second delay between pressing and sending, but you barely notice it. Show "Sending..." in a toast or the panel right after the press so there's no doubt whether the press registered.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;frame&lt;/code&gt; takes &lt;code&gt;$&lt;/code&gt;, so put it &lt;strong&gt;at the top level of the same file&lt;/strong&gt;. If you pass &lt;code&gt;$&lt;/code&gt; to a function created inside a hook or to a function in another file, the static check fails and the whole mod doesn't load (part 5).&lt;/p&gt;

&lt;h2&gt;
  
  
  For the same reason, start external processes from the clock
&lt;/h2&gt;

&lt;p&gt;Work that starts an external process with &lt;code&gt;$.process.run&lt;/code&gt; (opening a file in the default app, playing a sound) was also sometimes &lt;strong&gt;killed when the event ended&lt;/strong&gt; if started inside the event. Route that to the same clock too. Processes started from the clock keep running beyond the event.&lt;/p&gt;

&lt;p&gt;Remember "any work that outlives a single event starts from the &lt;code&gt;session.start&lt;/code&gt; clock" and you won't get lost. The official docs also describe running work that spans more than one event on a clock started in &lt;code&gt;session.start&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep only one clock
&lt;/h2&gt;

&lt;p&gt;If you create a clock per feature, you hit a different trap. In my versions, writing settings (&lt;code&gt;$.config.set&lt;/code&gt;) reloaded the plugin and ran &lt;code&gt;register&lt;/code&gt; again, so &lt;strong&gt;clocks multiplied and the same work ran twice&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The official explanation says that on reload the previous clocks stop and the new load creates its own. It may be fixed in newer versions, but which version changed it is &lt;strong&gt;not verified&lt;/strong&gt;. To be safe either way, I follow two rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only one place in the mod owns the clock&lt;/li&gt;
&lt;li&gt;Stop the previous clock before creating a new one
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startClock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;?.()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* do nothing if there isn't one */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clocks returned by &lt;code&gt;$.clock.every&lt;/code&gt; and &lt;code&gt;$.clock.after&lt;/code&gt; have &lt;code&gt;cancel()&lt;/code&gt; (official).&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-rendering too fast recreates the button you're pressing
&lt;/h2&gt;

&lt;p&gt;There was one more cause of "I pressed it but nothing happened". If you re-render the panel every second, the button can be recreated mid-press.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only animated graphics every second&lt;/li&gt;
&lt;li&gt;The normal view every 5 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Splitting it like this fixed it. Calling &lt;code&gt;$.ui.invalidate('ui.render')&lt;/code&gt; only when a value changes cuts wasted re-renders in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to word what you send
&lt;/h2&gt;

&lt;p&gt;Text sent with &lt;code&gt;$.prompt.submit&lt;/code&gt; looks to Claude like "a request that came from a mod".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normally don't set &lt;code&gt;asUser&lt;/code&gt;. Claude reads it knowing it came from a mod&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;asUser: true&lt;/code&gt; only when you want it treated the same as something a person typed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mod that asks Claude something with one button can also start turns without the person knowing. From the installer's side, where a mod calls &lt;code&gt;$.prompt.submit&lt;/code&gt; from is one of the things worth checking (part 10).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Asking from a button or command doesn't start Claude&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.prompt.submit&lt;/code&gt; inside the event was dropped (in my versions)&lt;/td&gt;
&lt;td&gt;Queue in a variable, send from the &lt;code&gt;session.start&lt;/code&gt; clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opened apps or sounds get cut off&lt;/td&gt;
&lt;td&gt;Processes are killed when the event ends&lt;/td&gt;
&lt;td&gt;Start them from the clock as well&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same work runs twice&lt;/td&gt;
&lt;td&gt;Clocks are created in more than one place&lt;/td&gt;
&lt;td&gt;One clock, stop it before creating a new one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The press feels registered but nothing happens&lt;/td&gt;
&lt;td&gt;Re-rendering recreated the button&lt;/td&gt;
&lt;td&gt;Re-render every 5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can read real mods that ask Claude from a button, with their code, in &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;the modscode reviewed gallery&lt;/a&gt;. Comparing how they create clocks and place buttons shows the patterns clearly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>javascript</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Claude Code mod works in the terminal but breaks in the desktop app (pane not opening, Svg shrinking, flicker)</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:31:41 +0000</pubDate>
      <link>https://dev.to/nakadadev/claude-code-mod-works-in-the-terminal-but-breaks-in-the-desktop-app-pane-not-opening-svg-30ka</link>
      <guid>https://dev.to/nakadadev/claude-code-mod-works-in-the-terminal-but-breaks-in-the-desktop-app-pane-not-opening-svg-30ka</guid>
      <description>&lt;p&gt;When a mod that works in the terminal is opened in the desktop app, these five problems come up most: &lt;strong&gt;a pane opened at startup does not appear (opens without a user action need 144 columns), hex colors are unreadable in the light theme, &lt;code&gt;Raster&lt;/code&gt; is not drawn, &lt;code&gt;Svg&lt;/code&gt; shrinks to 300x150, and an &lt;code&gt;isInteractive&lt;/code&gt; Svg flashes black every second&lt;/strong&gt;. All of them can be fixed by branching on &lt;code&gt;e.surface&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tested with Claude Code 2.1.286 to 2.1.288 (October 2026, Windows, 150% scaling).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A pane opened at startup does not appear
&lt;/h2&gt;

&lt;p&gt;Even if you call &lt;code&gt;$.ui.open(...)&lt;/code&gt; in &lt;code&gt;session.start&lt;/code&gt;, the pane may not appear on the desktop. The official interface page has rules for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An open backed by a &lt;strong&gt;user action&lt;/strong&gt; (a command, a pressed button, a sent prompt) is placed regardless of width&lt;/li&gt;
&lt;li&gt;An open without one (from &lt;code&gt;session.start&lt;/code&gt;, a timer, or &lt;code&gt;turn.start&lt;/code&gt;) needs &lt;strong&gt;144 columns&lt;/strong&gt;. A pane the user has opened themselves at least once needs 110 columns&lt;/li&gt;
&lt;li&gt;When there is not enough room, &lt;code&gt;$.ui.open&lt;/code&gt; returns &lt;code&gt;{ isPlaced: false, reason }&lt;/code&gt; and waits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The desktop side panel is narrow, so assume an open at startup may not be placed. I chose to &lt;strong&gt;reopen on the first prompt submission&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;opened&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt.submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;opened&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;composer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bridge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;opened&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PANE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Mod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values of &lt;code&gt;e.origin.kind&lt;/code&gt; (&lt;code&gt;composer&lt;/code&gt;, &lt;code&gt;bridge&lt;/code&gt;) are what I saw in the type definitions for my version. On other versions, check the type definitions (&lt;code&gt;.claude-plugin/types/&lt;/code&gt; from Part 2).&lt;/p&gt;

&lt;p&gt;Other options are to open the pane from a command or a band button, or to show a toast saying it can be opened instead of opening it on your own (Part 4).&lt;/p&gt;

&lt;p&gt;Also, the desktop app I tested (2.19675) accepted &lt;code&gt;rows&lt;/code&gt; and &lt;code&gt;columns&lt;/code&gt; in &lt;code&gt;$.ui.open&lt;/code&gt; but did not use them for the pane size. Draw on the assumption that the size is whatever width the user dragged it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hex colors are unreadable in the light theme
&lt;/h2&gt;

&lt;p&gt;In the terminal you can paint with &lt;code&gt;color: '#7FD1A6'&lt;/code&gt;. On the desktop, hard-coded colors can stand out oddly or become unreadable in the light theme. Pass &lt;strong&gt;theme color names&lt;/strong&gt; instead and they render correctly in both light and dark.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Up, success&lt;/td&gt;
&lt;td&gt;&lt;code&gt;success&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Down, removal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;diffRemovedWord&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;&lt;code&gt;warning&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dim text&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dimColor: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official reference for &lt;code&gt;Text&lt;/code&gt; also says &lt;code&gt;color&lt;/code&gt; is a theme key or a color like &lt;code&gt;'red'&lt;/code&gt;. If you keep hex values in your feature code and put a single wrapper that swaps them only on the desktop, you do not have to rewrite anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;THEMED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#7fd1a6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f2837f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diffRemovedWord&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#d9b26a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;themeProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;THEMED&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THEMED&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. &lt;code&gt;Raster&lt;/code&gt; is not drawn, or the surface is detected wrong
&lt;/h2&gt;

&lt;p&gt;A mod that draws a grid of colored cells with &lt;code&gt;Raster&lt;/code&gt; in the terminal cannot draw it on the desktop. &lt;code&gt;Raster&lt;/code&gt; and &lt;code&gt;Image&lt;/code&gt; are terminal-only, and &lt;code&gt;Svg&lt;/code&gt; is desktop-only.&lt;/p&gt;

&lt;p&gt;If you try to detect the surface by checking whether a component exists, like &lt;code&gt;if (t.Raster)&lt;/code&gt;, you will get it wrong, because the table from &lt;code&gt;$.ui.resolve(e)&lt;/code&gt; contains every component name. &lt;strong&gt;Always branch on &lt;code&gt;e.surface&lt;/code&gt; (&lt;code&gt;'terminal'&lt;/code&gt; or &lt;code&gt;'desktop'&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;terminal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Raster&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;svgOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Heatmap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. &lt;code&gt;Svg&lt;/code&gt; shrinks to a small box
&lt;/h2&gt;

&lt;p&gt;If you do not pass &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; to &lt;code&gt;Svg&lt;/code&gt;, the frame stays at the default &lt;strong&gt;300x150&lt;/strong&gt; (CSS pixels), and the picture shrinks to fit inside it. This happens even if the SVG itself has a &lt;code&gt;viewBox&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Chart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch is that the pane size reaches the mod only in columns and rows (&lt;code&gt;bodyColumns&lt;/code&gt;, &lt;code&gt;bodyRows&lt;/code&gt;), not in CSS pixels. On my screen, one pane cell measured &lt;strong&gt;7.6 wide and 18.6 tall&lt;/strong&gt; (CSS pixels).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CELL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;18.6&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CELL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CELL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values may differ on other setups, so make the drawing robust to a wrong estimate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the root &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; &lt;code&gt;width="100%" height="100%"&lt;/code&gt; and a &lt;code&gt;viewBox&lt;/code&gt;, so it keeps its aspect ratio and fits even if the frame differs from your estimate&lt;/li&gt;
&lt;li&gt;Estimate a little small and fill the leftover space with the background color of the outer &lt;code&gt;Box&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;source&lt;/code&gt; is limited to &lt;strong&gt;131,072 characters&lt;/strong&gt; (official reference). 200 candlesticks already take tens of thousands of characters. Round coordinates to one decimal place and group identical fills into classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. An &lt;code&gt;isInteractive&lt;/code&gt; Svg flashes black every second
&lt;/h2&gt;

&lt;p&gt;An Svg with &lt;code&gt;isInteractive: true&lt;/code&gt; is drawn inside a frame (an iframe), so &lt;code&gt;:hover&lt;/code&gt; and CSS animations work. However, &lt;strong&gt;every time the content changes, the whole frame reloads and goes black while it does&lt;/strong&gt;. A view redrawn every second flashes every second. I also tried stacking two frames and loading in the back one, but that did not fix it.&lt;/p&gt;

&lt;p&gt;The conclusion: &lt;strong&gt;do not use &lt;code&gt;isInteractive&lt;/code&gt;; use a single image Svg&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An image Svg keeps showing the old picture until the new one is ready, so it does not flash&lt;/li&gt;
&lt;li&gt;CSS &lt;code&gt;@keyframes&lt;/code&gt; written inside the SVG still run in an image Svg&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you lose is roughly &lt;code&gt;:hover&lt;/code&gt; and &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tooltips.&lt;/p&gt;

&lt;h3&gt;
  
  
  Animations restart from the beginning on every redraw
&lt;/h3&gt;

&lt;p&gt;Even with an image Svg, changed content becomes a new image, and CSS animations start over. If you write the animation delay as &lt;strong&gt;"event time minus the time the SVG was built,"&lt;/strong&gt; the animation continues from the middle even after a rebuild, because a negative delay means "already partway through."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;madeAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${((&lt;/span&gt;&lt;span class="nx"&gt;eventAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;madeAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;

&lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;text class="type" style="animation-delay:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/text&amp;gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things that change every second (elapsed time and so on) should stay out of the SVG and go in a &lt;code&gt;Text&lt;/code&gt; row above it. That reduces how often the SVG itself is rebuilt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Colors go wrong when two are side by side
&lt;/h3&gt;

&lt;p&gt;If you reference &lt;code&gt;&amp;lt;linearGradient id="g"&amp;gt;&lt;/code&gt; with &lt;code&gt;url(#g)&lt;/code&gt;, the second of two side-by-side images points at the first one's gradient. Give each one a unique prefix (&lt;code&gt;id="s3-g"&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things you will hit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  You cannot style buttons, and clicks on top of the picture do not register
&lt;/h3&gt;

&lt;p&gt;The desktop &lt;code&gt;Button&lt;/code&gt; has a fixed look; you cannot set its color, shape, or size. HTML cannot go into a pane either. I tried drawing the whole screen as an Svg and layering a &lt;code&gt;Client&lt;/code&gt; on top to catch where the user clicked, but &lt;strong&gt;clicks on a &lt;code&gt;Client&lt;/code&gt; layered over the picture did not work&lt;/strong&gt; (2.1.286 to 2.1.288). The layout I settled on is: visuals in an &lt;code&gt;Svg&lt;/code&gt; picture, and standard &lt;code&gt;Button&lt;/code&gt;s placed above or below the picture for anything clickable. Writing &lt;code&gt;plain: false&lt;/code&gt; makes the button undrawable on the desktop, so only use &lt;code&gt;true&lt;/code&gt; or omit it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layouts break in the narrow pane
&lt;/h3&gt;

&lt;p&gt;The usual way a pane opens on the desktop is &lt;strong&gt;the narrow side pane next to the conversation (about 58 columns, 440px)&lt;/strong&gt;. Layouts tuned on a wide screen broke here: charts collapsed into thin columns, axis labels overlapped, and legends were cut off.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If placing items side by side would leave a chart under 60 columns, switch to stacking them vertically&lt;/li&gt;
&lt;li&gt;Drop legend entries that do not fit, starting from the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Open it once in the narrow pane before you polish anything.&lt;/strong&gt; That is the fastest route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking an Svg without opening the app
&lt;/h2&gt;

&lt;p&gt;Opening the desktop app every time is slow, so I put the SVG in an HTML file and screenshot it with headless Edge. The trick is to shoot at the usual panel width (about 440px).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--screenshot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;out.png&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--window-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file:///C:/work/stage.html&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup pane does not appear&lt;/td&gt;
&lt;td&gt;Open from a user action (automatic opens need 144 columns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colors are unreadable&lt;/td&gt;
&lt;td&gt;Pass theme color names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Raster&lt;/code&gt; is not drawn&lt;/td&gt;
&lt;td&gt;Branch on &lt;code&gt;e.surface&lt;/code&gt;, use &lt;code&gt;Svg&lt;/code&gt; on the desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Svg&lt;/code&gt; shrinks&lt;/td&gt;
&lt;td&gt;Always pass &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; (1 cell is about 7.6x18.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Svg&lt;/code&gt; flashes&lt;/td&gt;
&lt;td&gt;Do not use &lt;code&gt;isInteractive&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buttons cannot be styled&lt;/td&gt;
&lt;td&gt;Visuals in Svg, clicks on standard Buttons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout breaks&lt;/td&gt;
&lt;td&gt;Make the 58-column narrow pane your pass line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can tell whether a mod written for the terminal will draw on the desktop by checking its code for an &lt;code&gt;e.surface&lt;/code&gt; branch before installing. The &lt;a href="https://modscode.com/" rel="noopener noreferrer"&gt;modscode gallery&lt;/a&gt; lists only mods that explicitly target the desktop app, with reproductions of how they look in the narrow desktop panel.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>cli</category>
      <category>debugging</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Claude Code mod not loading or rendering with no error? Where to look</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:14:25 +0000</pubDate>
      <link>https://dev.to/nakadadev/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look-3bfg</link>
      <guid>https://dev.to/nakadadev/claude-code-mod-not-loading-or-rendering-with-no-error-where-to-look-3bfg</guid>
      <description>&lt;p&gt;When a mod does nothing and shows no error, the cause almost always falls into one of three groups: &lt;strong&gt;(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&lt;/strong&gt;. Start with &lt;code&gt;claude plugin validate&lt;/code&gt;, then search the debug log for the mod's name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mods fail silently
&lt;/h2&gt;

&lt;p&gt;The official troubleshooting page opens with this: when a mod fails to load or a hook fails, &lt;strong&gt;Claude Code skips it and keeps the session going&lt;/strong&gt;. A broken mod looks the same as a mod that does nothing.&lt;/p&gt;

&lt;p&gt;That design keeps Claude Code itself from going down, but it leaves the author with few clues. So decide in advance where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Where to look&lt;/th&gt;
&lt;th&gt;Common causes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No command, no band, nothing&lt;/td&gt;
&lt;td&gt;1. Static analysis&lt;/td&gt;
&lt;td&gt;Put &lt;code&gt;$&lt;/code&gt; in a variable, passed &lt;code&gt;$&lt;/code&gt; to a function in another file, dynamic import, an event not in this version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loaded, but some hooks do not run&lt;/td&gt;
&lt;td&gt;2. Runtime&lt;/td&gt;
&lt;td&gt;Used &lt;code&gt;process&lt;/code&gt; or &lt;code&gt;setTimeout&lt;/code&gt;, ran over 10 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The band or panel shows the default UI&lt;/td&gt;
&lt;td&gt;3. Component rules&lt;/td&gt;
&lt;td&gt;Passed &lt;code&gt;undefined&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, missing a required field, a component not available on that surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  First step: &lt;code&gt;claude plugin validate&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\my-mod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without starting a session, this runs the same static analysis Claude Code runs at load time. It catches misspelled events, broken manifests, and unreadable modules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passes but no &lt;code&gt;hooks:&lt;/code&gt; line appears: &lt;code&gt;hooks/hooks.json&lt;/code&gt; has no &lt;code&gt;modules&lt;/code&gt;, or it is misspelled&lt;/li&gt;
&lt;li&gt;An event you registered is missing from &lt;code&gt;hooks:&lt;/code&gt;: Claude Code will not call that function either&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. It fails static analysis
&lt;/h2&gt;

&lt;p&gt;At load time, the module source is analyzed, and if it fails, &lt;strong&gt;the whole plugin&lt;/strong&gt; is not loaded.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;const s = $.store; s.get(k)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Write &lt;code&gt;$.store.get(k)&lt;/code&gt; in full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;const { store } = $&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Do not destructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;helper($)&lt;/code&gt; (where &lt;code&gt;helper&lt;/code&gt; is in another file)&lt;/td&gt;
&lt;td&gt;You can pass &lt;code&gt;$&lt;/code&gt; only to top-level functions in the same file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passing &lt;code&gt;$&lt;/code&gt; to a function created inside a hook&lt;/td&gt;
&lt;td&gt;Move the function to the top level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;on(eventName, ...)&lt;/code&gt; (name in a variable)&lt;/td&gt;
&lt;td&gt;Write the event name as a string literal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declaring another variable named &lt;code&gt;on&lt;/code&gt; inside &lt;code&gt;register&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rename it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;await import('./x.mjs')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dynamic import is not allowed. Use a top-level &lt;code&gt;import&lt;/code&gt; statement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Importing a file outside the plugin&lt;/td&gt;
&lt;td&gt;Copy it into the plugin. The only outside import allowed is &lt;code&gt;claude-code&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;require(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use ES module &lt;code&gt;import&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The analysis also follows &lt;code&gt;on()&lt;/code&gt; calls in files you import.&lt;/strong&gt; I had a leftover test &lt;code&gt;on()&lt;/code&gt; in a shared helper, and the mod failed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registering the same event and matcher pair twice fails.&lt;/strong&gt; When several features want &lt;code&gt;AbovePrompt&lt;/code&gt;, register once and dispatch inside&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registering an event that does not exist in that version prevents the whole mod from loading&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That 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 &lt;code&gt;ui.fault&lt;/code&gt; (the event for &lt;code&gt;Client&lt;/code&gt; component failures). It worked in the terminal but vanished entirely on the desktop (bundled 2.1.286). The official reference now says &lt;code&gt;ui.fault&lt;/code&gt; requires &lt;strong&gt;v2.1.289 or later&lt;/strong&gt;. When you use a new event, compare the version note in the reference with the version shown by &lt;code&gt;/status&lt;/code&gt; on the desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. It uses something the hook runtime does not have
&lt;/h2&gt;

&lt;p&gt;Hooks 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 &lt;code&gt;setTimeout&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not available&lt;/strong&gt;: &lt;code&gt;process&lt;/code&gt;, &lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;setInterval&lt;/code&gt;, &lt;code&gt;Buffer&lt;/code&gt;, the DOM, &lt;code&gt;require&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Available&lt;/strong&gt;: &lt;code&gt;URL&lt;/code&gt;, &lt;code&gt;TextEncoder&lt;/code&gt;/&lt;code&gt;TextDecoder&lt;/code&gt;, &lt;code&gt;AbortController&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt; (including &lt;code&gt;crypto.subtle&lt;/code&gt;), &lt;code&gt;atob&lt;/code&gt;/&lt;code&gt;btoa&lt;/code&gt;, &lt;code&gt;structuredClone&lt;/code&gt;, &lt;code&gt;performance&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want&lt;/th&gt;
&lt;th&gt;Use instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wait&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.clock.sleep(ms)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run once later&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.clock.after(ms, fn)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run periodically&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.clock.every(ms, fn)&lt;/code&gt; (create it in &lt;code&gt;session.start&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;await $.clock.now()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files, network, processes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.fs&lt;/code&gt;, &lt;code&gt;$.http&lt;/code&gt;, &lt;code&gt;$.process&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In your own code, you just rewrite these. The trouble comes when you copy in an npm package: &lt;strong&gt;it reads &lt;code&gt;process.env&lt;/code&gt; the moment it loads, throws, and the whole mod silently disappears&lt;/strong&gt;. Putting local stand-ins at the top of that package's file, scoped to that file only, got it through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Time limits
&lt;/h3&gt;

&lt;p&gt;The other issue is time. These are the limits from the official reference that people hit most often.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A hook's own run time per call (waiting inside &lt;code&gt;next&lt;/code&gt; and APIs does not count; &lt;code&gt;$.clock.sleep&lt;/code&gt; does)&lt;/td&gt;
&lt;td&gt;10 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;.catch&lt;/code&gt; handler&lt;/td&gt;
&lt;td&gt;1 second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All &lt;code&gt;session.end&lt;/code&gt; hooks&lt;/td&gt;
&lt;td&gt;1.5 seconds combined (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.process.run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30 seconds by default, 10 minutes max&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Hooks that run longer than 10 seconds are skipped.&lt;/strong&gt; If you do heavy work in a guard that blocks tool calls (&lt;code&gt;tool.check&lt;/code&gt;), the guard itself gets bypassed, so be careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The returned tree breaks the component rules
&lt;/h2&gt;

&lt;p&gt;If the tree returned from &lt;code&gt;ui.render&lt;/code&gt; breaks the rules, &lt;strong&gt;Claude Code draws its own UI without any error&lt;/strong&gt;. Your band just disappears and the default comes back, which makes this the hardest one to notice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passing a field whose value is &lt;code&gt;undefined&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;May be treated as invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;autoFocus: false&lt;/code&gt;, &lt;code&gt;focus: false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Throws. Pass only &lt;code&gt;true&lt;/code&gt;, or omit it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;plain: false&lt;/code&gt; on a &lt;code&gt;Button&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cannot be drawn on the desktop. Use &lt;code&gt;true&lt;/code&gt; or omit it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Svg&lt;/code&gt; without &lt;code&gt;source&lt;/code&gt; or &lt;code&gt;alt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not drawn (both are required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;undefined&lt;/code&gt; in &lt;code&gt;Client&lt;/code&gt; props&lt;/td&gt;
&lt;td&gt;Invalid (JSON values only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A component not available on that surface (&lt;code&gt;Raster&lt;/code&gt; on the desktop, etc.)&lt;/td&gt;
&lt;td&gt;Invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Props a component does not accept (including typos)&lt;/td&gt;
&lt;td&gt;Invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Conditional fields naturally let &lt;code&gt;undefined&lt;/code&gt; slip in, so I wrap the component factories in one place and strip those values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Do not pass fields that have no value to components&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;make&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;make&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;make&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PDF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dimColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;busy&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing &lt;code&gt;dimColor: busy ? true : undefined&lt;/code&gt; avoids ever passing &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the one line that gives the reason
&lt;/h2&gt;

&lt;p&gt;When a module fails to load, a hook is skipped, or a tree is rejected, Claude Code writes &lt;strong&gt;one line containing the mod's name&lt;/strong&gt;. Where it goes depends on the session (official troubleshooting page).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Where the line appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interactive session loaded with &lt;code&gt;--plugin-dir&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A faint line in the conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal session with a mod installed from a marketplace&lt;/td&gt;
&lt;td&gt;Debug log only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude -p&lt;/code&gt; with &lt;code&gt;--plugin-dir&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Standard error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words, &lt;strong&gt;when an installed mod is silent, no amount of looking at the conversation will show anything&lt;/strong&gt;. Capture a debug log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--debug-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\mod-debug.log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--plugin-dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\my-mod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the log in another window (PowerShell example).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\mod-debug.log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Wait&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;my-mod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the strings to look for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Log text&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks module my-mod@inline loaded ... events: ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loaded, with the list of registered events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks module my-mod ... not loaded:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejected by settings or policy. The reason follows the colon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks module did not load:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Top-level code threw. File and line are shown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hook skipped:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A hook threw, timed out, or returned the wrong shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ui.render (Pane) refused:&lt;/code&gt; / &lt;code&gt;a hook returned a tree that does not validate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The tree broke the component rules. Reason included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;it crashed the hooks worker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The mod was removed, for example because of a loop that never yields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A line like &lt;code&gt;ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;To write your own markers, use &lt;code&gt;$.ui.log('got here', { to: 'debug' })&lt;/code&gt; to write to the log.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting a log from the desktop app
&lt;/h3&gt;

&lt;p&gt;The 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 &lt;strong&gt;not verified&lt;/strong&gt;. My routine is to load the same mod in the terminal with &lt;code&gt;--debug-file&lt;/code&gt;, find the reason line, fix it, and then check in the desktop app. However, things that fail &lt;strong&gt;only on the desktop&lt;/strong&gt;, like an event missing from that version, do not reproduce in the terminal, so check the desktop version with &lt;code&gt;/status&lt;/code&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can mods load at all?
&lt;/h2&gt;

&lt;p&gt;Before blaming your mod, mods may be turned off by configuration. Run &lt;code&gt;claude plugin test&lt;/code&gt; in a folder with no mod to see the state.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no hooks module to load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mods can load (there is just nothing to test in this folder)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks modules are turned off here&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turned off by &lt;code&gt;disableAllHooks&lt;/code&gt; or an organization policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks modules are turned off in this process&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turned off remotely by Anthropic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other causes of "nothing loads at all" include not having answered the trust prompt for a newly opened folder, and starting with &lt;code&gt;--safe-mode&lt;/code&gt; or &lt;code&gt;--bare&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run static analysis with &lt;code&gt;claude plugin validate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check for &lt;code&gt;process&lt;/code&gt;, &lt;code&gt;setTimeout&lt;/code&gt;, and the 10-second limit&lt;/li&gt;
&lt;li&gt;Check for &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, required fields, and components not available on that surface&lt;/li&gt;
&lt;li&gt;If still stuck, search the debug log for the mod's name&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To see how other mods are written so they pass static analysis, read them in the &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;modscode reviewed list&lt;/a&gt;. Every mod listed there is code that &lt;code&gt;claude plugin validate&lt;/code&gt; can read.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>debugging</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to show a line above the prompt in Claude Code with a mod (band, status line, toast)</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:09:24 +0000</pubDate>
      <link>https://dev.to/nakadadev/how-to-show-a-line-above-the-prompt-in-claude-code-with-a-mod-band-status-line-toast-3947</link>
      <guid>https://dev.to/nakadadev/how-to-show-a-line-above-the-prompt-in-claude-code-with-a-mod-band-status-line-toast-3947</guid>
      <description>&lt;p&gt;To always show a line right above the prompt, handle &lt;code&gt;ui.render&lt;/code&gt; with &lt;code&gt;{ component: 'AbovePrompt' }&lt;/code&gt; and return a tree (the band). For a single line of text below the prompt, use &lt;code&gt;$.ui.status(text)&lt;/code&gt;. For a notification that disappears after a few seconds, use &lt;code&gt;$.ui.toast(text)&lt;/code&gt;. The last two need no render hook and can be called from anywhere in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they differ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;How to show it&lt;/th&gt;
&lt;th&gt;How long it stays&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Band&lt;/td&gt;
&lt;td&gt;Return a tree from &lt;code&gt;ui.render&lt;/code&gt; for &lt;code&gt;AbovePrompt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Until you stop returning it&lt;/td&gt;
&lt;td&gt;Values you always want to see: price, remaining quota, state. Buttons allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status line&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.ui.status(text)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Until you change it&lt;/td&gt;
&lt;td&gt;One-line results of background work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toast&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.ui.toast(text)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 seconds by default&lt;/td&gt;
&lt;td&gt;"Done" notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faint line in the conversation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.ui.log(text)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stays (Claude does not read it)&lt;/td&gt;
&lt;td&gt;A short note you want on record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The band (the strip above the prompt)
&lt;/h2&gt;

&lt;p&gt;The band is the strip right above the prompt input, and &lt;strong&gt;all mods share one band&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbovePrompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;columnGap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BTC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Do not erase other mods' bands
&lt;/h3&gt;

&lt;p&gt;When you return a tree for the band, &lt;strong&gt;it replaces whatever mods that run after yours drew in the band&lt;/strong&gt;. To keep theirs, put the result of &lt;code&gt;await next(e)&lt;/code&gt; inside your own &lt;code&gt;Box&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbovePrompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theirs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`BTC &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;theirs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do not want to show anything, return &lt;code&gt;next(e)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register once per mod
&lt;/h3&gt;

&lt;p&gt;Registering the same event and matcher pair (&lt;code&gt;ui.render&lt;/code&gt; with &lt;code&gt;AbovePrompt&lt;/code&gt;) twice in one mod makes it fail to load. When your mod grows and you want both a "clock band" and a "price band," register once and lay out the sections inside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the width
&lt;/h3&gt;

&lt;p&gt;The band width arrives as &lt;code&gt;e.props.bodyColumns&lt;/code&gt; (in columns). When things do not fit, it looked cleaner to &lt;strong&gt;drop whole low-priority sections&lt;/strong&gt; than to cut text midway. A leftover count like "+3" or a truncated price looks half-finished.&lt;/p&gt;

&lt;p&gt;Desktop text is proportional and narrow, so more characters fit than the column count suggests. If you lay out strictly by column count, you drop sections while there is still room on the right. I treat &lt;strong&gt;the desktop width as 1.5 times&lt;/strong&gt; the column count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;bodyColumns&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;terminal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1.5 is a value I tuned on my own setup, not an official number. It may change with the font or zoom level, so leave some margin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Band buttons and number keys
&lt;/h3&gt;

&lt;p&gt;You can put a &lt;code&gt;Button&lt;/code&gt; in the band too. According to the official reference, &lt;strong&gt;if a band button has a numeric &lt;code&gt;hotkey&lt;/code&gt;, typing just that digit into an empty prompt and waiting a moment presses it&lt;/strong&gt;. That lets you put actions like "press 1 to open" in the band.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask for a redraw after changing a value
&lt;/h3&gt;

&lt;p&gt;As with the pane in Part 3, call &lt;code&gt;$.ui.invalidate('ui.render')&lt;/code&gt; after changing a value. For values fetched from outside, such as a price, create a timer in &lt;code&gt;session.start&lt;/code&gt; and refetch periodically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// If the fetch fails, keep the previous value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;setInterval&lt;/code&gt; inside hooks. Use &lt;code&gt;$.clock.every&lt;/code&gt; and &lt;code&gt;$.clock.after&lt;/code&gt; for timers (Part 5).&lt;/p&gt;

&lt;h2&gt;
  
  
  The status line
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checks: 3 passing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the official docs, in the terminal this shows one line below the prompt and stays until you change it. It is prefixed with &lt;code&gt;⚠&lt;/code&gt; and the mod's name, like &lt;code&gt;⚠ my-mod: checks: 3 passing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since it needs no render hook, it is the easiest way to show results from inside a timer. The official example runs &lt;code&gt;gh pr checks&lt;/code&gt; every minute and puts the result in the status line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checks: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ng&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where and how the status line appears in the desktop app is not described on the official pages, so it is &lt;strong&gt;not verified&lt;/strong&gt;. For values you always want visible on the desktop, the band, which is reliably drawn, is the safer place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toasts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PDF exported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the terminal, a toast appears at the top right with the mod's name and disappears after a few seconds. The default is 4 seconds, and you can change it with &lt;code&gt;{ timeoutMs }&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Backtest finished&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two main uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifying that background work has finished&lt;/li&gt;
&lt;li&gt;Standing in when you cannot open a pane on your own. The official docs also say to use a toast when you want to signal something without opening a pane&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you open a pane with &lt;code&gt;holdToasts: true&lt;/code&gt;, toasts wait and do not appear until that pane is closed. Use it when you do not want notifications piling up while someone is reading.&lt;/p&gt;

&lt;p&gt;Where toasts appear on screen in the desktop app is not described on the official pages, so it is &lt;strong&gt;not verified&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leave a faint line in the conversation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;build finished&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leaves a faint line in the conversation like &lt;code&gt;● my-mod: build finished&lt;/code&gt;. &lt;strong&gt;Claude does not read this line.&lt;/strong&gt; To tell Claude something, use the &lt;code&gt;text&lt;/code&gt; in a command's return value, or &lt;code&gt;$.prompt.submit&lt;/code&gt; (Part 8).&lt;/p&gt;

&lt;p&gt;If you pass a second argument, &lt;code&gt;$.ui.log('message', { to: 'debug' })&lt;/code&gt;, it is written to the debug log instead of the conversation. Part 5 uses this for debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a word to the spinner
&lt;/h2&gt;

&lt;p&gt;Another easy target is the spinner line shown while Claude is working. The very first official example is a mod that appends the number of tool calls after the spinner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Spinner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;` · tool calls: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;…`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing a copy with modified props to &lt;code&gt;next&lt;/code&gt; keeps Claude Code's spinner behavior and just adds text. &lt;code&gt;Spinner&lt;/code&gt; is a site drawn in both the terminal and the desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A value you always want to see&lt;/strong&gt;: the band&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results of background work&lt;/strong&gt;: the status line (its desktop appearance is not verified, so if it matters, show it in the band too)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "done" notification&lt;/strong&gt;: a toast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something to look at in detail, or with many actions&lt;/strong&gt;: the pane from Part 3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://modscode.com/" rel="noopener noreferrer"&gt;modscode gallery&lt;/a&gt; has many real mods that combine bands and toasts. Mods that show remaining usage or context size in the band are especially common, and they are good for comparing how each one drops band sections.&lt;/p&gt;

&lt;p&gt;The following articles in this series answer common problems, starting with the most frequent one: nothing is drawn and there is no error.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>programming</category>
      <category>tools</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to add a side pane to Claude Code with a mod using $.ui.open and ui.render</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:09:15 +0000</pubDate>
      <link>https://dev.to/nakadadev/how-to-add-a-side-pane-to-claude-code-with-a-mod-using-uiopen-and-uirender-3c24</link>
      <guid>https://dev.to/nakadadev/how-to-add-a-side-pane-to-claude-code-with-a-mod-using-uiopen-and-uirender-3c24</guid>
      <description>&lt;p&gt;To show a pane from a mod, you do two things: &lt;strong&gt;open it with &lt;code&gt;$.ui.open({ id })&lt;/code&gt;, and return its contents from &lt;code&gt;ui.render&lt;/code&gt; with &lt;code&gt;{ component: 'Pane' }&lt;/code&gt;&lt;/strong&gt;. Opening alone draws nothing. After you open it, Claude Code calls &lt;code&gt;ui.render&lt;/code&gt; to ask what to draw in that pane, and you return a tree (nested components) only when &lt;code&gt;e.requestId&lt;/code&gt; matches your &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a pane is
&lt;/h2&gt;

&lt;p&gt;A pane is your own panel next to the conversation. According to the official docs, it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a side panel next to the conversation on a wide screen&lt;/li&gt;
&lt;li&gt;a frame above the prompt when space is tight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you open several panes, you switch between them with tabs labeled by title.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal pane
&lt;/h2&gt;

&lt;p&gt;This pane opens with &lt;code&gt;/hello-pane&lt;/code&gt; and has a button that increments a counter. Keep the three-file layout from Part 2 and replace only &lt;code&gt;register.tsx&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The pane's name. Used as the marker when opening and drawing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PANE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello-pane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// The value the pane draws&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// At session start, add the command and read the previously saved count&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello-pane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Open the hello-pane panel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Open with /hello-pane&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;command.run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello-pane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PANE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;closeOnEscape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;// Show nothing in the conversation&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw the pane&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Leave other mods' panes alone&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;PANE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello pane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;columnGap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;more&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// Keep it for the next session&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`Count: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;code&gt;hello-tabs&lt;/code&gt; example from the official interface page, reduced to a single screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening: &lt;code&gt;$.ui.open&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;id&lt;/code&gt; is required. Everything else is optional.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The pane's name. Used as &lt;code&gt;e.requestId&lt;/code&gt; when drawing and in &lt;code&gt;$.ui.close({ id })&lt;/code&gt; when closing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tab label when there are several panes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;focus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send keyboard input to the pane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;closeOnEscape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close with Esc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;holdToasts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hold toasts until the pane is closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preferred height when shown above the prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;columns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preferred width when shown next to the conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;focus&lt;/code&gt;, &lt;code&gt;closeOnEscape&lt;/code&gt;, and &lt;code&gt;holdToasts&lt;/code&gt; accept only &lt;code&gt;true&lt;/code&gt;.&lt;/strong&gt; Writing &lt;code&gt;focus: false&lt;/code&gt; throws. To set one conditionally, include or omit the whole field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pane&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PANE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pane&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pane&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pane &lt;code&gt;id&lt;/code&gt; can use letters, digits, &lt;code&gt;_&lt;/code&gt;, and &lt;code&gt;-&lt;/code&gt;, up to 64 characters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open from a command
&lt;/h3&gt;

&lt;p&gt;The example opens the pane from the &lt;code&gt;/hello-pane&lt;/code&gt; command. &lt;strong&gt;Opening in response to a user action (a command, a button press, a sent prompt) is the norm.&lt;/strong&gt; An open with no user action behind it, for example from &lt;code&gt;session.start&lt;/code&gt; or a timer, is only placed when the screen is wide enough. On the desktop, this is almost always the cause of "I started it but the pane did not appear." Part 6 covers it in detail.&lt;/p&gt;

&lt;p&gt;If you want the command to open the pane even while Claude is working, add &lt;code&gt;immediate: true&lt;/code&gt; to &lt;code&gt;$.command.register&lt;/code&gt;. Without it, the command waits until the turn ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing: &lt;code&gt;ui.render&lt;/code&gt; and &lt;code&gt;Pane&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ui.render&lt;/code&gt; is called many times, once per place to draw. The second argument &lt;code&gt;{ component: 'Pane' }&lt;/code&gt; (the matcher) narrows it to panes, and then &lt;code&gt;e.requestId&lt;/code&gt; tells you whether it is your pane. &lt;strong&gt;If it is not your pane, &lt;code&gt;return next(e)&lt;/code&gt;&lt;/strong&gt; to pass it on. Forget this and you will blank out other mods' panes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The redraw cycle
&lt;/h3&gt;

&lt;p&gt;A pane is a snapshot of "the last tree &lt;code&gt;ui.render&lt;/code&gt; returned." Changing a value alone does not change the screen.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the value in the button's &lt;code&gt;onPress&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ask for a redraw with &lt;code&gt;$.ui.invalidate('ui.render')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Claude Code calls &lt;code&gt;ui.render&lt;/code&gt; again and rebuilds the tree with the new value&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This cycle is the same for every component. Claude Code redraws on its own only when the site's props or the screen width change. It does not redraw on a timer by itself. To redraw on a timer, create &lt;code&gt;$.clock.every(1000, () =&amp;gt; $.ui.invalidate('ui.render'))&lt;/code&gt; in &lt;code&gt;session.start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Redraws are throttled to 10 per second. Faster requests are merged into one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fit the width
&lt;/h3&gt;

&lt;p&gt;The pane width arrives as &lt;code&gt;e.props.bodyColumns&lt;/code&gt; (in columns). When the pane is next to the conversation (&lt;code&gt;e.props.placement === 'dock'&lt;/code&gt;), the height arrives as &lt;code&gt;e.props.scroll.bodyRows&lt;/code&gt;. If the tree is taller than the pane, the whole thing scrolls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The narrow side pane you usually open on the desktop was about 58 columns (about 440px).&lt;/strong&gt; Layouts tuned on a wide screen break here. If you make "no overlapping or clipped text at this width" your pass line from the start, you will not have to rebuild later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping values: three places
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Place&lt;/th&gt;
&lt;th&gt;How long it lasts&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A top-level variable in the file&lt;/td&gt;
&lt;td&gt;Until the mod reloads (every save during development)&lt;/td&gt;
&lt;td&gt;Display state you can afford to lose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Until the session ends, or until &lt;code&gt;/clear&lt;/code&gt;, &lt;code&gt;/resume&lt;/code&gt;, &lt;code&gt;/branch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Values the renderer reads that should survive a reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$.store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Until the mod deletes it (saved as JSON per plugin)&lt;/td&gt;
&lt;td&gt;Settings, history, things you want next time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The example keeps the count in &lt;code&gt;$.store&lt;/code&gt;, so it survives closing and reopening Claude Code. &lt;code&gt;$.store&lt;/code&gt; is &lt;strong&gt;limited to 4 MiB per plugin&lt;/strong&gt;, and every session on the same PC shares one store. If two sessions read the same key and write it back, the later write overwrites the earlier one. For values written from several sessions, split the keys or re-read right before writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can draw on the desktop
&lt;/h2&gt;

&lt;p&gt;A desktop pane can hold &lt;code&gt;Box&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;, &lt;code&gt;Markdown&lt;/code&gt;, &lt;code&gt;Svg&lt;/code&gt;, and others. &lt;strong&gt;HTML is not supported.&lt;/strong&gt; You cannot put a web page into the panel as is, so draw rich visuals with &lt;code&gt;Svg&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Svg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" width="100%" height="100%"&amp;gt;
    &amp;lt;rect x="0" y="0" width="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" height="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" fill="#4a90d9"/&amp;gt;
  &amp;lt;/svg&amp;gt;`&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Count &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Svg&lt;/code&gt; needs both &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;alt&lt;/code&gt;, and without &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; it shrinks to a small box. Part 6 explains why and how to fix it.&lt;/p&gt;

&lt;p&gt;You cannot change how buttons look (color, shape, size). The most you can do is drop the brackets with &lt;code&gt;plain: true&lt;/code&gt; and dim them with &lt;code&gt;dimColor: true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard
&lt;/h2&gt;

&lt;p&gt;A pane receives keyboard input when it was opened with &lt;code&gt;focus: true&lt;/code&gt;, when it is clicked, or after Ctrl+X followed by Tab. While it has focus, Tab moves to the next component, Enter presses, a button's &lt;code&gt;hotkey&lt;/code&gt; (one digit or one lowercase letter) presses that button, and Esc returns to the prompt. On the desktop, the key is shown small next to the button label.&lt;/p&gt;

&lt;h2&gt;
  
  
  See real examples
&lt;/h2&gt;

&lt;p&gt;Real mods that use panes can be read alongside reproductions of their UI in the &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;modscode reviewed list&lt;/a&gt;. Seeing how they lay things out in the narrow desktop panel was the most useful part for me.&lt;/p&gt;

&lt;p&gt;The next article in this series covers the band, status line, and toast, which are lighter-weight than a pane.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why a Claude Code mod doesn't work on Windows: tail, date, open, osascript in $.process and silent sound</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:06:52 +0000</pubDate>
      <link>https://dev.to/nakadadev/why-a-claude-code-mod-doesnt-work-on-windows-tail-date-open-osascript-in-process-and-silent-2gjg</link>
      <guid>https://dev.to/nakadadev/why-a-claude-code-mod-doesnt-work-on-windows-tail-date-open-osascript-in-process-and-silent-2gjg</guid>
      <description>&lt;p&gt;Most mods written on Mac or Linux break on Windows because they &lt;strong&gt;use &lt;code&gt;$.process&lt;/code&gt; to launch programs Windows doesn't have&lt;/strong&gt; (&lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;osascript&lt;/code&gt;, &lt;code&gt;afplay&lt;/code&gt;, &lt;code&gt;xdg-open&lt;/code&gt; and so on). &lt;code&gt;$.process.run&lt;/code&gt; doesn't go through a shell, so cmd built-ins like &lt;code&gt;start&lt;/code&gt; or &lt;code&gt;dir&lt;/code&gt; can't be launched either. The other cause is sound: on Windows, &lt;code&gt;$.audio.play&lt;/code&gt; &lt;strong&gt;plays nothing and still reports success&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Versions checked: Claude Code 2.1.284 to 2.1.288 (October 2026, Windows 11).&lt;/p&gt;

&lt;h2&gt;
  
  
  How common is this?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modscode.com/review/" rel="noopener noreferrer"&gt;The modscode review&lt;/a&gt; read 1,383 public mods on GitHub and found 167 that use &lt;code&gt;$.process&lt;/code&gt; to launch a program Windows doesn't have (as of 2026-10-06). That's roughly 1 in 8. Many still had Mac tool names left in them as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;$.process.run&lt;/code&gt; doesn't use a shell
&lt;/h2&gt;

&lt;p&gt;The official API page says &lt;code&gt;$.process.run&lt;/code&gt; &lt;strong&gt;takes an array of arguments and does not use a shell&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;// r is { exitCode, stdout, stderr }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because there's no shell, this happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;['tail', '-n', '20', 'log.txt']&lt;/code&gt;: Windows has no &lt;code&gt;tail&lt;/code&gt;, so it can't start and the call &lt;strong&gt;rejects&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;['start', 'report.pdf']&lt;/code&gt;: &lt;code&gt;start&lt;/code&gt; is a cmd built-in, not an executable, so it can't start&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;['ls']&lt;/code&gt;, &lt;code&gt;['cat', ...]&lt;/code&gt;, &lt;code&gt;['date']&lt;/code&gt;: same, they don't exist (&lt;code&gt;date&lt;/code&gt; is a cmd built-in and means something different anyway)&lt;/li&gt;
&lt;li&gt;A single string like &lt;code&gt;'git status'&lt;/code&gt;: pass an array instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;$.process.run&lt;/code&gt; rejects when the program can't start and when it times out (30 seconds by default). A non-zero exit code still resolves. &lt;strong&gt;If you don't wrap it in &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt;, the throw takes the whole hook down with it&lt;/strong&gt; (part 5).&lt;/p&gt;

&lt;h2&gt;
  
  
  Programs Windows doesn't have, and what to use instead
&lt;/h2&gt;

&lt;p&gt;The best fix is to &lt;strong&gt;not launch an external program at all and use the mods API instead&lt;/strong&gt;. With the API you don't have to care about the OS.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common on Mac/Linux&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;th&gt;Replacement inside a mod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current time&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;await $.clock.now()&lt;/code&gt; or &lt;code&gt;Date.now()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reading a file&lt;/td&gt;
&lt;td&gt;Read with &lt;code&gt;$.fs.read(path)&lt;/code&gt; and slice in JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Listing a folder&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.fs.list(path)&lt;/code&gt; (one level only, not recursive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test -f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checking existence&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.fs.exists(path)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.http.fetch(url, init)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;osascript -e 'display notification'&lt;/code&gt;, &lt;code&gt;notify-send&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Notifications&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$.ui.toast(text)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;afplay&lt;/code&gt;, &lt;code&gt;say&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sound, speech&lt;/td&gt;
&lt;td&gt;See the "Sound" section below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;open&lt;/code&gt;, &lt;code&gt;xdg-open&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Open with the default app&lt;/td&gt;
&lt;td&gt;On Windows only, PowerShell's &lt;code&gt;Start-Process&lt;/code&gt; (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pbcopy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clipboard&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.ui.copy&lt;/code&gt; (listed in the official API list; check the behavior in the type definitions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sh -c '...'&lt;/code&gt;, &lt;code&gt;bash -c '...'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;One-line shell commands&lt;/td&gt;
&lt;td&gt;Launch directly with an array, or replace with the API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Programs that ship an executable with the same name on Windows, like &lt;code&gt;git&lt;/code&gt; or &lt;code&gt;gh&lt;/code&gt;, work as-is, as long as they're on the PATH.&lt;/p&gt;

&lt;p&gt;One more thing to watch: commands &lt;strong&gt;installed as &lt;code&gt;.cmd&lt;/code&gt; files&lt;/strong&gt; on Windows, like &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;npx&lt;/code&gt;. Whether a &lt;code&gt;.cmd&lt;/code&gt; file can be launched directly without a shell isn't documented on the official pages, so this is &lt;strong&gt;not verified&lt;/strong&gt;. If it doesn't work, launch it explicitly, e.g. &lt;code&gt;['cmd.exe', '/c', 'npm', ...]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting the OS
&lt;/h2&gt;

&lt;p&gt;There's no &lt;code&gt;process&lt;/code&gt; inside a hook, so you can't use &lt;code&gt;process.platform&lt;/code&gt;. I tell them apart by the shape of &lt;code&gt;$.plugin.root&lt;/code&gt; (the plugin folder).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isWindows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// starts with a drive letter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Opening a file with the default app
&lt;/h2&gt;

&lt;p&gt;Instead of Mac's &lt;code&gt;open report.pdf&lt;/code&gt;, launch PowerShell's &lt;code&gt;Start-Process&lt;/code&gt; on Windows. &lt;strong&gt;Don't embed the path in the command string; pass it through an environment variable.&lt;/strong&gt; A single &lt;code&gt;'&lt;/code&gt; or space in the path breaks it, and it also opens the door to injection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isWindows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;powershell.exe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-NoProfile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-NonInteractive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-Command&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Start-Process -FilePath $env:MOD_FILE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;MOD_FILE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the open &lt;strong&gt;from a clock&lt;/strong&gt;, not inside the button-press event. Processes started inside an event were sometimes killed when the event ended (part 8).&lt;/p&gt;

&lt;h2&gt;
  
  
  No sound: &lt;code&gt;$.audio.play&lt;/code&gt; succeeds silently on Windows
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sounds/success.wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// Windows: plays nothing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows this &lt;strong&gt;plays nothing, throws nothing, and never reaches &lt;code&gt;.catch&lt;/code&gt;&lt;/strong&gt;. The Code tab in the desktop app runs the same executable, so it was silent there too.&lt;/p&gt;

&lt;p&gt;In this version, the core plays sound only through &lt;code&gt;globalThis.Audio&lt;/code&gt; or macOS &lt;code&gt;afplay&lt;/code&gt;. On Windows it writes "no audio player on windows; not played" to the debug log and &lt;strong&gt;resolves as a success&lt;/strong&gt;. From your code it never looks like a failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: launch PowerShell's SoundPlayer
&lt;/h3&gt;

&lt;p&gt;On Windows only, use &lt;code&gt;$.process.run&lt;/code&gt; to start PowerShell and play the WAV with &lt;code&gt;System.Media.SoundPlayer&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PS_PLAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(New-Object System.Media.SoundPlayer $env:MOD_WAV).PlaySync()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;soundBusy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;playSound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;soundBusy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;                           &lt;span class="c1"&gt;// drop sounds that arrive while one is playing&lt;/span&gt;
  &lt;span class="nx"&gt;soundBusy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;powershell.exe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-NoProfile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-NonInteractive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-WindowStyle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-Command&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PS_PLAY&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;MOD_WAV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wav&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// don't stop the mod even if the sound fails&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;soundBusy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Five things to watch
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass the path through an environment variable&lt;/strong&gt; (same reason as above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use PCM WAV.&lt;/strong&gt; SoundPlayer can't play MP3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup takes about 1 second.&lt;/strong&gt; Drop sounds that arrive while one is playing so processes don't pile up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't change the volume&lt;/strong&gt; (&lt;code&gt;gain&lt;/code&gt; has no effect). Lower it in the WAV itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't start it inside an event (button or command).&lt;/strong&gt; Play it from a &lt;code&gt;$.clock.every&lt;/code&gt; clock created in &lt;code&gt;session.start&lt;/code&gt; and it plays to the end&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To narrow down "no sound" problems, it helps to keep one command that plays a sound and reports the result as text. In practice, almost every "no sound" case was either the PC being muted or this path difference. Once the core supports sound on Windows, this workaround becomes unnecessary. When the version goes up, try &lt;code&gt;$.audio.play&lt;/code&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weight of launching PowerShell
&lt;/h2&gt;

&lt;p&gt;So far the Windows workarounds have used PowerShell. But PowerShell is a shell that can do anything. &lt;strong&gt;When a mod launches a shell, a reader has to check the actual command string to know whether it's safe.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The modscode review also has "doesn't use a shell" as a check (the program name is written in the code and isn't a shell). The sound workaround above trips that check.&lt;/p&gt;

&lt;p&gt;For a mod you publish, think in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the API can do it, use the API (time, files, notifications, network)&lt;/li&gt;
&lt;li&gt;Launch an external program only when it can't, keep the command string fixed, and pass changing values through environment variables&lt;/li&gt;
&lt;li&gt;For "nice to have" features like sound, not playing it on Windows is also an option&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Small Windows differences
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path separators&lt;/strong&gt;: relative paths passed to &lt;code&gt;$.fs&lt;/code&gt; are resolved from the session's working folder. When you build paths yourself, accept both &lt;code&gt;\&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE_CODE_PLUGIN_DIRS&lt;/code&gt;&lt;/strong&gt;: when listing multiple folders, Windows separates them with &lt;code&gt;;&lt;/code&gt; (Mac and Linux use &lt;code&gt;:&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WSL&lt;/strong&gt;: in the desktop app's WSL sessions, plugins themselves aren't available, so mods don't run (part 1)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A feature silently stops&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;osascript&lt;/code&gt; don't exist on Windows&lt;/td&gt;
&lt;td&gt;Replace with the mods API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;start&lt;/code&gt; can't open files&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.process.run&lt;/code&gt; doesn't go through a shell&lt;/td&gt;
&lt;td&gt;Call &lt;code&gt;powershell.exe&lt;/code&gt; &lt;code&gt;Start-Process&lt;/code&gt; explicitly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No sound (no exception)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.audio.play&lt;/code&gt; succeeds silently on Windows&lt;/td&gt;
&lt;td&gt;PowerShell SoundPlayer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opened apps or sounds get cut off&lt;/td&gt;
&lt;td&gt;Processes are killed when the event ends&lt;/td&gt;
&lt;td&gt;Start them from a clock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To find out whether a mod works on Windows before installing it, search its code for &lt;code&gt;$.process&lt;/code&gt; and look at the program names it launches. The 49 mods in &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;the modscode reviewed gallery&lt;/a&gt; are only those whose code was checked to not launch programs missing on Windows.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>How to make a Claude Code mod with three files that work in the desktop app</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:06:48 +0000</pubDate>
      <link>https://dev.to/nakadadev/how-to-make-a-claude-code-mod-with-three-files-that-work-in-the-desktop-app-26mm</link>
      <guid>https://dev.to/nakadadev/how-to-make-a-claude-code-mod-with-three-files-that-work-in-the-desktop-app-26mm</guid>
      <description>&lt;p&gt;You can build a Claude Code mod with &lt;strong&gt;just three files&lt;/strong&gt;: &lt;code&gt;.claude-plugin/plugin.json&lt;/code&gt;, &lt;code&gt;hooks/hooks.json&lt;/code&gt;, and &lt;code&gt;hooks/register.tsx&lt;/code&gt;. No Node.js and no build step. Claude Code loads &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, and &lt;code&gt;.tsx&lt;/code&gt; files directly. To draw differently in the desktop app and the terminal, branch on &lt;code&gt;e.surface === 'desktop'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  File layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello-band/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.claude-plugin/plugin.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The plugin's name and version. Nothing extra is needed for a mod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks/hooks.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists one file to load under &lt;code&gt;modules&lt;/code&gt;. &lt;strong&gt;This is what turns a plugin into a mod&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks/register.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The main code. Exports &lt;code&gt;register(on)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The main file can be any of &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.mjs&lt;/code&gt;, &lt;code&gt;.cjs&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.mts&lt;/code&gt;, &lt;code&gt;.cts&lt;/code&gt;, or &lt;code&gt;.tsx&lt;/code&gt;, written as an ES module (see the Files table in the official reference). This series uses &lt;code&gt;register.tsx&lt;/code&gt;, but &lt;code&gt;register.js&lt;/code&gt; would have the same contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. plugin.json
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-band"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A minimal mod that shows one line above the prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick a &lt;code&gt;name&lt;/code&gt; you do not plan to change. People installing from a marketplace use &lt;code&gt;hello-band@&amp;lt;marketplace-name&amp;gt;&lt;/code&gt;, so renaming makes it a different plugin (Part 9). Names starting with &lt;code&gt;claude-&lt;/code&gt; look like Anthropic's own and are rejected by &lt;code&gt;claude plugin validate&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. hooks/hooks.json
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"modules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./register.tsx"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path is relative to &lt;code&gt;hooks.json&lt;/code&gt;, and you list &lt;strong&gt;exactly one&lt;/strong&gt; file. If &lt;code&gt;modules&lt;/code&gt; is missing or misspelled, validation still passes but the plugin is not loaded as a mod (Part 5).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. hooks/register.tsx
&lt;/h2&gt;

&lt;p&gt;This mod shows one line in the strip above the prompt (the band), with different text in the desktop app and the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// How many prompts have been sent. Shared between hooks&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Count every time a prompt is sent&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt.submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="c1"&gt;// Ask for a redraw&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw the strip above the prompt&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbovePrompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Which app is drawing&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;columnGap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dimColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`Sent &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Terminal&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`[terminal] sent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The three arguments
&lt;/h3&gt;

&lt;p&gt;Every function you pass to &lt;code&gt;on&lt;/code&gt; receives the same three arguments.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Argument&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The mods API. Everything that touches the outside world (UI, files, network, processes) goes through it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The event payload. It is frozen, so to change it you pass a copy to &lt;code&gt;next&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The next step. Calling it runs the later mods and Claude Code's default behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;return next(e)&lt;/code&gt; means "I only looked; pass it through unchanged." If you return a value without calling &lt;code&gt;next&lt;/code&gt;, you have answered the event yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch on &lt;code&gt;e.surface&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;e.surface&lt;/code&gt; is either &lt;code&gt;'terminal'&lt;/code&gt; or &lt;code&gt;'desktop'&lt;/code&gt;. When you draw differently on desktop and terminal, &lt;strong&gt;always branch on this&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The component table returned by &lt;code&gt;$.ui.resolve(e)&lt;/code&gt; contains the names of every component. Checking whether &lt;code&gt;t.Svg&lt;/code&gt; exists does not tell you whether you are on the desktop. &lt;code&gt;Svg&lt;/code&gt; exists only on the desktop and &lt;code&gt;Raster&lt;/code&gt; only in the terminal, so if you get the surface wrong, nothing is drawn (Part 6).&lt;/p&gt;

&lt;h3&gt;
  
  
  Use theme color names
&lt;/h3&gt;

&lt;p&gt;The example uses &lt;code&gt;color: 'success'&lt;/code&gt; so the text is readable in both the light and dark desktop themes. A hard-coded hex value like &lt;code&gt;'#7FD1A6'&lt;/code&gt; can become unreadable in the light theme (Part 6).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Validate what you wrote
&lt;/h2&gt;

&lt;p&gt;Before launching, run the validator from your shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\hello-band&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it passes, you see lines like these in the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ❯ ./register.tsx hooks: prompt.submit, ui.render{component=AbovePrompt}
  ❯ ./register.tsx calls: $.ui.invalidate, $.ui.resolve

✔ Validation passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hooks:&lt;/code&gt; lists the events you registered, and &lt;code&gt;calls:&lt;/code&gt; lists the APIs you use. If an event you meant to register is not under &lt;code&gt;hooks:&lt;/code&gt;, Claude Code will not call that function either. A misspelled event name shows up as an error like &lt;code&gt;"tool.calls" is not an event&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(The contents of &lt;code&gt;calls:&lt;/code&gt; can differ slightly between versions. The two lines above show the shape.)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Load it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Try it in the terminal
&lt;/h3&gt;

&lt;p&gt;In the terminal, &lt;code&gt;--plugin-dir&lt;/code&gt; loads the mod for a single session. It reloads every time you save.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--plugin-dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\hello-band&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Try it in the desktop app
&lt;/h3&gt;

&lt;p&gt;The desktop app cannot take flags at launch. The official docs describe these routes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn a local folder into a marketplace and install from it.&lt;/strong&gt; Add a folder containing &lt;code&gt;marketplace.json&lt;/code&gt; by local path, for example &lt;code&gt;claude plugin marketplace add .\my-mods&lt;/code&gt;, and install from there (Part 9). The terminal, local desktop sessions, and VS Code read the same settings file, so installing at user scope makes the desktop app load it too&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;CLAUDE_CODE_PLUGIN_DIRS&lt;/code&gt; environment variable.&lt;/strong&gt; The official reference describes it as folders loaded like &lt;code&gt;--plugin-dir&lt;/code&gt;, for apps that cannot take the flag. On Windows, separate entries with &lt;code&gt;;&lt;/code&gt;. You can also put it in &lt;code&gt;env&lt;/code&gt; in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have Claude write it.&lt;/strong&gt; In a session, ask something like "make a mod that shows the current branch above the prompt." Claude writes it with the built-in &lt;code&gt;plugin-authoring&lt;/code&gt; skill, and once you approve, it loads in the same session&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For route 1, the official loading page says that &lt;strong&gt;for a marketplace added by local path, &lt;code&gt;./&lt;/code&gt; sources are read in place without copying&lt;/strong&gt;. After editing the source, changes apply at the start of the next session or with &lt;code&gt;/reload-plugins&lt;/code&gt;, and you do not need to bump &lt;code&gt;version&lt;/code&gt;. (A marketplace added from GitHub installs a copy, so changes do not arrive unless you bump &lt;code&gt;version&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Whether routes 2 and 3 behave the same way in the desktop app's Code tab is not stated on the official pages, so it is &lt;strong&gt;not verified&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If unsure, a safe split is: &lt;strong&gt;build with &lt;code&gt;--plugin-dir&lt;/code&gt; in the terminal, do the final check in the desktop app&lt;/strong&gt;. As Part 6 shows, some things break only on the desktop, so always open it there at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check that it loaded
&lt;/h3&gt;

&lt;p&gt;In the terminal, open &lt;code&gt;/plugin&lt;/code&gt; and you will see a faint line under the tabs like &lt;code&gt;1 mod active · hello-band&lt;/code&gt;. On the desktop, if the band shows up, it loaded. If it does not, see Part 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the type definitions
&lt;/h2&gt;

&lt;p&gt;When you load with &lt;code&gt;--plugin-dir&lt;/code&gt; or have Claude write the mod, Claude Code writes the type definitions (&lt;code&gt;.d.ts&lt;/code&gt;) for that version into &lt;code&gt;.claude-plugin/types/&lt;/code&gt; inside the mod folder. They list the events, APIs, and components accurately for that version, so when in doubt, trust them over web pages (the official create page says the same).&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules for writing &lt;code&gt;$&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One rule that even a minimal mod can trip over: at load time, Claude Code &lt;strong&gt;statically analyzes the source&lt;/strong&gt; to check how you use &lt;code&gt;$&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write &lt;code&gt;$&lt;/code&gt;, the namespace, and the function name &lt;strong&gt;in full&lt;/strong&gt;, like &lt;code&gt;$.store.get(k)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;const ui = $.ui&lt;/code&gt; or &lt;code&gt;const { store } = $&lt;/code&gt; fails&lt;/li&gt;
&lt;li&gt;You may pass &lt;code&gt;$&lt;/code&gt; only to functions declared at the top level of the same file&lt;/li&gt;
&lt;li&gt;Write event names as string literals, like &lt;code&gt;'prompt.submit'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this check fails, &lt;strong&gt;the whole mod is not loaded&lt;/strong&gt;. Part 5 goes into detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;You now have one line in the band. The next article in this series opens your own panel next to the conversation: a pane.&lt;/p&gt;

&lt;p&gt;To see how other people write the three files, the &lt;a href="https://modscode.com/gallery/" rel="noopener noreferrer"&gt;modscode reviewed list&lt;/a&gt; shows each mod's code side by side with its UI.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>plugin</category>
      <category>tutorial</category>
      <category>typescript</category>
    </item>
    <item>
      <title>What are Claude Code mods? What they can draw and where they run in the desktop app</title>
      <dc:creator>Haruki Nakada</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:06:44 +0000</pubDate>
      <link>https://dev.to/nakadadev/what-are-claude-code-mods-what-they-can-draw-and-where-they-run-in-the-desktop-app-58pc</link>
      <guid>https://dev.to/nakadadev/what-are-claude-code-mods-what-they-can-draw-and-where-they-run-in-the-desktop-app-58pc</guid>
      <description>&lt;p&gt;Claude Code mods are plugins made of JavaScript or TypeScript functions that run inside Claude Code. They can add your own panels to the screen and intercept Claude's tool calls. What they draw only shows up in &lt;strong&gt;the terminal &lt;code&gt;claude&lt;/code&gt; and the Code tab of the desktop app&lt;/strong&gt;. In the desktop app, mods work from Claude Code 2.1.286.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a mod does
&lt;/h2&gt;

&lt;p&gt;The official docs describe a mod as "a plugin that changes how Claude Code looks and behaves." Inside, it is a set of functions keyed by event, and Claude Code calls the matching function every time an event happens. Events are things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude is about to use a tool (Bash, Read, and so on)&lt;/li&gt;
&lt;li&gt;The user sent a prompt&lt;/li&gt;
&lt;li&gt;Claude Code is about to draw some part of the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The function that gets called can &lt;strong&gt;just observe&lt;/strong&gt; the event, &lt;strong&gt;rewrite&lt;/strong&gt; it, or &lt;strong&gt;answer it itself and stop the default behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code already had "hooks" (shell commands written in settings.json). Mods differ in that they run as functions inside Claude Code. The official pages call a mod's function a "hook" and the settings-file kind a "settings hook."&lt;/p&gt;

&lt;h2&gt;
  
  
  What mods can do
&lt;/h2&gt;

&lt;p&gt;A short summary of what the official overview lists:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Draw your own UI&lt;/td&gt;
&lt;td&gt;A pane next to the conversation, a band above the prompt. Tabs, buttons, and inputs are allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redraw Claude Code's own UI&lt;/td&gt;
&lt;td&gt;Tool call rows, the spinner, the question dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intercept tool calls and requests&lt;/td&gt;
&lt;td&gt;Stop a dangerous command and confirm, route to another model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add your own commands&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/xxx&lt;/code&gt; runs a function without using a Claude turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share values between hooks&lt;/td&gt;
&lt;td&gt;One hook counts something, another draws it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference from existing tools is whether you can draw. Settings hooks, skills, and MCP servers cannot draw on the screen. Use a mod when you want a pane or a band, a skill when you keep pasting the same instructions, and an MCP server when you want to connect an external service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where mods can draw
&lt;/h2&gt;

&lt;p&gt;The official docs call the places a mod can draw "render sites." This series covers four: the pane and the band, plus the status line and toast, which you show by just calling an API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Where it appears&lt;/th&gt;
&lt;th&gt;Article in this series&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pane (&lt;code&gt;Pane&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A panel next to the conversation. A frame above the prompt when space is tight&lt;/td&gt;
&lt;td&gt;Part 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Band (&lt;code&gt;AbovePrompt&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The strip right above the prompt. Shared by all mods&lt;/td&gt;
&lt;td&gt;Part 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status line (&lt;code&gt;$.ui.status&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;One line below the prompt&lt;/td&gt;
&lt;td&gt;Part 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toast (&lt;code&gt;$.ui.toast&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A small notification that disappears after a few seconds&lt;/td&gt;
&lt;td&gt;Part 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also redraw the spinner (&lt;code&gt;Spinner&lt;/code&gt;), messages (&lt;code&gt;UserMessage&lt;/code&gt;, &lt;code&gt;AssistantMessage&lt;/code&gt;), tool rows (&lt;code&gt;ToolUse&lt;/code&gt; and others), and the question dialog (&lt;code&gt;AskUserQuestion&lt;/code&gt;). &lt;strong&gt;The only thing you cannot redraw is the permission prompt.&lt;/strong&gt; That is so a mod cannot disguise what you are being asked to approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where mods run
&lt;/h2&gt;

&lt;p&gt;This is the most commonly misunderstood part. &lt;strong&gt;Where the functions (hooks) run and where the drawn UI is visible are two different things.&lt;/strong&gt; Here is the official table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where Claude Code runs&lt;/th&gt;
&lt;th&gt;Hooks run&lt;/th&gt;
&lt;th&gt;Drawn UI is visible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal &lt;code&gt;claude&lt;/code&gt; (including terminals inside editors and JetBrains)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop app Code tab (except WSL sessions)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (except terminal-only components)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop app WSL sessions&lt;/td&gt;
&lt;td&gt;No (plugins are not available in WSL)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code extension chat panel&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude -p&lt;/code&gt; and the Agent SDK&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote Control from a phone or claude.ai&lt;/td&gt;
&lt;td&gt;Yes (in the session on your local PC)&lt;/td&gt;
&lt;td&gt;Only in the local terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud sessions&lt;/td&gt;
&lt;td&gt;Yes, if the plugin was delivered&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words, &lt;strong&gt;mod panels do not appear in the mobile app or on claude.ai/code&lt;/strong&gt;. They are not drawn in the VS Code extension either. If you build a mod that draws, assume it will be used in the terminal or the desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the desktop app needs
&lt;/h2&gt;

&lt;p&gt;According to the official docs, mods are enabled by default. The version requirement depends on where you run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terminal&lt;/strong&gt;: Claude Code 2.1.287 or later. Check with &lt;code&gt;claude --version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop app&lt;/strong&gt;: the app bundles its own Claude Code, and mods work &lt;strong&gt;from 2.1.286&lt;/strong&gt;. Type &lt;code&gt;/status&lt;/code&gt; in a local session in the Code tab and look at the version on the "Claude Code" line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Claude Code inside the desktop app is separate from the one you installed for the terminal. &lt;strong&gt;Even if your terminal is up to date, the bundled desktop version may be older.&lt;/strong&gt; When you use a new event or component, check first that the desktop version supports it. (Part 5 covers a case where registering an event that did not exist in that version made the whole mod disappear.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Components available in the desktop app
&lt;/h2&gt;

&lt;p&gt;Available UI components also differ by surface.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Terminal&lt;/th&gt;
&lt;th&gt;Desktop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Box&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt;, &lt;code&gt;Code&lt;/code&gt;, &lt;code&gt;Markdown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Svg&lt;/code&gt; (SVG graphics)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Client&lt;/code&gt; (a region drawn by your own separate file)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Raster&lt;/code&gt; (a grid of colored cells), &lt;code&gt;Image&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the desktop, &lt;code&gt;Svg&lt;/code&gt; is the most expressive component. You can draw charts and diagrams across the whole panel. Conversely, a mod built in the terminal with &lt;code&gt;Raster&lt;/code&gt; cannot draw on the desktop (Part 6).&lt;/p&gt;

&lt;h2&gt;
  
  
  How to turn mods off
&lt;/h2&gt;

&lt;p&gt;The official docs give three levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn off one mod&lt;/strong&gt;: disable or remove that plugin in the Installed tab of &lt;code&gt;/plugin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn off everything for one session&lt;/strong&gt;: start with &lt;code&gt;--safe-mode&lt;/code&gt; (other settings are disabled too)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn off everything permanently&lt;/strong&gt;: add &lt;code&gt;"disableAllHooks": true&lt;/code&gt; to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the desktop app, the official install page says you can disable a plugin from the plugin management screen (+ → Plugins → Manage plugins). A later article in this series comes back to this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look at real mods
&lt;/h2&gt;

&lt;p&gt;Seeing real mods is the fastest way to understand them. Anthropic keeps sample mods in &lt;code&gt;claude-code/mods&lt;/code&gt; of &lt;code&gt;anthropics/claude-code-playground&lt;/code&gt; (&lt;code&gt;token-weather&lt;/code&gt;, &lt;code&gt;blast-radius&lt;/code&gt;, &lt;code&gt;replay-theater&lt;/code&gt;, and others). Claude Code's own &lt;code&gt;/diff&lt;/code&gt; is built as a mod.&lt;/p&gt;

&lt;p&gt;To browse public mods on GitHub in one place, you can use the &lt;a href="https://modscode.com/" rel="noopener noreferrer"&gt;modscode gallery&lt;/a&gt;. It read 1,383 public mods on GitHub and lists the 49 (as of 2026-10-06) that target the desktop app, run on Windows, and passed review, each with a reproduction of its actual UI. A gentler explanation of the mods concept is in &lt;a href="https://modscode.com/guide/" rel="noopener noreferrer"&gt;What are mods&lt;/a&gt;. The official overview is at &lt;a href="https://code.claude.com/docs/en/plugins/mods/overview" rel="noopener noreferrer"&gt;code.claude.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this series is organized
&lt;/h2&gt;

&lt;p&gt;Parts 2 to 4 are the minimum steps to build one mod for the desktop app. Parts 5 to 10 answer commonly searched problems. If you have a specific symptom, start with that part.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;The minimal three files, and branching on &lt;code&gt;e.surface&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Building a pane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Band, status line, toast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Nothing is drawn and there is no error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;A mod built in the terminal breaks in the desktop app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;It does not work on Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Asking Claude from a button does not work the first time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Distributing and installing through a marketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Telling whether a mod is safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tested with Claude Code 2.1.286 to 2.1.288 (October 2026, Windows 11). Mods are brand new and change from version to version. Anything I could not confirm in the official docs is marked "not verified."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>javascript</category>
      <category>programming</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
