π File detail
utils/promptShellExecution.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 executeShellCommandsInPrompt β mainly functions, hooks, or classes. Dependencies touch crypto. It composes internal code from Tool, tools, debug, errors, and frontmatterParser (relative imports). What the file header says: Narrow structural slice both BashTool and PowerShellTool satisfy. We can't use the base Tool type: it marks call()'s canUseTool/parentMessage as.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Narrow structural slice both BashTool and PowerShellTool satisfy. We can't use the base Tool type: it marks call()'s canUseTool/parentMessage as
π€ Exports (heuristic)
executeShellCommandsInPrompt
π External import roots
Package roots from from "β¦" (relative paths omitted).
crypto
π₯οΈ Source preview
import { randomUUID } from 'crypto'
import type { Tool, ToolUseContext } from '../Tool.js'
import { BashTool } from '../tools/BashTool/BashTool.js'
import { logForDebugging } from './debug.js'
import { errorMessage, MalformedCommandError, ShellError } from './errors.js'
import type { FrontmatterShell } from './frontmatterParser.js'
import { createAssistantMessage } from './messages.js'
import { hasPermissionsToUseTool } from './permissions/permissions.js'
import { processToolResultBlock } from './toolResultStorage.js'
// Narrow structural slice both BashTool and PowerShellTool satisfy. We can't
// use the base Tool type: it marks call()'s canUseTool/parentMessage as
// required, but both concrete tools have them optional and the original code
// called BashTool.call({ command }, ctx) with just 2 args. We can't use
// `typeof BashTool` either: BashTool's input schema has fields (e.g.
// _simulatedSedEdit) that PowerShellTool's does not.
// NOTE: call() is invoked directly here, bypassing validateInput β any
// load-bearing check must live in call() itself (see PR #23311).
type ShellOut = { stdout: string; stderr: string; interrupted: boolean }
type PromptShellTool = Tool & {
call(
input: { command: string },
context: ToolUseContext,
): Promise<{ data: ShellOut }>
}
import { isPowerShellToolEnabled } from './shell/shellToolUtils.js'
// Lazy: this file is on the startup import chain (main β commands β
// loadSkillsDir β here). A static import would load PowerShellTool.ts
// (and transitively parser.ts, validators, etc.) at startup on all
// platforms, defeating tools.ts's lazy require. Deferred until the
// first skill with `shell: powershell` actually runs.
/* eslint-disable @typescript-eslint/no-require-imports */
const getPowerShellTool = (() => {
let cached: PromptShellTool | undefined
return (): PromptShellTool => {
if (!cached) {
cached = (
require('../tools/PowerShellTool/PowerShellTool.js') as typeof import('../tools/PowerShellTool/PowerShellTool.js')
).PowerShellTool
}
return cached
}
})()
/* eslint-enable @typescript-eslint/no-require-imports */
// Pattern for code blocks: ```! command ```
const BLOCK_PATTERN = /```!\s*\n?([\s\S]*?)\n?```/g
// Pattern for inline: !`command`
// Uses a positive lookbehind to require whitespace or start-of-line before !
// This prevents false matches inside markdown inline code spans like `!!` or
// adjacent spans like `foo`!`bar`, and shell variables like $!
// eslint-disable-next-line custom-rules/no-lookbehind-regex -- gated by text.includes('!`') below (PR#22986)
const INLINE_PATTERN = /(?<=^|\s)!`([^`]+)`/gm
/**
* Parses prompt text and executes any embedded shell commands.
* Supports two syntaxes:
* - Code blocks: ```! command ```
* - Inline: !`command`
*
* @param shell - Shell to route commands through. Defaults to bash.
* This is *never* read from settings.defaultShell β it comes from .md
* frontmatter (author's choice) or is undefined for built-in commands.
* See docs/design/ps-shell-selection.md Β§5.3.
*/
export async function executeShellCommandsInPrompt(
text: string,
context: ToolUseContext,
slashCommandName: string,
shell?: FrontmatterShell,
): Promise<string> {
let result = text
// Resolve the tool once. `shell === undefined` and `shell === 'bash'` both
// hit BashTool. PowerShell only when the runtime gate allows β a skill
// author's frontmatter choice doesn't override the user's opt-in/out.
const shellTool: PromptShellTool =
shell === 'powershell' && isPowerShellToolEnabled()
? getPowerShellTool()
: BashTool
// INLINE_PATTERN's lookbehind is ~100x slower than BLOCK_PATTERN on large
// skill content (265Β΅s vs 2Β΅s @ 17KB). 93% of skills have no !` at all,
// so gate the expensive scan on a cheap substring check. BLOCK_PATTERN
// (```!) doesn't require !` in the text, so it's always scanned.
const blockMatches = text.matchAll(BLOCK_PATTERN)
const inlineMatches = text.includes('!`') ? text.matchAll(INLINE_PATTERN) : []
await Promise.all(
[...blockMatches, ...inlineMatches].map(async match => {
const command = match[1]?.trim()
if (command) {
try {
// Check permissions before executing
const permissionResult = await hasPermissionsToUseTool(
shellTool,
{ command },
context,
createAssistantMessage({ content: [] }),
'',
)
if (permissionResult.behavior !== 'allow') {
logForDebugging(
`Shell command permission check failed for command in ${slashCommandName}: ${command}. Error: ${permissionResult.message}`,
)
throw new MalformedCommandError(
`Shell command permission check failed for pattern "${match[0]}": ${permissionResult.message || 'Permission denied'}`,
)
}
const { data } = await shellTool.call({ command }, context)
// Reuse the same persistence flow as regular Bash tool calls
const toolResultBlock = await processToolResultBlock(
shellTool,
data,
randomUUID(),
)
// Extract the string content from the block
const output =
typeof toolResultBlock.content === 'string'
? toolResultBlock.content
: formatBashOutput(data.stdout, data.stderr)
// Function replacer β String.replace interprets $$, $&, $`, $' in
// the replacement string even with a string search pattern. Shell
// output (especially PowerShell: $env:PATH, $$, $PSVersionTable)
// is arbitrary user data; a bare string arg would corrupt it.
result = result.replace(match[0], () => output)
} catch (e) {
if (e instanceof MalformedCommandError) {
throw e
}
formatBashError(e, match[0])
}
}
}),
)
return result
}
function formatBashOutput(
stdout: string,
stderr: string,
inline = false,
): string {
const parts: string[] = []
if (stdout.trim()) {
parts.push(stdout.trim())
}
if (stderr.trim()) {
if (inline) {
parts.push(`[stderr: ${stderr.trim()}]`)
} else {
parts.push(`[stderr]\n${stderr.trim()}`)
}
}
return parts.join(inline ? ' ' : '\n')
}
function formatBashError(e: unknown, pattern: string, inline = false): never {
if (e instanceof ShellError) {
if (e.interrupted) {
throw new MalformedCommandError(
`Shell command interrupted for pattern "${pattern}": [Command interrupted]`,
)
}
const output = formatBashOutput(e.stdout, e.stderr, inline)
throw new MalformedCommandError(
`Shell command failed for pattern "${pattern}": ${output}`,
)
}
const message = errorMessage(e)
const formatted = inline ? `[Error: ${message}]` : `[Error]\n${message}`
throw new MalformedCommandError(formatted)
}