Plattformübergreifende Hooks für Claude Code
Plattformübergreifende Claude Code Hooks: Vermeide .cmd-, .sh- und .ps1-Wrapper und rufe node direkt auf, damit eine .mjs-Datei auf macOS, Linux und Windows im Team funktioniert.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
Problem: Ein Hook, den du von Windows via cmd /c oder PowerShell ausgeliefert hast, leuchtet rot, sobald ein Linux-Teamkollege das Repo öffnet. Die Lösung, auf die die meisten landen, ist hässlich. Drei Shim-Skripte pro Hook: ein .cmd für Windows, ein .sh für Linux, ein .ps1 für PowerShell. Alle drei machen genau dasselbe: die .mjs-Datei aufrufen, die eigentlich zählt.
Schneller Gewinn: Wirf die Wrapper weg. Zeige die Hook-Konfiguration direkt auf Node.js:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/formatter.mjs"
}
]
}
]
}
}Läuft auf Windows, Linux und macOS ohne Änderungen. node ist immer im $PATH, weil Claude Code Node.js als feste Voraussetzung listet.
Warum Hooks brechen, wenn sich das OS ändert
Wenn niemand sonst dein Setup anfasst, beißt dich das nie. Ärger beginnt in dem Moment, wo .claude/settings.json geteilt wird, das Repo öffentlich wird oder du zwischen einem Windows-Tower und einem macOS-Laptop wechselst. Sobald bash oder powershell in einem Befehl auftaucht, kann die Hälfte des Teams ihn nicht ausführen.
Die meisten Tutorials gehen sowieso plattformspezifisch vor:
// 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"Jeder dieser Wrapper besteht aus zwei Zeilen Boilerplate um einen node-Aufruf. Drei Dateien, drei Plattformen, gleicher Job auf allen. Wenn die einzige Schicht, die das OS kennt, der Shim ist, kannst du den Shim löschen.
Das universelle Muster in settings.json
Hooks innerhalb von settings.json teilen eine Form:
{
"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
}
]
}
]
}
}Kein cmd /c. Kein bash. Kein powershell. Nur node. Jeder Hook-Typ nimmt dieselbe Form an, also PostToolUse, SessionStart/SessionEnd, Stop und alle 12 Lifecycle-Events werden identisch verdrahtet.
Drei Regeln für plattformübergreifende Hook-Logik
Innerhalb jeder .mjs-Datei halten drei Gewohnheiten die Logik portabel, egal welches OS sie ausführt.
Verwende os.homedir() statt Plattform-Variablen
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");Halte $HOME, $env:USERPROFILE und %USERPROFILE% aus dem Quellcode. Lass den Helper die richtige wählen.
Verwende os.tmpdir() für temporäre Dateipfade
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");Greif nicht von Hand nach /tmp oder $env:TEMP.
Verwende path.join() für alle Dateipfad-Konstruktionen
import { join } from "path";
// Cross-platform path construction
const logFile = join(".claude", "hooks", "logs", "hook.log");Hör auf, Pfade mit / oder \\ von Hand zusammenzukleben. Node.js kennt bereits den richtigen Separator für das OS, auf dem der Hook landet.
Berechtigungen, die beide Plattformen abdecken
Der permissions-Block in settings.json sollte den Windows-Namen und den Unix-Namen nebeneinander enthalten:
{
"permissions": {
"allow": [
"Bash(where:*)",
"Bash(which:*)",
"Bash(tasklist:*)",
"Bash(ps:*)",
"Bash(taskkill:*)",
"Bash(kill:*)",
"Bash(findstr:*)",
"Bash(node:*)"
]
}
}Welcher Befehl auch immer nicht auf dem aktuellen Rechner installiert ist, bleibt still. Beide aufzulisten kostet nichts, und dein Hook nimmt den, der tatsächlich existiert, ohne einen Berechtigungs-Dialog auszulösen. Für tiefere Automatisierung auf dieser Seite, lies den Permission-Hook-Leitfaden.
Vollständiges Beispiel: Plattformübergreifender Datei-Logger
Ein vollständiger Hook, portabel wie er ist. Die Aufgabe ist ein Datei-Logger. Jeder Write oder Edit, den Claude ausführt, wird an ein Log angehängt:
#!/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);Registriere ihn in deiner settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/file-logger.mjs"
}
]
}
]
}
}Gleiches Verhalten auf Windows 11, Arch Linux und macOS Sequoia. Null Wrapper.
Debuggen, wenn ein OS bricht
Hast du einen Hook, der auf einem Rechner läuft und auf dem nächsten fehlschlägt? Geh diese Liste von oben nach unten durch:
- Hardcodierte Pfad-Trennzeichen. Durchsuche jede
.mjs-Datei nach/oder\\innerhalb eines Pfades. Übergib diese Fälle anpath.join(). - Umgebungsvariablen-Referenzen. Suche nach
process.env.HOME,process.env.USERPROFILEoderprocess.env.TEMPund tausche jeden gegenos.homedir()oderos.tmpdir()aus. - Shell-spezifische Befehle in settings.json. Jeder
command-Eintrag, derbash,cmd,powershellodersherwähnt, bricht überall außer seinem Heim-OS. Zeige es aufnode.
Führe den Hook manuell aus, damit der Fehler sich nicht verstecken kann:
echo '{"tool_name":"Write","tool_input":{"file_path":"test.js"}}' | node .claude/hooks/your-hook.mjs
echo $? # Should output 0Eine 0 auf einem OS und etwas anderes auf dem nächsten zeigt auf die Pfad-Behandlung in der .mjs-Datei, nie auf den Hook-Eintrag in settings.
Release-Checkliste
Geh diese Liste durch, bevor du ein Team-Rollout oder ein öffentliches Release machst:
- Jeder
commandinsettings.jsonzeigt aufnode, nicht aufcmd,powershelloderbash - Home-Verzeichnisse kommen aus
os.homedir(), nie aus$HOMEoder%USERPROFILE% - Temporäre Pfade kommen aus
os.tmpdir(), nie aus/tmpoder$env:TEMP - Pfade werden mit
path.join()gebaut, nicht mit hand-getippten Trennzeichen - Berechtigungen listen sowohl Windows- als auch Unix-Äquivalente auf
- Der
statusLine-Befehl löst sich zunodeauf, nicht zupowershell
Eine Datei. Drei Plattformen. Kein Aufwand.
Funktionieren Claude Code Hooks auf Windows?
Ja. Ruf sie über node auf statt über eine Shell, die nur auf einem OS existiert, und sie laufen gleich auf Windows, Linux und macOS. Node.js ist eine feste Voraussetzung von Claude Code auf jeder Plattform, also ist node immer im $PATH. Schreib node .claude/hooks/dein-hook.mjs in settings.json und du bekommst identisches Verhalten auf allen dreien.
Kann ich Python statt Node.js für Hooks verwenden?
Python funktioniert auch, vorausgesetzt jeder Teamkollege hat bereits einen Interpreter auf seinem Rechner. Greif im command-Feld auf python3 statt python, weil bestimmte Linux-Distributionen nie ein einfaches python mitliefern. Node.js bleibt die sicherere Wahl: Claude Code verspricht es auf jeder Plattform, Python tut das nicht.
Wie gehe ich mit Zeilenenden auf verschiedenen Plattformen um?
Meistens gar nicht. readFileSync und writeFileSync normalisieren die Zeilenenden bereits. Jeder Hook liest JSON von stdin, und der JSON-Parser ist egal ob die Zeilenumbrüche CRLF oder LF sind. Die Ausnahme ist jeder Hook, der sein eigenes Shell-Skript ausgibt. Für diesen Fall schreib \n und überlass den Rest Gits autocrlf-Einstellung.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.