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