π File detail
utils/shell/powershellProvider.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes buildPowerShellArgs and createPowerShellProvider β mainly functions, hooks, or classes. Dependencies touch Node OS/process metadata and Node path helpers. It composes internal code from sessionEnvVars and shellProvider (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { tmpdir } from 'os' import { join } from 'path' import { join as posixJoin } from 'path/posix' import { getSessionEnvVars } from '../sessionEnvVars.js' import type { ShellProvider } from './shellProvider.js'
π€ Exports (heuristic)
buildPowerShellArgscreatePowerShellProvider
π External import roots
Package roots from from "β¦" (relative paths omitted).
ospath
π₯οΈ Source preview
import { tmpdir } from 'os'
import { join } from 'path'
import { join as posixJoin } from 'path/posix'
import { getSessionEnvVars } from '../sessionEnvVars.js'
import type { ShellProvider } from './shellProvider.js'
/**
* PowerShell invocation flags + command. Shared by the provider's getSpawnArgs
* and the hook spawn path in hooks.ts so the flag set stays in one place.
*/
export function buildPowerShellArgs(cmd: string): string[] {
return ['-NoProfile', '-NonInteractive', '-Command', cmd]
}
/**
* Base64-encode a string as UTF-16LE for PowerShell's -EncodedCommand.
* Same encoding the parser uses (parser.ts toUtf16LeBase64). The output
* is [A-Za-z0-9+/=] only β survives ANY shell-quoting layer, including
* @anthropic-ai/sandbox-runtime's shellquote.quote() which would otherwise
* corrupt !$? to \!$? when re-wrapping a single-quoted string in double
* quotes. Review 2964609818.
*/
function encodePowerShellCommand(psCommand: string): string {
return Buffer.from(psCommand, 'utf16le').toString('base64')
}
export function createPowerShellProvider(shellPath: string): ShellProvider {
let currentSandboxTmpDir: string | undefined
return {
type: 'powershell' as ShellProvider['type'],
shellPath,
detached: false,
async buildExecCommand(
command: string,
opts: {
id: number | string
sandboxTmpDir?: string
useSandbox: boolean
},
): Promise<{ commandString: string; cwdFilePath: string }> {
// Stash sandboxTmpDir for getEnvironmentOverrides (mirrors bashProvider)
currentSandboxTmpDir = opts.useSandbox ? opts.sandboxTmpDir : undefined
// When sandboxed, tmpdir() is not writable β the sandbox only allows
// writes to sandboxTmpDir. Put the cwd tracking file there so the
// inner pwsh can actually write it. Only applies on Linux/macOS/WSL2;
// on Windows native, sandbox is never enabled so this branch is dead.
const cwdFilePath =
opts.useSandbox && opts.sandboxTmpDir
? posixJoin(opts.sandboxTmpDir, `claude-pwd-ps-${opts.id}`)
: join(tmpdir(), `claude-pwd-ps-${opts.id}`)
const escapedCwdFilePath = cwdFilePath.replace(/'/g, "''")
// Exit-code capture: prefer $LASTEXITCODE when a native exe ran.
// On PS 5.1, a native command that writes to stderr while the stream
// is PS-redirected (e.g. `git push 2>&1`) sets $? = $false even when
// the exe returned exit 0 β so `if (!$?)` reports a false positive.
// $LASTEXITCODE is $null only when no native exe has run in the
// session; in that case fall back to $? for cmdlet-only pipelines.
// Tradeoff: `native-ok; cmdlet-fail` now returns 0 (was 1). Reverse
// is also true: `native-fail; cmdlet-ok` now returns the native
// exit code (was 0 β old logic only looked at $? which the trailing
// cmdlet set true). Both rarer than the git/npm/curl stderr case.
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`
const psCommand = command + cwdTracking
// Sandbox wraps the returned commandString as `<binShell> -c '<cmd>'` β
// hardcoded `-c`, no way to inject -NoProfile -NonInteractive. So for
// the sandbox path, build a command that itself invokes pwsh with the
// full flag set. Shell.ts passes /bin/sh as the sandbox binShell,
// producing: bwrap ... sh -c 'pwsh -NoProfile ... -EncodedCommand ...'.
// The non-sandbox path returns the bare PS command; getSpawnArgs() adds
// the flags via buildPowerShellArgs().
//
// -EncodedCommand (base64 UTF-16LE), not -Command: the sandbox runtime
// applies its OWN shellquote.quote() on top of whatever we build. Any
// string containing ' triggers double-quote mode which escapes ! as \! β
// POSIX sh preserves that literally, pwsh parse error. Base64 is
// [A-Za-z0-9+/=] β no chars that any quoting layer can corrupt.
// Review 2964609818.
//
// shellPath is POSIX-single-quoted so a space-containing install path
// (e.g. /opt/my tools/pwsh) survives the inner `/bin/sh -c` word-split.
// Flags and base64 are [A-Za-z0-9+/=-] only β no quoting needed.
const commandString = opts.useSandbox
? [
`'${shellPath.replace(/'/g, `'\\''`)}'`,
'-NoProfile',
'-NonInteractive',
'-EncodedCommand',
encodePowerShellCommand(psCommand),
].join(' ')
: psCommand
return { commandString, cwdFilePath }
},
getSpawnArgs(commandString: string): string[] {
return buildPowerShellArgs(commandString)
},
async getEnvironmentOverrides(): Promise<Record<string, string>> {
const env: Record<string, string> = {}
// Apply session env vars set via /env (child processes only, not
// the REPL). Without this, `/env PATH=...` affects Bash tool
// commands but not PowerShell β so PyCharm users with a stripped
// PATH can't self-rescue.
// Ordering: session vars FIRST so the sandbox TMPDIR below can't be
// overridden by `/env TMPDIR=...`. bashProvider.ts has these in the
// opposite order (pre-existing), but sandbox isolation should win.
for (const [key, value] of getSessionEnvVars()) {
env[key] = value
}
if (currentSandboxTmpDir) {
// PowerShell on Linux/macOS honors TMPDIR for [System.IO.Path]::GetTempPath()
env.TMPDIR = currentSandboxTmpDir
env.CLAUDE_CODE_TMPDIR = currentSandboxTmpDir
}
return env
},
}
}