Add Folder Trust Support To Hooks (#15325)

This commit is contained in:
Sehoon Shon
2025-12-22 11:46:38 -05:00
committed by GitHub
parent d6a2f1d670
commit dced409ac4
10 changed files with 188 additions and 23 deletions

View File

@@ -11,6 +11,8 @@ import { HookEventName, HookType } from './types.js';
import type { HookConfig } from './types.js';
import type { HookInput } from './types.js';
import type { Readable, Writable } from 'node:stream';
import type { Config } from '../config/config.js';
import { ConfigSource } from './types.js';
// Mock type for the child_process spawn
type MockChildProcessWithoutNullStreams = ChildProcessWithoutNullStreams & {
@@ -53,6 +55,7 @@ vi.stubGlobal('console', mockConsole);
describe('HookRunner', () => {
let hookRunner: HookRunner;
let mockSpawn: MockChildProcessWithoutNullStreams;
let mockConfig: Config;
const mockInput: HookInput = {
session_id: 'test-session',
@@ -65,7 +68,11 @@ describe('HookRunner', () => {
beforeEach(() => {
vi.resetAllMocks();
hookRunner = new HookRunner();
mockConfig = {
isTrustedFolder: vi.fn().mockReturnValue(true),
} as unknown as Config;
hookRunner = new HookRunner(mockConfig);
// Mock spawn with accessible mock functions
const mockStdoutOn = vi.fn();
@@ -100,6 +107,89 @@ describe('HookRunner', () => {
});
describe('executeHook', () => {
describe('security checks', () => {
it('should block project hooks in untrusted folders', async () => {
vi.mocked(mockConfig.isTrustedFolder).mockReturnValue(false);
const projectHookConfig: HookConfig = {
type: HookType.Command,
command: './hooks/test.sh',
source: ConfigSource.Project,
};
const result = await hookRunner.executeHook(
projectHookConfig,
HookEventName.BeforeTool,
mockInput,
);
expect(result.success).toBe(false);
expect(result.error?.message).toContain(
'Security: Blocked execution of project hook in untrusted folder',
);
expect(mockDebugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Security: Blocked execution'),
);
expect(spawn).not.toHaveBeenCalled();
});
it('should allow project hooks in trusted folders', async () => {
vi.mocked(mockConfig.isTrustedFolder).mockReturnValue(true);
const projectHookConfig: HookConfig = {
type: HookType.Command,
command: './hooks/test.sh',
source: ConfigSource.Project,
};
// Mock successful execution
mockSpawn.mockProcessOn.mockImplementation(
(event: string, callback: (code: number) => void) => {
if (event === 'close') {
setTimeout(() => callback(0), 10);
}
},
);
const result = await hookRunner.executeHook(
projectHookConfig,
HookEventName.BeforeTool,
mockInput,
);
expect(result.success).toBe(true);
expect(spawn).toHaveBeenCalled();
});
it('should allow non-project hooks even in untrusted folders', async () => {
vi.mocked(mockConfig.isTrustedFolder).mockReturnValue(false);
const systemHookConfig: HookConfig = {
type: HookType.Command,
command: './hooks/test.sh',
source: ConfigSource.System,
};
// Mock successful execution
mockSpawn.mockProcessOn.mockImplementation(
(event: string, callback: (code: number) => void) => {
if (event === 'close') {
setTimeout(() => callback(0), 10);
}
},
);
const result = await hookRunner.executeHook(
systemHookConfig,
HookEventName.BeforeTool,
mockInput,
);
expect(result.success).toBe(true);
expect(spawn).toHaveBeenCalled();
});
});
describe('command hooks', () => {
const commandConfig: HookConfig = {
type: HookType.Command,