Skip to content

Hook install: use the args exec form to cut 4-5 processes per invocation to 1 #13

Description

@ElliotDrel

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions