Skip to content

Adopt exec-form hook rows fleet-wide once upstream #90495 is ruled out on Windows #3686

Description

@kyle-sexton

Summary

Every shell-form hook row (no args key) is passed to a shell before its own interpreter starts: sh -c on macOS and Linux, Git Bash on Windows (hooks doc, "Exec form and shell form"). Exec form (an args array, even empty) spawns the executable directly with no shell and substitutes ${CLAUDE_PLUGIN_ROOT} into command and each args element as plain strings. On this fleet 51 of the 52 always-on rows measured by the hook-performance program are shell form; only context-budget's node row is exec form. That row was the cheapest per-tool-call hook in the final run (81 ms against 1,360 ms and up for the bash-scripted rows), though it also does the least work, so the shell spawn's share is unmeasured.

Why it is bigger than the shebang hop (#3684)

Two spawns disappear per fire, not one: the Git Bash bash.exe that shell form starts, and the env the shebang adds. A peer session's host measurement adds a third cost: C:\Program Files\Git\usr\bin\bash.exe carries a signer certificate that expired 2026-05-05 and msys-2.0.dll is unsigned, and Microsoft documents that Defender real-time protection scans every launch of an unsigned binary (learn.microsoft.com, defender-endpoint troubleshoot-performance-issues). That fits the steady MsMpEng share the peer observed; exec form on a signed interpreter avoids the scan too. The machine-side remedy (a Defender performance recording, then a narrow process exclusion or a Git for Windows update with a re-signed bash) is outside this repo.

What blocks it

The hook-performance program deferred exec form on purpose (its constraint 8): upstream anthropics/claude-code#90495, open, last updated 2026-08-29, reports that on Windows exec-form args are dropped and the hook is still routed through bash.exe, crashing on stdin. The program's phase 3 live probe under claude --debug-file did show the context-budget exec-form row firing and returning ask on this host, which is one data point against the bug reproducing here, but it never exercised a bash-scripted hook in exec form.

Ask

  1. A live probe on this Windows host: one throwaway hook row in exec form ("command": "bash", "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/x.sh"]) under claude --debug-file, confirming the args arrive and the script reads its stdin payload. That decides whether #90495 applies.
  2. If it works: a paired fan-out harness run (--runs 3, one host) on a representative row set, then a fleet sweep of hooks.json commands to exec form, one plugin per PR, every touched plugin bumped. Rows that need shell features (pipes, &&) stay shell form. The harness itself must learn to spawn exec-form rows the way Claude Code does, or its numbers will not reflect the change.
  3. If #90495 reproduces: subscribe to it and keep this issue open as the tracker.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: lowNice-to-have, cosmetic, or speculative; opportunistic.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions