feat(windows): wrap .wolf hook commands in wscript+VBS to hide console flash#42
Open
mann1x wants to merge 1 commit into
Open
feat(windows): wrap .wolf hook commands in wscript+VBS to hide console flash#42mann1x wants to merge 1 commit into
mann1x wants to merge 1 commit into
Conversation
…e flash
On Windows, Claude Code spawns each .claude/settings.json hook command via
its parent shell. The node.exe used by the bare `node "..."` form is a
console-subsystem binary, so even with CREATE_NO_WINDOW set by Claude Code's
spawn, a brief black console window flashes onscreen for every PostToolUse,
SessionStart, etc. fire — repeatedly throughout a session.
Wrap the command in `wscript //nologo "<vbs>" node "..."`. `wscript.exe` is
a windows-subsystem host, so the OS never allocates a console for it;
WScript.Shell.Run(cmd, 0, True) then invokes the underlying `node "..."`
with SW_HIDE and waits for completion, propagating the exit code so Claude
Code's hook contract is preserved.
POSIX hosts get the unchanged bare form — buildHookCommand falls back to
`node "$CLAUDE_PROJECT_DIR/.wolf/hooks/<x>.js"` when platform !== 'win32'
or the VBS asset isn't found.
Changes:
- assets/hook-runner.vbs — universal SW_HIDE wrapper (re-quotes args)
- scripts/copy-hook-runner.mjs — build step copies VBS to dist/assets/
- src/utils/hook-command.ts — buildHookCommand() + findHookRunnerVbs()
- src/cli/init.ts, src/cli/update.ts — call buildHookCommand() per hook
- package.json — build appends `node scripts/copy-hook-runner.mjs`
Existing projects need an `openwolf init` (or `openwolf update`) re-run for
the new command form to land in their .claude/settings.json; the
replaceOpenWolfHooks() matcher (".wolf/hooks/" substring) handles the
upgrade in place without touching user-authored hooks.
Companion to the Caliber wscript+VBS wrapper landed in
caliber-ai-org/ai-setup#fix/windows-hide-spawn — same approach, same VBS
shape, validated empirically on pandorum where zero flashes remained after
both wrappers were active.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Windows, every
.claude/settings.jsonhook that OpenWolf installs(
SessionStart,PreToolUse× 2,PostToolUse× 2,Stop— sixper project) currently uses the bare form:
When Claude Code fires the hook, it spawns this without
windowsHide: true.node.exeis a console-subsystem binary, soWindows allocates a fresh console window for every hook fire —
visible as a brief black flash. Under active editor use this stacks
to multiple flashes per second (every Edit/Write triggers
PreToolUse + PostToolUse). It steals focus from the editor and is
unusable in practice.
Anthropic acknowledged the upstream gap
(anthropics/claude-code#19012)
and closed it as not planned. The standard workaround — adopted
this PR — is a tiny VBS wrapper invoked via
wscript.exe.wscript.exeis a windows-subsystem host so the OS never allocatesa console for it;
WScript.Shell.Run(cmd, 0, True)then launchesthe underlying
node "..."withSW_HIDEand waits for completion,propagating the exit code so the Claude Code hook contract is
preserved.
The fix
A single new helper
buildHookCommand(scriptName)(insrc/utils/hook-command.ts) decides per-platform whether to wrap:Both
init.tsandupdate.tsroute their six hook-command stringsthrough this helper, so a
openwolf initoropenwolf updateafterthis lands rewrites existing projects'
.claude/settings.jsontothe wscript-wrapped form (replaceOpenWolfHooks() already filters by
.wolf/hooks/substring so the swap is in-place and idempotent).Cross-platform safety
buildHookCommandreturns the existing
node "..."shape whenprocess.platform !== 'win32'.same fallback — the helper returns the bare
node "..."shapeif
findHookRunnerVbs()can't locate the asset.openwolf init/openwolf updateswap; until then they keepflashing as today (no regression).
Files touched
assets/hook-runner.vbs0— SW_HIDE: never show a windowTrue— wait for completion, propagate exit codeC:\Program Files\nodejs\node.exe)survive the round-trip through
WScript.Arguments→ cmd stringscripts/copy-hook-runner.mjsTiny ESM build step that copies
assets/hook-runner.vbs→dist/assets/hook-runner.vbsnext to the compiled CLI. Already inthe
dist/glob shipped viapackage.jsonfilessonpm packpicks it up without further configuration.
src/utils/hook-command.tsTwo exports:
findHookRunnerVbs(): string | null— resolves the VBS path atruntime relative to the compiled module location (
dist/src/utils/→
dist/assets/)buildHookCommand(scriptName, platform?, vbsPath?)— emits theper-platform command string; both
platformandvbsPathareinjectable for tests
Verification
tscclean;node scripts/copy-hook-runner.mjscopiesthe VBS into
dist/assets/;npm packships it inside thetarball under the
dist/globbuildHookCommand('post-write.js', 'linux')returns the unchanged
node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"buildHookCommand('post-write.js')on the deployed install returns
wscript //nologo "C:/Users/manni/AppData/Roaming/npm/node_modules/openwolf/dist/assets/hook-runner.vbs" node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"openwolf initon three real Windows projects;.claude/settings.jsonnow contains the wscript-wrapped form forall six hooks per project; zero console flashes during an
extended Claude Code session, operator-confirmed
wrapper shape for Caliber's hook commands; the combined deploy on
pandorum eliminates every Windows-flash source observed during
the audit
Compatibility
replaceOpenWolfHooks()matches on.wolf/hooks/substring soit identifies both the old bare-
nodeand new wscript-wrappedshapes as OpenWolf-owned and rewrites in place — no stale
duplicates after upgrade
.wolf/hooks/) are preserved untouched, same as todayreplaceOpenWolfHookspath — they continue to pass because thehelper is platform-gated and they run on Linux
Status
Ready for review. The change is empirically validated end-to-end on
Windows 10 22H2 (pandorum, three real OpenWolf-managed projects,
extended Claude Code session — zero flashes from
.wolf/hooks).The companion Caliber PR
(caliber-ai-org/ai-setup#222)
ships the same VBS-wrapper shape for Caliber's hook commands and
has CI green across the 6-cell Linux/Windows × Node 20/22/25
matrix; combined deploy was the configuration that produced the
zero-flash result.