source dump of claude code
at main 123 lines 5.8 kB view raw
1import { tmpdir } from 'os' 2import { join } from 'path' 3import { join as posixJoin } from 'path/posix' 4import { getSessionEnvVars } from '../sessionEnvVars.js' 5import type { ShellProvider } from './shellProvider.js' 6 7/** 8 * PowerShell invocation flags + command. Shared by the provider's getSpawnArgs 9 * and the hook spawn path in hooks.ts so the flag set stays in one place. 10 */ 11export function buildPowerShellArgs(cmd: string): string[] { 12 return ['-NoProfile', '-NonInteractive', '-Command', cmd] 13} 14 15/** 16 * Base64-encode a string as UTF-16LE for PowerShell's -EncodedCommand. 17 * Same encoding the parser uses (parser.ts toUtf16LeBase64). The output 18 * is [A-Za-z0-9+/=] only — survives ANY shell-quoting layer, including 19 * @anthropic-ai/sandbox-runtime's shellquote.quote() which would otherwise 20 * corrupt !$? to \!$? when re-wrapping a single-quoted string in double 21 * quotes. Review 2964609818. 22 */ 23function encodePowerShellCommand(psCommand: string): string { 24 return Buffer.from(psCommand, 'utf16le').toString('base64') 25} 26 27export function createPowerShellProvider(shellPath: string): ShellProvider { 28 let currentSandboxTmpDir: string | undefined 29 30 return { 31 type: 'powershell' as ShellProvider['type'], 32 shellPath, 33 detached: false, 34 35 async buildExecCommand( 36 command: string, 37 opts: { 38 id: number | string 39 sandboxTmpDir?: string 40 useSandbox: boolean 41 }, 42 ): Promise<{ commandString: string; cwdFilePath: string }> { 43 // Stash sandboxTmpDir for getEnvironmentOverrides (mirrors bashProvider) 44 currentSandboxTmpDir = opts.useSandbox ? opts.sandboxTmpDir : undefined 45 46 // When sandboxed, tmpdir() is not writable — the sandbox only allows 47 // writes to sandboxTmpDir. Put the cwd tracking file there so the 48 // inner pwsh can actually write it. Only applies on Linux/macOS/WSL2; 49 // on Windows native, sandbox is never enabled so this branch is dead. 50 const cwdFilePath = 51 opts.useSandbox && opts.sandboxTmpDir 52 ? posixJoin(opts.sandboxTmpDir, `claude-pwd-ps-${opts.id}`) 53 : join(tmpdir(), `claude-pwd-ps-${opts.id}`) 54 const escapedCwdFilePath = cwdFilePath.replace(/'/g, "''") 55 // Exit-code capture: prefer $LASTEXITCODE when a native exe ran. 56 // On PS 5.1, a native command that writes to stderr while the stream 57 // is PS-redirected (e.g. `git push 2>&1`) sets $? = $false even when 58 // the exe returned exit 0 — so `if (!$?)` reports a false positive. 59 // $LASTEXITCODE is $null only when no native exe has run in the 60 // session; in that case fall back to $? for cmdlet-only pipelines. 61 // Tradeoff: `native-ok; cmdlet-fail` now returns 0 (was 1). Reverse 62 // is also true: `native-fail; cmdlet-ok` now returns the native 63 // exit code (was 0 — old logic only looked at $? which the trailing 64 // cmdlet set true). Both rarer than the git/npm/curl stderr case. 65 const cwdTracking = `\n; $_ec = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } elseif ($?) { 0 } else { 1 }\n; (Get-Location).Path | Out-File -FilePath '${escapedCwdFilePath}' -Encoding utf8 -NoNewline\n; exit $_ec` 66 const psCommand = command + cwdTracking 67 68 // Sandbox wraps the returned commandString as `<binShell> -c '<cmd>'` — 69 // hardcoded `-c`, no way to inject -NoProfile -NonInteractive. So for 70 // the sandbox path, build a command that itself invokes pwsh with the 71 // full flag set. Shell.ts passes /bin/sh as the sandbox binShell, 72 // producing: bwrap ... sh -c 'pwsh -NoProfile ... -EncodedCommand ...'. 73 // The non-sandbox path returns the bare PS command; getSpawnArgs() adds 74 // the flags via buildPowerShellArgs(). 75 // 76 // -EncodedCommand (base64 UTF-16LE), not -Command: the sandbox runtime 77 // applies its OWN shellquote.quote() on top of whatever we build. Any 78 // string containing ' triggers double-quote mode which escapes ! as \! — 79 // POSIX sh preserves that literally, pwsh parse error. Base64 is 80 // [A-Za-z0-9+/=] — no chars that any quoting layer can corrupt. 81 // Review 2964609818. 82 // 83 // shellPath is POSIX-single-quoted so a space-containing install path 84 // (e.g. /opt/my tools/pwsh) survives the inner `/bin/sh -c` word-split. 85 // Flags and base64 are [A-Za-z0-9+/=-] only — no quoting needed. 86 const commandString = opts.useSandbox 87 ? [ 88 `'${shellPath.replace(/'/g, `'\\''`)}'`, 89 '-NoProfile', 90 '-NonInteractive', 91 '-EncodedCommand', 92 encodePowerShellCommand(psCommand), 93 ].join(' ') 94 : psCommand 95 96 return { commandString, cwdFilePath } 97 }, 98 99 getSpawnArgs(commandString: string): string[] { 100 return buildPowerShellArgs(commandString) 101 }, 102 103 async getEnvironmentOverrides(): Promise<Record<string, string>> { 104 const env: Record<string, string> = {} 105 // Apply session env vars set via /env (child processes only, not 106 // the REPL). Without this, `/env PATH=...` affects Bash tool 107 // commands but not PowerShell — so PyCharm users with a stripped 108 // PATH can't self-rescue. 109 // Ordering: session vars FIRST so the sandbox TMPDIR below can't be 110 // overridden by `/env TMPDIR=...`. bashProvider.ts has these in the 111 // opposite order (pre-existing), but sandbox isolation should win. 112 for (const [key, value] of getSessionEnvVars()) { 113 env[key] = value 114 } 115 if (currentSandboxTmpDir) { 116 // PowerShell on Linux/macOS honors TMPDIR for [System.IO.Path]::GetTempPath() 117 env.TMPDIR = currentSandboxTmpDir 118 env.CLAUDE_CODE_TMPDIR = currentSandboxTmpDir 119 } 120 return env 121 }, 122 } 123}