Summary
setupRepoSearchNudgeHook() in bin/install.cjs registers the hook using the shell-string command form. On Windows that costs 4-5 processes per invocation instead of 1, because Claude Code runs the string through a shell. The args exec form spawns the executable directly and removes the shell layer entirely.
The chain today
// bin/install.cjs
command: `node "${path.join(HOOKS_DIR, 'repo-search-nudge.js').replace(/\\/g, '/')}"`,
produces:
claude.exe
└─ bash.exe -c "node .../repo-search-nudge.js"
└─ bash.exe <- the -c re-exec, a second bash
└─ node.exe
└─ conhost.exe
Proposed change
{
type: 'command',
command: 'node',
args: [path.join(HOOKS_DIR, 'repo-search-nudge.js').replace(/\\/g, '/')],
timeout: 5,
}
Both bash layers disappear. Same stdin payload, same hook output contract, no change to repo-search-nudge.js itself.
Evidence
Verified on Windows 11 by registering a probe hook in the args form and having it walk its own parent chain:
psver=5.1.26100.8875 | chain=powershell.exe(16404) <- claude.exe(8004)
The interpreter's parent is claude.exe directly. No bash in the chain.
Context for why this is worth doing: on my box a full Claude Code hook config was generating ~334 process creations/min (~20k/hour). The dominant cost is not memory, it is that every spawn triggers a Defender image scan.
Caveats
args requires a real executable. node resolves to node.exe on Windows and a real binary on macOS/Linux, so this hook is safe. It would not work for .cmd/.bat shims such as npx, which need the shell form or an explicit node path/to/cli.js.
- No shell features. The exec form gets no tokenization, quoting, pipes,
&&, or variable expansion. This hook needs none of them. Worth noting because the quoting in the current template exists only to survive the shell, and becomes unnecessary.
docs/hook-authoring.md uses repo-search-nudge.js as the reference implementation, so if this lands it is probably worth documenting the exec form there as the default for hooks that invoke a real binary.
Not proposed (deliberately)
I looked at also adding an if filter to skip invocations that will no-op. In my history the hook fired 1,060 times and only 82 involved github.com, so ~92% of runs do nothing. But when I tried if gating on a different hook, a single command matched many patterns and spawned it 16 times concurrently, which was worse than not gating. I do not understand the multiplicity semantics well enough to recommend it yet, and the existing WebFetch|WebSearch matcher is already narrow. Flagging it as a possible follow-up, not part of this issue.
Why an issue rather than a PR
I had made this change locally and reverted it, since e-stack owns this file and local edits would be overwritten by the next bin/install.cjs --install. Raising it here so it can flow through the normal publish path.
Summary
setupRepoSearchNudgeHook()inbin/install.cjsregisters the hook using the shell-stringcommandform. On Windows that costs 4-5 processes per invocation instead of 1, because Claude Code runs the string through a shell. Theargsexec form spawns the executable directly and removes the shell layer entirely.The chain today
produces:
Proposed change
Both bash layers disappear. Same stdin payload, same hook output contract, no change to
repo-search-nudge.jsitself.Evidence
Verified on Windows 11 by registering a probe hook in the
argsform and having it walk its own parent chain:The interpreter's parent is
claude.exedirectly. No bash in the chain.Context for why this is worth doing: on my box a full Claude Code hook config was generating ~334 process creations/min (~20k/hour). The dominant cost is not memory, it is that every spawn triggers a Defender image scan.
Caveats
argsrequires a real executable.noderesolves tonode.exeon Windows and a real binary on macOS/Linux, so this hook is safe. It would not work for.cmd/.batshims such asnpx, which need the shell form or an explicitnode path/to/cli.js.&&, or variable expansion. This hook needs none of them. Worth noting because the quoting in the current template exists only to survive the shell, and becomes unnecessary.docs/hook-authoring.mdusesrepo-search-nudge.jsas the reference implementation, so if this lands it is probably worth documenting the exec form there as the default for hooks that invoke a real binary.Not proposed (deliberately)
I looked at also adding an
iffilter to skip invocations that will no-op. In my history the hook fired 1,060 times and only 82 involvedgithub.com, so ~92% of runs do nothing. But when I triedifgating on a different hook, a single command matched many patterns and spawned it 16 times concurrently, which was worse than not gating. I do not understand the multiplicity semantics well enough to recommend it yet, and the existingWebFetch|WebSearchmatcher is already narrow. Flagging it as a possible follow-up, not part of this issue.Why an issue rather than a PR
I had made this change locally and reverted it, since e-stack owns this file and local edits would be overwritten by the next
bin/install.cjs --install. Raising it here so it can flow through the normal publish path.