diff --git a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts
index fcc9b7543b..e9bc2b7f8f 100644
--- a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts
+++ b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts
@@ -34,6 +34,7 @@ import {
isStrictlyApproved,
} from './commandSafety.js';
import { verifySandboxOverrides } from '../utils/commandUtils.js';
+import { parseWindowsSandboxDenials } from './windowsSandboxDenialUtils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -66,8 +67,8 @@ export class WindowsSandboxManager implements SandboxManager {
return isDangerousCommand(args);
}
- parseDenials(_result: ShellExecutionResult): ParsedSandboxDenial | undefined {
- return undefined; // TODO: Implement Windows-specific denial parsing
+ parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined {
+ return parseWindowsSandboxDenials(result);
}
/**
diff --git a/packages/core/src/sandbox/windows/windowsSandboxDenialUtils.test.ts b/packages/core/src/sandbox/windows/windowsSandboxDenialUtils.test.ts
new file mode 100644
index 0000000000..93e479a11d
--- /dev/null
+++ b/packages/core/src/sandbox/windows/windowsSandboxDenialUtils.test.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import { parseWindowsSandboxDenials } from './windowsSandboxDenialUtils.js';
+import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
+
+describe('parseWindowsSandboxDenials', () => {
+ it('should detect CMD "Access is denied" and extract paths', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output: 'Access is denied.\r\n',
+ error: new Error('Command failed: dir C:\\Windows\\System32\\config'),
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ expect(parsed?.filePaths).toContain('C:\\Windows\\System32\\config');
+ });
+
+ it('should detect PowerShell "Access to the path is denied"', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output:
+ "Set-Content : Access to the path 'C:\\test.txt' is denied.\r\nAt line:1 char:1\r\n",
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ expect(parsed?.filePaths).toContain('C:\\test.txt');
+ });
+
+ it('should detect Node.js EPERM on Windows', () => {
+ const parsed = parseWindowsSandboxDenials({
+ error: {
+ message:
+ "Error: EPERM: operation not permitted, open 'D:\\project\\file.ts'",
+ },
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ expect(parsed?.filePaths).toContain('D:\\project\\file.ts');
+ });
+
+ it('should detect network denial (EACCES)', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output: 'Error: listen EACCES: permission denied 0.0.0.0:3000',
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ expect(parsed?.network).toBe(true);
+ });
+
+ it('should detect native Windows error code 0x80070005', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output: 'HRESULT: 0x80070005',
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ // No path in output, but recognized as denial
+ });
+
+ it('should handle extended-length paths', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output: 'Access is denied to \\\\?\\C:\\Very\\Long\\Path\\file.txt',
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ expect(parsed?.filePaths).toContain(
+ '\\\\?\\C:\\Very\\Long\\Path\\file.txt',
+ );
+ });
+
+ it('should detect Windows paths with forward slashes', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output:
+ "Error: EPERM: operation not permitted, open 'C:/project/file.ts'",
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeDefined();
+ expect(parsed?.filePaths).toContain('C:/project/file.ts');
+ });
+
+ it('should return undefined if no denial detected', () => {
+ const parsed = parseWindowsSandboxDenials({
+ output:
+ 'Directory of C:\\Users\r\n03/26/2026 11:40 AM
.',
+ } as unknown as ShellExecutionResult);
+
+ expect(parsed).toBeUndefined();
+ });
+});
diff --git a/packages/core/src/sandbox/windows/windowsSandboxDenialUtils.ts b/packages/core/src/sandbox/windows/windowsSandboxDenialUtils.ts
new file mode 100644
index 0000000000..a2b12b0336
--- /dev/null
+++ b/packages/core/src/sandbox/windows/windowsSandboxDenialUtils.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { type ParsedSandboxDenial } from '../../services/sandboxManager.js';
+import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
+
+/**
+ * Windows-specific sandbox denial detection.
+ * Extracts paths from "Access is denied" and related errors.
+ */
+export function parseWindowsSandboxDenials(
+ result: ShellExecutionResult,
+): ParsedSandboxDenial | undefined {
+ const output = result.output || '';
+ const errorOutput = result.error?.message;
+ const combined = (output + ' ' + (errorOutput || '')).toLowerCase();
+
+ const isFileDenial = [
+ 'access is denied',
+ 'access to the path',
+ 'unauthorizedaccessexception',
+ '0x80070005',
+ 'eperm: operation not permitted',
+ ].some((keyword) => combined.includes(keyword));
+
+ const isNetworkDenial = [
+ 'eacces: permission denied',
+ 'an attempt was made to access a socket in a way forbidden by its access permissions',
+ // 10013 is WSAEACCES
+ '10013',
+ ].some((keyword) => combined.includes(keyword));
+
+ if (!isFileDenial && !isNetworkDenial) {
+ return undefined;
+ }
+
+ const filePaths = new Set();
+
+ // Regex for Windows absolute paths (e.g., C:\Path or \\?\C:\Path)
+ // Handles drive letters and potentially quoted paths.
+ // We use two passes: one for quoted paths (which can contain spaces)
+ // and one for unquoted paths (which end at common separators).
+
+ // 1. Quoted paths: 'C:\Foo Bar' or "C:\Foo Bar"
+ const quotedRegex = /['"]((?:\\\\(?:\?|\.)\\)?[a-zA-Z]:[\\/][^'"]+)['"]/g;
+ for (const match of output.matchAll(quotedRegex)) {
+ filePaths.add(match[1]);
+ }
+ if (errorOutput) {
+ for (const match of errorOutput.matchAll(quotedRegex)) {
+ filePaths.add(match[1]);
+ }
+ }
+
+ // 2. Unquoted paths or paths in PowerShell error format: PermissionDenied: (C:\path:String)
+ const generalRegex =
+ /(?:^|[\s(])((?:\\\\(?:\?|\.)\\)?[a-zA-Z]:[\\/][^"'\s()<>|?*]+)/g;
+ for (const match of output.matchAll(generalRegex)) {
+ // Clean up trailing colon which might be part of the error message rather than the path
+ let p = match[1];
+ if (p.endsWith(':')) p = p.slice(0, -1);
+ filePaths.add(p);
+ }
+ if (errorOutput) {
+ for (const match of errorOutput.matchAll(generalRegex)) {
+ let p = match[1];
+ if (p.endsWith(':')) p = p.slice(0, -1);
+ filePaths.add(p);
+ }
+ }
+
+ return {
+ network: isNetworkDenial || undefined,
+ filePaths: filePaths.size > 0 ? Array.from(filePaths) : undefined,
+ };
+}
diff --git a/packages/core/src/services/sandboxManager.test.ts b/packages/core/src/services/sandboxManager.test.ts
index 9d82a3d87f..02e16fd5e9 100644
--- a/packages/core/src/services/sandboxManager.test.ts
+++ b/packages/core/src/services/sandboxManager.test.ts
@@ -10,7 +10,6 @@ import fsPromises from 'node:fs/promises';
import { afterEach, describe, expect, it, vi, beforeEach } from 'vitest';
import {
NoopSandboxManager,
- LocalSandboxManager,
sanitizePaths,
findSecretFiles,
isSecretFile,
@@ -374,6 +373,7 @@ describe('SandboxManager', () => {
it.each([
{ platform: 'linux', expected: LinuxSandboxManager },
{ platform: 'darwin', expected: MacOsSandboxManager },
+ { platform: 'win32', expected: WindowsSandboxManager },
] as const)(
'should return $expected.name if sandboxing is enabled and platform is $platform',
({ platform, expected }) => {
@@ -385,23 +385,5 @@ describe('SandboxManager', () => {
expect(manager).toBeInstanceOf(expected);
},
);
-
- it("should return WindowsSandboxManager if sandboxing is enabled with 'windows-native' command on win32", () => {
- vi.spyOn(os, 'platform').mockReturnValue('win32');
- const manager = createSandboxManager(
- { enabled: true, command: 'windows-native' },
- { workspace: '/workspace' },
- );
- expect(manager).toBeInstanceOf(WindowsSandboxManager);
- });
-
- it('should return LocalSandboxManager on win32 if command is not windows-native', () => {
- vi.spyOn(os, 'platform').mockReturnValue('win32');
- const manager = createSandboxManager(
- { enabled: true, command: 'docker' as unknown as 'windows-native' },
- { workspace: '/workspace' },
- );
- expect(manager).toBeInstanceOf(LocalSandboxManager);
- });
});
});
diff --git a/packages/core/src/services/sandboxManagerFactory.ts b/packages/core/src/services/sandboxManagerFactory.ts
index 29c89cc722..cb70f796d1 100644
--- a/packages/core/src/services/sandboxManagerFactory.ts
+++ b/packages/core/src/services/sandboxManagerFactory.ts
@@ -33,7 +33,7 @@ export function createSandboxManager(
}
if (sandbox?.enabled) {
- if (os.platform() === 'win32' && sandbox?.command === 'windows-native') {
+ if (os.platform() === 'win32') {
return new WindowsSandboxManager(options);
} else if (os.platform() === 'linux') {
return new LinuxSandboxManager(options);