Conversation
…on-blocking Windows console children (cmd / bash) allocate a visible console window when spawned by the desktop app, so every hook run flashed a black box; and inline network pulls could exceed short timeouts. ZCode entries now launch through wscript.exe (GUI subsystem, present in System32) running a teamai-hook-dispatch.vbs written next to config.json: the VBS runs the dispatch hidden (window style 0) and does not wait for it, so session start stays instant. The payload travels verbatim as a single argument, preserving the manifest command invariant. POSIX entries keep bash -lc.
jeff-r2026
left a comment
There was a problem hiding this comment.
Thanks for chasing the console flash — wscript.exe is the right primitive for a GUI-subsystem launcher on Windows. CI is green and the branch is mergeable, but as it stands I don't think this can land: the implementation is narrower than the PR description in one place and wider in another.
The two blockers
- The
process.platform === 'win32'branch is gone, sowscript.exeis written on every platform. The description says POSIX keepsbash -lc; the code doesn't. Existing macOS/Linux entries get replaced on the next reconcile. - The VBS launcher drops STDIN and makes all four events fire-and-forget, which breaks the payload contract
hook-dispatchrelies on and removes the effect of the per-eventtimeoutMstable. Details inline.
Smaller things worth folding in
removeAllnever deletesteamai-hook-dispatch.vbs, so uninstall leaves it behind.- The
zcodeEntryCommanddoc comment ("Both variants (posix bash -lc / win32 cmd /c)") no longer describes the code. - Behavior change isn't reflected in the docs.
docs/usage-guide.mdstill says hook entries use "bash -lc <dispatch>as an argv vector", and the zh-CN guide has the matching sentence. Per the repo rules both need updating in the same PR.
Suggested validation after the rework
On Windows: SessionStart auto-pull, plus a PostToolUse event with a Skill / TodoWrite matcher to confirm tool_name actually arrives. On macOS or Linux: confirm the written entry is still bash -lc and that reconcile stays idempotent across the platform change.
| // managed-entry detection and the manifest keep one command representation. | ||
| const entry: ZcodeHookEntry = { | ||
| type: 'process', | ||
| command: 'wscript.exe', |
There was a problem hiding this comment.
This writes wscript.exe unconditionally — the process.platform === 'win32' branch that used to pick cmd vs bash is gone, which contradicts the "POSIX (macOS/Linux) entries keep bash -lc" line in the PR description.
On macOS/Linux the next reconcile will swap the existing bash -lc entries for this one: the payload still lives in args[1], so isManaged recognizes them as managed and rewrites them. ZCode then tries to spawn a launcher that doesn't exist on those platforms and every hook stops firing.
The Ubuntu and macOS CI jobs don't catch this because the test was changed to assert wscript.exe on all platforms too. Could you restore the platform branch and keep wscript.exe scoped to win32?
| [ | ||
| "' TeamAI hook dispatcher - hidden, fire-and-forget (no console window).", | ||
| 'Set sh = CreateObject("WScript.Shell")', | ||
| 'If WScript.Arguments.Count > 0 Then sh.Run "cmd /c " & WScript.Arguments(0), 0, False', |
There was a problem hiding this comment.
Three concerns with this line.
STDIN is dropped. WScript.Shell.Run starts a fresh process with no inherited stdin pipe, so whatever ZCode writes to wscript.exe never reaches teamai. hook-dispatch reads its payload from STDIN JSON (cwd, tool_name, tool_input, prompt). SessionStart may still look fine because the pull handler can fall back to the child's cwd — which matches what the dogfood run observed — but PostToolUse track / todowrite-hint no-op without tool_name, UserPromptSubmit loses prompt, and any Stop-time hint can't reach the host. Spooling the payload to a temp file and redirecting it into the command would keep the contract intact.
Wait=False is applied too broadly. All four events become fire-and-forget, not just SessionStart. The pull handler is already registered with background: true in the dispatcher, so it doesn't need this; meanwhile the ZCODE_TIMEOUT_MS table above (SessionStart 180s, Stop 60s, …) no longer bounds anything, since the launcher returns immediately regardless.
The payload isn't escaped. "cmd /c " & WScript.Arguments(0) concatenates the command straight into cmd. Built-in dispatch commands survive it, but team hooks are arbitrary strings — the test suite itself uses sh /tmp/audit.sh — and one &, >, or embedded quote gets re-split by cmd. Arguments(0) needs cmd-level quoting.
| const expanded = expandHome(settingsPath); | ||
| await ensureDir(path.dirname(expanded)); | ||
| const vbsPath = path.join(path.dirname(expanded), 'teamai-hook-dispatch.vbs'); | ||
| if (!opts.removeAll) { |
There was a problem hiding this comment.
Two minor points on the write itself:
- The
!opts.removeAllguard means uninstall skips the write, but nothing ever deletesteamai-hook-dispatch.vbs—removeAllshould remove it so teardown leaves no stray file next toconfig.json. - This rewrites the VBS on every reconcile, even when the content is identical. The rest of this function is careful to only write on change (the
changedflag); worth matching that so a no-op sync doesn't touch the file's mtime.
| // wscript.exe is a GUI-subsystem binary — hook runs never flash a | ||
| // console window, and the hidden VBS launcher keeps the session | ||
| // start non-blocking even while the dispatch pulls over the network. | ||
| expect(hook.command).toBe('wscript.exe'); |
There was a problem hiding this comment.
With the platform conditional removed here, the macOS and Ubuntu CI jobs no longer assert anything about the POSIX entry shape — which is why the regression above passes CI. If the win32 branch comes back in toZcodeEntry, this assertion should branch with it.
The PR's Validation section also mentions a "VBS launcher existence" assertion, but I don't see one in the diff; a check that teamai-hook-dispatch.vbs is written next to config.json (and removed by removeAll) would be a good addition.
…t launcher Aligns the test assertions and zcodeEntryCommand extraction with the wscript launcher argv (vbsPath, mode, payload). The explicitly configured timeout (team hooks.yaml / builtin.overrides) now wins over the per-event table, and the table is the per-event default otherwise.
Summary
wscript.exe(GUI subsystem) running a smallteamai-hook-dispatch.vbswritten next toconfig.json— the VBS executes the dispatchhidden (window style 0) and fire-and-forget
never block the session
Context: why cmd / bash entries are problematic for ZCode
ZCode desktop (Windows) spawns
process-type hook entries directly:cmd /c "..."allocates a visible console window when spawned by the desktop app — everyhook run flashed a black box over the desktop (reported by our team during dogfooding)
bashis not safe either: CreateProcess resolves it to System32's WSL launcher beforeany PATH directory, and the WSL side has a different
$HOME(no teamai state) and often noNode ≥ 20 — the hook then no-ops or dies silently
wscript.exeis a GUI-subsystem binary that is always present, so it is the only reliablelauncher on Windows
Design
type/command/args/timeoutMs); the payloadtravels verbatim as a single argument (
args[1]), so managed-entry detection and themanaged-hooks manifest keep the one-command-representation invariant introduced for team hooks
config.json(one per scope) and is idempotent —re-injection does not duplicate it
SessionStart180s, others 30–60s) still bound the runnerbash -lc; there is no console-window concept therefollow-up once its storage/format is documented
Validation
npx vitest run src/__tests__/zcode.test.ts— 11/11 passed (shape assertions updated:wscript.exe+ payload arg, VBS launcher existence, matcher rules)npm run typecheck,npm run buildpasshooks inject, session-start auto-pull completedthrough the hidden launcher (
teamai statuslast-pull refreshed), zerohook.run.failedentries for the wscript entries, second sync byte-identical (idempotent)
Boundary
wscript.exeships with Windows; POSIX keepsbash)storage/format is documented
Type of Change
Test Plan
npx tsc --noEmitpassesnpx vitest runpassesRelated Issues
Windows desktop