promptShellExecution.ts
utils/promptShellExecution.ts
184
Lines
7011
Bytes
1
Exports
10
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with shell-safety. It contains 184 lines, 10 detected imports, and 1 detected exports.
Important relationships
Detected exports
executeShellCommandsInPrompt
Keywords
commandshellbashtoolstderrpatterninlinepowershelltooltexttoolmalformedcommanderror
Detected imports
crypto../Tool.js../tools/BashTool/BashTool.js./debug.js./errors.js./frontmatterParser.js./messages.js./permissions/permissions.js./toolResultStorage.js./shell/shellToolUtils.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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)
}