fix(agent-core): hide console window when running hooks on Windows (#1466)

Pass windowsHide:true when spawning the hook process so a visible console
no longer flashes and steals focus on Windows. The Bash-tool path was
already hardened (KAOS buildLocalSpawnOptions); the hook runner missed the
flag even though its own taskkill helper already set it.

Extract the spawn options into a pure builder and add a regression test
asserting windowsHide, mirroring the existing KAOS spawn-options test.

Relates to #1298.
This commit is contained in:
liruifengv 2026-07-07 15:55:47 +08:00 committed by GitHub
parent 08b3c3fc53
commit 063bce2a2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 8 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix console windows flashing on Windows each time a hook runs.

View file

@ -1,4 +1,4 @@
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process';
import { z } from 'zod';
@ -11,6 +11,24 @@ export interface RunHookOptions {
readonly signal?: AbortSignal;
}
export function buildHookSpawnOptions(options: {
cwd?: string;
env?: Readonly<Record<string, string>>;
}): SpawnOptionsWithoutStdio {
return {
shell: true,
cwd: options.cwd,
stdio: 'pipe',
detached: process.platform !== 'win32',
// Hide the console Windows would otherwise allocate for the shell child.
// Without `windowsHide:true`, each hook flashes a visible console window —
// the same regression the Bash tool path already guards against in KAOS
// (see `buildLocalSpawnOptions`). Unconditional: it is a no-op on POSIX.
windowsHide: true,
env: options.env ? { ...process.env, ...options.env } : undefined,
};
}
const DEFAULT_TIMEOUT_SECONDS = 30;
const KILL_GRACE_MS = 100;
const OptionalStringSchema = z.preprocess(
@ -46,13 +64,7 @@ export async function runHook(
): Promise<HookResult> {
let child: ChildProcessWithoutNullStreams;
try {
child = spawn(command, {
shell: true,
cwd: options.cwd,
stdio: 'pipe',
detached: process.platform !== 'win32',
env: options.env ? { ...process.env, ...options.env } : undefined,
});
child = spawn(command, buildHookSpawnOptions({ cwd: options.cwd, env: options.env }));
} catch (error) {
return allowResult({ stderr: errorMessage(error) });
}

View file

@ -1,5 +1,7 @@
import { describe, expect, it } from 'vitest';
import { buildHookSpawnOptions } from '../../src/session/hooks/runner';
const RUNNER_MODULE = '../../src/session/hooks/runner' as string;
interface HookResult {
@ -98,3 +100,27 @@ describe('runHook process runner', () => {
expect(result.stdout?.trim()).toBe('WriteFile');
});
});
// Regression coverage for the "every hook flashes an empty console window on
// Windows" bug. With `shell:true` and no `windowsHide`, Node allocates a
// visible console for each hook child process on Windows. The fix is to pass
// `windowsHide:true` (mirrors KAOS' `buildLocalSpawnOptions` and the runner's
// own taskkill spawn). The flag is only observable on Windows, so we assert
// the spawn options builder directly.
describe('buildHookSpawnOptions (Windows console-window regression)', () => {
it('sets windowsHide:true so hooks do not flash a console on Windows', () => {
expect(buildHookSpawnOptions({}).windowsHide).toBe(true);
});
it('runs through the shell with stdio piped', () => {
const options = buildHookSpawnOptions({});
expect(options.shell).toBe(true);
expect(options.stdio).toBe('pipe');
});
it('merges hook env onto process.env and forwards cwd', () => {
const options = buildHookSpawnOptions({ cwd: '/repo', env: { FOO: 'bar' } });
expect(options.cwd).toBe('/repo');
expect(options.env).toMatchObject({ FOO: 'bar' });
});
});