v3/implementation/adrs/ADR-062-cross-platform-hook-commands-2026-03.md
Date: 2026-03-05
Status: Accepted
Context: Settings.json hook commands failed on Windows due to node -e quoting issues.
The hookCmd() function in settings-generator.ts generated complex node -e "..." one-liners for hook commands:
node -e "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/hook-handler.cjs');process.argv.splice(1,0,s);require(s)" pre-bash
This broke on Windows because:
node -e arguments are treated as literal characters, not JavaScript string delimiters. Parentheses in catch(e), .trim(), process.cwd() may be interpreted as cmd.exe grouping operators in certain contexts.$ variables and escape characters.execSync shell commands in a fragile chain.Replace node -e "..." one-liners with direct script invocation:
// Before (broken on Windows):
function hookCmd(script: string, subcommand: string): string {
const scriptLiteral = `'${script}'`;
const resolver = [
"var c=require('child_process'),p=require('path'),r;",
"try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}",
'catch(e){r=process.cwd()}',
`var s=p.join(r,${scriptLiteral});`,
'process.argv.splice(1,0,s);',
'require(s)',
].join('');
return `node -e "${resolver}" ${subcommand}`.trim();
}
// After (works on all platforms):
function hookCmd(script: string, subcommand: string): string {
return `node ${script} ${subcommand}`.trim();
}
Generated command example:
node .claude/helpers/hook-handler.cjs pre-bash
The node -e one-liner existed to resolve the git root at runtime, ensuring hooks worked regardless of CWD. However, Claude Code always runs hooks from the project root directory, making git-root resolution redundant. Direct invocation with relative paths works identically on Windows, macOS, and Linux.
Added detectPlatform() integration to generateSettings(). The detected platform (OS, architecture, shell type) is now stored in claudeFlow.platform within settings.json:
{
"claudeFlow": {
"platform": {
"os": "windows",
"arch": "x64",
"shell": "powershell"
}
}
}
Platform detection was already implemented in types.ts (PlatformInfo interface, detectPlatform() function) but was not being used in settings generation.
All 12 hook types, auto-memory, learning bridge, memory graph, agent scopes, neural training, PreCompact context preservation, Stop sync, statusline, and all 10 daemon workers remain fully functional. Only the invocation mechanism changed.
| Before | After |
|---|---|
node -e "var c=require('child_process')..." pre-bash | node .claude/helpers/hook-handler.cjs pre-bash |
node -e "...import(u.pathToFileURL(f)...)" import | node .claude/helpers/auto-memory-hook.mjs import |
node -e "..." (statusline) | node .claude/helpers/statusline.cjs |
| File | Change |
|---|---|
src/init/settings-generator.ts | Simplified hookCmd() and hookCmdEsm() to direct invocation; imported detectPlatform(); added platform info to generated settings |
init --force to regenerate settings.json with fixed commands