Claude Code クロスプラットフォームフック
Claude Codeのクロスプラットフォームフック: .cmd・.sh・.ps1のラッパーを捨て、nodeを直接呼び出すことで、1つの.mjsファイルがmacOS・Linux・Windowsで動く方法。
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
問題: cmd /c や PowerShell 経由で Windows から配布したフックは、Linux チームメンバーがリポジトリを開いた瞬間にエラーになる。多くの人が行き着く回避策は見苦しい。フックごとに3つのシムスクリプトを用意する方法だ。Windowsには .cmd、Linuxには .sh、PowerShell には .ps1。3つとも全く同じことをする。つまり、本当に重要な .mjs ファイルを呼び出すだけだ。
素早い解決策: ラッパーを捨てる。フック設定を Node.js に直接向ける:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/formatter.mjs"
}
]
}
]
}
}変更なしで Windows、Linux、macOS で動く。node は常に $PATH に存在する。Claude Code が Node.js をハードな要件としているからだ。
OSが変わるとフックが壊れる理由
自分しか設定を触らない場合、この問題には遭遇しない。問題が生じるのは .claude/settings.json が共有されたとき、リポジトリが公開されたとき、あるいは Windows のデスクトップと macOS のノートパソコンを行き来するようになったときだ。コマンド内に bash や powershell が現れた瞬間、チームの半分が実行できなくなる。
多くのチュートリアルがプラットフォーム固有の書き方をしている:
// Windows-only
"command": "cmd /c \".claude\\hooks\\formatter.cmd\""
// Linux-only
"command": "bash .claude/hooks/formatter.sh"
// PowerShell-only
"command": "powershell -NoProfile -File .claude/hooks/statusline.ps1"これらのラッパーはどれも node 呼び出しの周りに2行のボイラープレートを追加しているだけだ。3つのファイル、3つのプラットフォーム、すべてで同じ仕事をしている。OSについて知っているレイヤーがシムだけなら、シムを削除できる。
settings.json のユニバーサルパターン
settings.json 内のフックは共通の形状を持つ:
{
"statusLine": {
"type": "command",
"command": "node .claude/hooks/statusline-monitor.mjs"
},
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/skill-activation.mjs"
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/backup.mjs",
"async": true
}
]
}
]
}
}cmd /c なし。bash なし。powershell なし。ただ node だけ。すべてのフック型が同じ形状を取るため、PostToolUse、SessionStart/SessionEnd、Stop、12すべてのライフサイクルイベントが同じように設定できる。
クロスプラットフォームフックロジックの3つのルール
各 .mjs ファイルの中で、3つの習慣がロジックをどのOSでも移植可能に保つ。
プラットフォーム変数の代わりに os.homedir() を使う
import { homedir } from "os";
import { join } from "path";
// Cross-platform: resolves to C:\Users\you or /home/you
const settingsPath = join(homedir(), ".claude", "settings.json");$HOME、$env:USERPROFILE、%USERPROFILE% をソースコードに書かない。ヘルパーに適切なものを選ばせる。
一時ファイルパスには os.tmpdir() を使う
import { tmpdir } from "os";
import { join } from "path";
// Cross-platform: resolves to C:\Users\you\AppData\Local\Temp or /tmp
const cacheFile = join(tmpdir(), "my-hook-cache.json");/tmp や $env:TEMP を手書きしない。
すべてのファイルパス構築に path.join() を使う
import { join } from "path";
// Cross-platform path construction
const logFile = join(".claude", "hooks", "logs", "hook.log");/ や \\ でパスを手動で連結するのをやめる。Node.js はフックが実行されるOSに応じた適切なセパレータをすでに知っている。
両プラットフォームをカバーするパーミッション
settings.json の permissions ブロックには Windows の名前と Unix の名前を並べて記述する:
{
"permissions": {
"allow": [
"Bash(where:*)",
"Bash(which:*)",
"Bash(tasklist:*)",
"Bash(ps:*)",
"Bash(taskkill:*)",
"Bash(kill:*)",
"Bash(findstr:*)",
"Bash(node:*)"
]
}
}現在のマシンにインストールされていないコマンドは静かに無視される。両方を記述してもコストはかからないし、フックは権限ダイアログを発生させることなく実際に存在するコマンドを選んで実行する。この側面のより詳細な自動化については、パーミッションフックガイドを参照してほしい。
完全な例: クロスプラットフォームファイルロガー
1つのフックをそのまま使える形で示す。仕事はファイルロガーだ。Claude が行った Write または Edit をすべてログに追記する:
#!/usr/bin/env node
import { readFileSync, appendFileSync, mkdirSync } from "fs";
import { join } from "path";
const logDir = join(".claude", "hooks", "logs");
mkdirSync(logDir, { recursive: true });
try {
const input = JSON.parse(readFileSync(0, "utf-8"));
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path || "unknown";
const timestamp = new Date().toISOString();
appendFileSync(
join(logDir, "file-changes.log"),
`${timestamp} | ${toolName} | ${filePath}\n`,
);
} catch {
// Silent fail -- don't block Claude
}
process.exit(0);settings.json に登録する:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/file-logger.mjs"
}
]
}
]
}
}Windows 11、Arch Linux、macOS Sequoia で同じ動作をする。ラッパーはゼロだ。
1つのOSで壊れるときのデバッグ
あるマシンでは動くがほかのマシンで失敗するフックがある?このリストを上から順に確認してほしい:
- ハードコードされたパスセパレータ。 すべての
.mjsファイルで、パス内に現れる/や\\を検索する。それらをpath.join()に渡す。 - 環境変数の参照。
process.env.HOME、process.env.USERPROFILE、process.env.TEMPを探し、それぞれをos.homedir()またはos.tmpdir()に置き換える。 - settings.json 内のシェル固有のコマンド。
bash、cmd、powershell、shに言及するcommandエントリはそのOS以外ではすべて壊れる。nodeに向け直す。
手動でフックを発火させると失敗を隠せない:
echo '{"tool_name":"Write","tool_input":{"file_path":"test.js"}}' | node .claude/hooks/your-hook.mjs
echo $? # Should output 0あるOSでは 0 が返り、ほかでは違う値が返る場合、問題は settings.json のフックエントリではなく .mjs ファイルのパス処理にある。
リリースチェックリスト
チームへの展開または公開リリース前にこのリストを確認してほしい:
settings.jsonのすべてのcommandがcmd、powershell、bashではなくnodeを指している- ホームディレクトリは
$HOMEや%USERPROFILE%ではなくos.homedir()から取得している - 一時パスは
/tmpや$env:TEMPではなくos.tmpdir()から取得している - パスは手打ちのセパレータではなく
path.join()で構築している - パーミッションに Windows と Unix の両方の同等コマンドが記述されている
statusLineのコマンドがpowershellではなくnodeを指している
1つのファイル。3つのプラットフォーム。メンテナンスなし。
Claude Code のフックは Windows で動くか?
動く。そのOSにしか存在しないシェルではなく node を通じて呼び出せば、Windows、Linux、macOS で同じように動作する。Node.js はすべてのプラットフォームで Claude Code のハードな要件なので、node は常に $PATH にある。settings.json に node .claude/hooks/your-hook.mjs を記述すれば、3つすべてで同一の動作が得られる。
フックに Node.js の代わりに Python を使えるか?
Python でも動く。ただし、チームメンバー全員がすでにインタープリターをマシンに持っている必要がある。command フィールドでは python ではなく python3 を使う。一部の Linux ディストリビューションには python というコマンドが存在しないからだ。Node.js の方が安全な選択だ。Claude Code はすべてのプラットフォームで Node.js を保証しているが、Python は保証していない。
プラットフォームをまたいだ改行コードの扱いは?
ほぼ何もしなくてよい。readFileSync と writeFileSync はすでに改行を正規化している。すべてのフックは stdin から JSON を読み込み、JSONパーサーは改行が CRLF か LF かを気にしない。例外は自分でシェルスクリプトを出力するフックだ。そのパスでは \n を書き、残りは Git の autocrlf 設定に任せる。
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。