【ITニュース解説】Spawning a CLI from Node on Windows is not the same as on a Mac
2026年10月16日に「Dev.to」が公開したITニュース「Spawning a CLI from Node on Windows is not the same as on a Mac」について初心者にもわかりやすく解説しています。
ITニュース概要
Node.jsでWindowsからCLIを起動するとMacと異なり問題が多い。npm製CLIは`.cmd`シムで、Nodeの更新により`shell: true`がないと動かず、引数処理も複雑になる。`child.kill()`では子プロセスが残り、`taskkill`等で終了させる必要がある。Windowsでの動作確認は早期に行うべきだ。
ITニュース解説
システムエンジニアを目指す皆さんにとって、日々の開発作業でプログラミング言語を使ってさまざまなツールを連携させることは非常に一般的です。例えば、Node.jsやElectronといった環境で開発したアプリケーションが、Gitや独自のコード生成エージェントなど、他のコマンドラインツール(CLI)を起動して処理を進める場面は少なくありません。しかし、このようなツール連携において、開発環境のOS(オペレーティングシステム)によって予期せぬ挙動の違いが生じることがあります。特にWindowsとMacでは、同じコードでも異なる結果になるケースがあり、その一つが、Node.jsからコマンドラインツールを起動する際に発生する問題です。
まず、MacとWindowsでコマンドラインツールの「実体」が異なるという根本的な違いを理解することが重要です。Mac環境では、Node.jsのプログラムが spawn('claude', args) のように記述するだけで、多くの場合、指定されたコマンドラインツール(この例では「claude」というツール)を直接起動し、意図通りに動作させることができます。しかし、Windows環境では、npmなどのパッケージマネージャーを通じてインストールされる多くのコマンドラインツールは、直接実行できるプログラムファイル(例えば.exeファイル)として提供されるわけではありません。代わりに、それらは通常「.cmdファイル」という形式のスクリプト、いわゆる「シム(shim)」としてインストールされます。この.cmdファイルは、実際には別のJavaScriptファイルやバッチファイルを呼び出すための「仲介役」のようなものです。
この「仲介役」の存在が、Windowsでの問題の引き金となります。Node.jsには、外部プログラムを起動するための spawn 関数がありますが、2024年4月に行われたセキュリティアップデート(CVE-2024-27980)以降、Node.jsは、shell: true というオプションを指定しない限り、セキュリティ上の理由から直接.cmdファイルを起動することを拒否するようになりました。この変更自体はセキュリティを高めるためのものですが、これによって開発者は新たな課題に直面することになりました。
もし、このセキュリティ上の制約を回避するために shell: true オプションを使って.cmdファイルを起動すると、今度は別の問題が発生します。shell: true を指定すると、Node.jsは指定されたコマンドを直接実行するのではなく、Windowsのコマンドプロンプト(cmd.exe)を経由して実行します。この cmd.exe を通すことで、引数の扱いが非常に複雑になります。cmd.exe は独自のルールで引数を解釈するため、特殊文字やスペースを含む引数は、そのルールに従って適切に「エスケープ」する、つまり特殊文字を特殊な方法で記述し直す必要があります。このエスケープ処理を間違えると、引数が正しく渡されず、コマンドラインツールが期待通りに動作しません。さらに深刻な問題として、cmd.exe を通すと、引数の中に改行コードが含まれている場合、その改行が適切に処理されず、引数が途中で切れてしまうことがあります。例えば、複数の行からなる長いメッセージをツールに渡そうとしても、最初の行しか認識されないといった事態が発生するのです。
この問題の解決策の一つは、.cmdファイルが何を「仲介」しているのかを直接読み解くことです。つまり、.cmdファイルの中身を調べて、それが最終的にどのJavaScriptファイルをNode.jsで実行しようとしているのかを見つけ出し、そのJavaScriptファイルを node コマンドで直接起動するという方法です。これにより、cmd.exe を介さずに済むため、引数のエスケープや改行の問題を回避できます。また、一般的な.cmdファイルについては、cross-spawn と呼ばれるライブラリを使用する方法もあります。cross-spawn は、OSの違いを吸収し、cmd.exe 向けの適切な引数エスケープ処理を自動的に行ってくれるため、開発者はOSごとの細かい違いを意識せずに spawn 関数を使えるようになります。
次に、起動したコマンドラインツールを適切に終了させる際の問題です。Node.jsの spawn 関数でプログラムを起動すると、その起動されたプログラムを操作するための「子プロセス」を表すオブジェクトが返されます。通常、この子プロセスオブジェクトの kill() メソッドを呼び出せば、起動したプログラムを終了させることができます。しかし、Windowsで.cmdファイルを起動した場合、Node.jsが spawn するのは「仲介役」である cmd.exe そのものです。この cmd.exe が、さらに目的のコマンドラインツールを起動するという二段階のプロセスになります。そのため、child.kill() を使って cmd.exe を終了させても、その cmd.exe が起動した「本来の」コマンドラインツールは終了せず、バックグラウンドで動き続けてしまうという問題が発生します。これは、あたかも店の店員(cmd.exe)を帰らせても、店員が呼んだ配達員(本来のツール)が仕事を終えずにそのまま居座ってしまうようなものです。
この、バックグラウンドで動き続けるプロセスを確実に終了させるためには、より強力な手段が必要です。Windowsには taskkill というコマンドがあり、これを使うと特定のプロセスだけでなく、そのプロセスが起動した子プロセスまで含めた「プロセスツリー」全体を強制的に終了させることができます。具体的には、taskkill /pid <pid> /T /F のように、cmd.exe のプロセスID(PID)を指定し、/T オプションでプロセスツリー全体を、/F オプションで強制的に終了させます。これにより、意図せずバックグラウンドに残ってしまったツールを確実に停止させることが可能になります。
これらの複雑な問題を開発者一人ひとりが個別に解決しようとすると、コードの品質低下やバグの温床になりかねません。そのため、このプロジェクトでは、上記で説明したようなWindows特有の挙動に対応するための修正をすべて含んだ「ヘルパー関数」を作成し、Node.jsやElectronアプリケーション内で外部コマンドラインツールを起動する際は、必ずこのヘルパー関数を経由するようにしました。これにより、プロジェクトの誰もが直接 spawn 関数を呼び出す必要がなくなり、OS間の違いを意識せずにツール連携を安全に行えるようになります。
この経験は、システムエンジニアを目指す皆さんにとって非常に重要な教訓を教えてくれます。それは、Node.jsやElectronといった環境で、他のコマンドラインツールを起動するようなアプリケーションを開発する際には、開発の初期段階からWindows環境での動作検証を徹底することが極めて重要だということです。MacやLinux環境でのみテストを進めていると、Windows特有の環境要因によって、本番環境で予期せぬ問題が発生し、リリースが遅れたり、ユーザー体験が損なわれたりするリスクがあります。異なるOS環境での互換性とそのテストは、堅牢なソフトウェアを開発する上で不可欠な要素であり、特にシステムエンジニアとして多岐にわたる環境に対応する能力は非常に価値があります。