DEV Community

Haruki Nakada
Haruki Nakada

Posted on

Why a Claude Code mod doesn't work on Windows: tail, date, open, osascript in $.process and silent sound

Most mods written on Mac or Linux break on Windows because they use $.process to launch programs Windows doesn't have (tail, date, open, osascript, afplay, xdg-open and so on). $.process.run doesn't go through a shell, so cmd built-ins like start or dir can't be launched either. The other cause is sound: on Windows, $.audio.play plays nothing and still reports success.

Versions checked: Claude Code 2.1.284 to 2.1.288 (October 2026, Windows 11).

How common is this?

The modscode review read 1,383 public mods on GitHub and found 167 that use $.process 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.

$.process.run doesn't use a shell

The official API page says $.process.run takes an array of arguments and does not use a shell.

const r = await $.process.run(['git', 'status'])
// r is { exitCode, stdout, stderr }
Enter fullscreen mode Exit fullscreen mode

Because there's no shell, this happens:

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

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

Programs Windows doesn't have, and what to use instead

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

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

Programs that ship an executable with the same name on Windows, like git or gh, work as-is, as long as they're on the PATH.

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

Detecting the OS

There's no process inside a hook, so you can't use process.platform. I tell them apart by the shape of $.plugin.root (the plugin folder).

function isWindows($) {
  const root = String($.plugin.root ?? '')
  return /^[A-Za-z]:[\\/]/.test(root)   // starts with a drive letter
}
Enter fullscreen mode Exit fullscreen mode

Opening a file with the default app

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

async function openFile($, path) {
  if (isWindows($)) {
    await $.process.run(
      ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', 'Start-Process -FilePath $env:MOD_FILE'],
      { env: { MOD_FILE: path }, timeoutMs: 15_000 },
    )
    return
  }
  await $.process.run(['open', path])
}
Enter fullscreen mode Exit fullscreen mode

Start the open from a clock, not inside the button-press event. Processes started inside an event were sometimes killed when the event ended (part 8).

No sound: $.audio.play succeeds silently on Windows

await $.audio.play({ asset: 'sounds/success.wav' })   // Windows: plays nothing
Enter fullscreen mode Exit fullscreen mode

On Windows this plays nothing, throws nothing, and never reaches .catch. The Code tab in the desktop app runs the same executable, so it was silent there too.

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

Fix: launch PowerShell's SoundPlayer

On Windows only, use $.process.run to start PowerShell and play the WAV with System.Media.SoundPlayer.

const PS_PLAY = '(New-Object System.Media.SoundPlayer $env:MOD_WAV).PlaySync()'
let soundBusy = false

async function playSound($, clip) {
  const root = String($.plugin.root ?? '')
  if (!/^[A-Za-z]:[\\/]/.test(root)) return $.audio.play(clip)
  if (soundBusy) return                           // drop sounds that arrive while one is playing
  soundBusy = true
  try {
    const wav = `${root.replace(/[\\/]+$/, '')}\\${clip.asset.replace(/\//g, '\\')}`
    await $.process.run(
      ['powershell.exe', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', PS_PLAY],
      { env: { MOD_WAV: wav }, timeoutMs: 15_000 },
    )
  } catch {
    // don't stop the mod even if the sound fails
  } finally {
    soundBusy = false
  }
}
Enter fullscreen mode Exit fullscreen mode

Five things to watch

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

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 $.audio.play first.

The weight of launching PowerShell

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

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.

For a mod you publish, think in this order:

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

Small Windows differences

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

Summary

Symptom Cause Fix
A feature silently stops tail, date, open, osascript don't exist on Windows Replace with the mods API
start can't open files $.process.run doesn't go through a shell Call powershell.exe Start-Process explicitly
No sound (no exception) $.audio.play succeeds silently on Windows PowerShell SoundPlayer
Opened apps or sounds get cut off Processes are killed when the event ends Start them from a clock

To find out whether a mod works on Windows before installing it, search its code for $.process and look at the program names it launches. The 49 mods in the modscode reviewed gallery are only those whose code was checked to not launch programs missing on Windows.


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.

Top comments (0)