Hooks Multiplataforma para o Claude Code
Hooks do Claude Code multiplataforma: elimina wrappers .cmd, .sh e .ps1 e invoca node diretamente para que um único ficheiro .mjs corra em macOS, Linux e Windows por toda a equipa.
Pare de configurar. Comece a construir.
Templates SaaS com orquestração de IA.
Problema: Um hook que enviaste do Windows via cmd /c ou PowerShell fica vermelho no momento em que um colega Linux abre o repositório. A solução que a maioria das pessoas usa é feia. Três scripts shim por hook: um .cmd para Windows, um .sh para Linux, um .ps1 para PowerShell. Os três fazem exatamente a mesma coisa, que é chamar o ficheiro .mjs que realmente importa.
Solução Rápida: Deita os wrappers fora. Aponta a configuração do hook diretamente para o Node.js:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/formatter.mjs"
}
]
}
]
}
}Corre em Windows, Linux e macOS sem alterações. node está sempre no $PATH porque o Claude Code lista o Node.js como requisito obrigatório.
Por Que os Hooks Partem Quando o OS Muda
Se mais ninguém tocar no teu setup, nada disto te afeta. O problema começa no momento em que .claude/settings.json é partilhado, o repositório se torna público, ou começas a alternar entre um PC Windows e um portátil macOS. Logo que bash ou powershell apareça dentro de um comando, metade da equipa não consegue executá-lo.
A maioria dos tutoriais vai logo para plataformas específicas de qualquer forma:
// 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"Cada um desses wrappers são duas linhas de boilerplate à volta de uma chamada node. Três ficheiros, três plataformas, mesmo trabalho em todos. Se a única camada que sabe sobre o OS é o shim, podes apagar o shim.
O Padrão Universal no settings.json
Os hooks dentro de settings.json partilham uma forma única:
{
"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
}
]
}
]
}
}Sem cmd /c. Sem bash. Sem powershell. Apenas node. Cada tipo de hook tem a mesma forma, por isso PostToolUse, SessionStart/SessionEnd, Stop, e todos os 12 eventos do ciclo de vida são configurados de forma idêntica.
Três Regras para Lógica de Hook Multiplataforma
Dentro de cada ficheiro .mjs, três hábitos mantêm a lógica portável independentemente do OS que a corra.
Usa os.homedir() em Vez de Variáveis de Plataforma
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");Mantém $HOME, $env:USERPROFILE, e %USERPROFILE% fora do código. Deixa o helper escolher o certo.
Usa os.tmpdir() para Caminhos de Ficheiros Temporários
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");Não uses /tmp ou $env:TEMP à mão.
Usa path.join() para Toda a Construção de Caminhos de Ficheiro
import { join } from "path";
// Cross-platform path construction
const logFile = join(".claude", "hooks", "logs", "hook.log");Para de juntar caminhos com / ou \\ à mão. O Node.js já sabe o separador certo para qualquer OS onde o hook corra.
Permissões que Cobrem Ambas as Plataformas
O bloco permissions no settings.json deve ter o nome Windows e o nome Unix lado a lado:
{
"permissions": {
"allow": [
"Bash(where:*)",
"Bash(which:*)",
"Bash(tasklist:*)",
"Bash(ps:*)",
"Bash(taskkill:*)",
"Bash(kill:*)",
"Bash(findstr:*)",
"Bash(node:*)"
]
}
}Seja qual for o comando não instalado na máquina atual, fica em silêncio. Listar ambos não tem custo, e o teu hook escolhe o que realmente existe sem acionar um diálogo de permissão. Para automação mais profunda neste lado, consulta o guia de Permission Hook.
Exemplo Completo: Logger de Ficheiros Multiplataforma
Um hook completo, portável tal como está. O trabalho é um logger de ficheiros. Qualquer escrita ou edição que o Claude faça é adicionada a um log:
#!/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);Regista-o no teu settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/file-logger.mjs"
}
]
}
]
}
}Mesmo comportamento em Windows 11, Arch Linux e macOS Sequoia. Zero wrappers.
Depuração Quando um OS Parte
Tens um hook que corre numa máquina e falha na seguinte? Percorre esta lista de cima para baixo:
- Separadores de caminho hardcoded. Pesquisa em todos os ficheiros
.mjspor/ou\\a aparecer dentro de um caminho. Passa esses casos parapath.join(). - Referências a variáveis de ambiente. Procura
process.env.HOME,process.env.USERPROFILE, ouprocess.env.TEMPe substitui cada um poros.homedir()ouos.tmpdir(). - Comandos específicos de shell no settings.json. Qualquer entrada
commandque mencionebash,cmd,powershell, oushparte em todo o lado exceto no seu OS de origem. Aponta paranode.
Dispara o hook manualmente para que a falha não se esconda:
echo '{"tool_name":"Write","tool_input":{"file_path":"test.js"}}' | node .claude/hooks/your-hook.mjs
echo $? # Should output 0Um 0 num OS e outra coisa no seguinte aponta-te para o tratamento de caminhos no ficheiro .mjs, nunca para a entrada de hook no settings.
Checklist de Lançamento
Percorre esta lista antes de um rollout de equipa ou lançamento público:
- Cada
commandnosettings.jsonaponta paranode, nãocmd,powershell, oubash - Os diretórios home vêm de
os.homedir(), nunca de$HOMEou%USERPROFILE% - Os caminhos temporários vêm de
os.tmpdir(), nunca de/tmpou$env:TEMP - Os caminhos são construídos com
path.join(), não com separadores escritos à mão - As permissões listam tanto os equivalentes Windows como Unix
- O comando
statusLineresolve paranode, não parapowershell
Um ficheiro. Três plataformas. Zero manutenção.
Os hooks do Claude Code funcionam no Windows?
Sim. Chama-os através de node em vez de uma shell que só existe num OS, e correm igual em Windows, Linux e macOS. Node.js é um requisito obrigatório do Claude Code em todas as plataformas, por isso node está sempre no $PATH. Coloca node .claude/hooks/your-hook.mjs no settings.json e tens comportamento idêntico nas três.
Posso usar Python em vez de Node.js para hooks?
Python também funciona, desde que todos os colegas já tenham um interpretador na máquina. No campo command, usa python3 em vez de python, porque certas distribuições Linux nunca incluem um python simples. Node.js continua a ser a escolha mais segura: o Claude Code garante-o em todas as plataformas; Python não.
Como lidar com os finais de linha entre plataformas?
Na maior parte não precisas. readFileSync e writeFileSync já normalizam os finais. Cada hook lê JSON do stdin, e o parser JSON não se importa se as newlines são CRLF ou LF. A exceção é qualquer hook que emita o seu próprio script de shell. Para esse caso, escreve \n e deixa o resto para a configuração autocrlf do Git.
Pare de configurar. Comece a construir.
Templates SaaS com orquestração de IA.