Skip to content

Commit c24da82

Browse files
committed
chore(scripts/claude): defuse shell concatenation in pseudo-TTY spawn
The two call sites that wrap claude in script -q / winpty / sh to get a pseudo-TTY for Ink built a single command string and invoked it via spawn(cmd, [], { shell: true }). The string embedded claudeCommand and tmpFile directly — safe today because both are internally generated, but fragile if a future refactor ever pulls either from user input. Pass the command and temp file to sh -c via environment variables (CLAUDE_CMD / CLAUDE_TMP) so the inner shell reads them as variables and the outer spawn no longer needs shell: true. Same behavior, no string concatenation into a shell context.
1 parent 2ff5994 commit c24da82

1 file changed

Lines changed: 47 additions & 22 deletions

File tree

scripts/claude.mts

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4996,28 +4996,41 @@ Fix all issues by making necessary file changes. Be direct, don't ask questions.
49964996
? `${claudeCmd} ${claudeArgs}`
49974997
: claudeCmd
49984998

4999-
// Use script command to create pseudo-TTY for Ink compatibility
5000-
// Platform-specific script command syntax
5001-
let scriptCmd
4999+
// Use script command to create pseudo-TTY for Ink compatibility.
5000+
// Pass the inner command and tmp file to sh -c via environment
5001+
// variables so spaces / special chars in either cannot inject into
5002+
// the shell. Outer spawn uses shell: false.
5003+
let cmdBin: string
5004+
let cmdArgs: string[]
50025005
if (WIN32) {
5003-
// Try winpty (comes with Git for Windows)
50045006
const winptyCheck = await runCommandWithOutput('where', ['winpty'])
50055007
if (winptyCheck.exitCode === 0) {
5006-
scriptCmd = `winpty ${claudeCommand} < "${tmpFile}"`
5008+
cmdBin = 'winpty'
5009+
cmdArgs = ['sh', '-c', '"$CLAUDE_CMD" < "$CLAUDE_TMP"']
50075010
} else {
5008-
// No winpty, try direct (may fail with raw mode error)
5009-
scriptCmd = `${claudeCommand} < "${tmpFile}"`
5011+
cmdBin = 'sh'
5012+
cmdArgs = ['-c', '"$CLAUDE_CMD" < "$CLAUDE_TMP"']
50105013
}
50115014
} else {
5012-
// Unix/macOS: use script command with quoted command
5013-
scriptCmd = `script -q /dev/null sh -c '${claudeCommand} < "${tmpFile}"'`
5015+
cmdBin = 'script'
5016+
cmdArgs = [
5017+
'-q',
5018+
'/dev/null',
5019+
'sh',
5020+
'-c',
5021+
'"$CLAUDE_CMD" < "$CLAUDE_TMP"',
5022+
]
50145023
}
50155024

50165025
const exitCode = await new Promise((resolve, _reject) => {
5017-
const spawnPromise = spawn(scriptCmd, [], {
5026+
const spawnPromise = spawn(cmdBin, cmdArgs, {
50185027
stdio: 'inherit',
50195028
cwd: rootPath,
5020-
shell: true,
5029+
env: {
5030+
...process.env,
5031+
CLAUDE_CMD: claudeCommand,
5032+
CLAUDE_TMP: tmpFile,
5033+
},
50215034
})
50225035

50235036
const child = spawnPromise.process
@@ -5317,30 +5330,42 @@ Fix the issue by making necessary file changes. Be direct, don't ask questions.`
53175330
log.substep(`Running: claude ${claudeArgs}`)
53185331
}
53195332

5320-
// Use script command to create pseudo-TTY for Ink compatibility
5321-
// Platform-specific script command syntax
5322-
let scriptCmd
5333+
// Use script command to create pseudo-TTY for Ink compatibility.
5334+
// Pass CLAUDE_CMD / CLAUDE_TMP via env so special chars in
5335+
// either cannot inject into the shell.
5336+
let cmdBin: string
5337+
let cmdArgs: string[]
53235338
if (WIN32) {
5324-
// Try winpty (comes with Git for Windows)
53255339
const winptyCheck = await runCommandWithOutput('where', [
53265340
'winpty',
53275341
])
53285342
if (winptyCheck.exitCode === 0) {
5329-
scriptCmd = `winpty ${claudeCommand} < "${tmpFile}"`
5343+
cmdBin = 'winpty'
5344+
cmdArgs = ['sh', '-c', '"$CLAUDE_CMD" < "$CLAUDE_TMP"']
53305345
} else {
5331-
// No winpty, try direct (may fail with raw mode error)
5332-
scriptCmd = `${claudeCommand} < "${tmpFile}"`
5346+
cmdBin = 'sh'
5347+
cmdArgs = ['-c', '"$CLAUDE_CMD" < "$CLAUDE_TMP"']
53335348
}
53345349
} else {
5335-
// Unix/macOS: use script command with quoted command
5336-
scriptCmd = `script -q /dev/null sh -c '${claudeCommand} < "${tmpFile}"'`
5350+
cmdBin = 'script'
5351+
cmdArgs = [
5352+
'-q',
5353+
'/dev/null',
5354+
'sh',
5355+
'-c',
5356+
'"$CLAUDE_CMD" < "$CLAUDE_TMP"',
5357+
]
53375358
}
53385359

53395360
const exitCode = await new Promise((resolve, _reject) => {
5340-
const spawnPromise = spawn(scriptCmd, [], {
5361+
const spawnPromise = spawn(cmdBin, cmdArgs, {
53415362
stdio: 'inherit',
53425363
cwd: rootPath,
5343-
shell: true,
5364+
env: {
5365+
...process.env,
5366+
CLAUDE_CMD: claudeCommand,
5367+
CLAUDE_TMP: tmpFile,
5368+
},
53445369
})
53455370

53465371
const child = spawnPromise.process

0 commit comments

Comments
 (0)