bashCommandHelpers.ts
tools/BashTool/bashCommandHelpers.ts
266
Lines
8589
Bytes
2
Exports
9
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 part of the tool layer, which means it describes actions the system can perform for the user or model.
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 tool-system, commands. It contains 266 lines, 9 detected imports, and 2 detected exports.
Important relationships
Detected exports
CommandIdentityCheckerscheckCommandOperatorPermissions
Keywords
commandsegmentinputdecisionreasonbehaviorbashtoolsegmentsmessagepermissionresultresult
Detected imports
zod/v4../../utils/bash/commands.js../../utils/bash/ParsedCommand.js../../utils/bash/parser.js../../utils/permissions/PermissionResult.js../../utils/permissions/PermissionUpdateSchema.js../../utils/permissions/permissions.js./BashTool.js./bashSecurity.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 type { z } from 'zod/v4'
import {
isUnsafeCompoundCommand_DEPRECATED,
splitCommand_DEPRECATED,
} from '../../utils/bash/commands.js'
import {
buildParsedCommandFromRoot,
type IParsedCommand,
ParsedCommand,
} from '../../utils/bash/ParsedCommand.js'
import { type Node, PARSE_ABORTED } from '../../utils/bash/parser.js'
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
import { createPermissionRequestMessage } from '../../utils/permissions/permissions.js'
import { BashTool } from './BashTool.js'
import { bashCommandIsSafeAsync_DEPRECATED } from './bashSecurity.js'
export type CommandIdentityCheckers = {
isNormalizedCdCommand: (command: string) => boolean
isNormalizedGitCommand: (command: string) => boolean
}
async function segmentedCommandPermissionResult(
input: z.infer<typeof BashTool.inputSchema>,
segments: string[],
bashToolHasPermissionFn: (
input: z.infer<typeof BashTool.inputSchema>,
) => Promise<PermissionResult>,
checkers: CommandIdentityCheckers,
): Promise<PermissionResult> {
// Check for multiple cd commands across all segments
const cdCommands = segments.filter(segment => {
const trimmed = segment.trim()
return checkers.isNormalizedCdCommand(trimmed)
})
if (cdCommands.length > 1) {
const decisionReason = {
type: 'other' as const,
reason:
'Multiple directory changes in one command require approval for clarity',
}
return {
behavior: 'ask',
decisionReason,
message: createPermissionRequestMessage(BashTool.name, decisionReason),
}
}
// SECURITY: Check for cd+git across pipe segments to prevent bare repo fsmonitor bypass.
// When cd and git are in different pipe segments (e.g., "cd sub && echo | git status"),
// each segment is checked independently and neither triggers the cd+git check in
// bashPermissions.ts. We must detect this cross-segment pattern here.
// Each pipe segment can itself be a compound command (e.g., "cd sub && echo"),
// so we split each segment into subcommands before checking.
{
let hasCd = false
let hasGit = false
for (const segment of segments) {
const subcommands = splitCommand_DEPRECATED(segment)
for (const sub of subcommands) {
const trimmed = sub.trim()
if (checkers.isNormalizedCdCommand(trimmed)) {
hasCd = true
}
if (checkers.isNormalizedGitCommand(trimmed)) {
hasGit = true
}
}
}
if (hasCd && hasGit) {
const decisionReason = {
type: 'other' as const,
reason:
'Compound commands with cd and git require approval to prevent bare repository attacks',
}
return {
behavior: 'ask',
decisionReason,
message: createPermissionRequestMessage(BashTool.name, decisionReason),
}
}
}
const segmentResults = new Map<string, PermissionResult>()
// Check each segment through the full permission system
for (const segment of segments) {
const trimmedSegment = segment.trim()
if (!trimmedSegment) continue // Skip empty segments
const segmentResult = await bashToolHasPermissionFn({
...input,
command: trimmedSegment,
})
segmentResults.set(trimmedSegment, segmentResult)
}
// Check if any segment is denied (after evaluating all)
const deniedSegment = Array.from(segmentResults.entries()).find(
([, result]) => result.behavior === 'deny',
)
if (deniedSegment) {
const [segmentCommand, segmentResult] = deniedSegment
return {
behavior: 'deny',
message:
segmentResult.behavior === 'deny'
? segmentResult.message
: `Permission denied for: ${segmentCommand}`,
decisionReason: {
type: 'subcommandResults',
reasons: segmentResults,
},
}
}
const allAllowed = Array.from(segmentResults.values()).every(
result => result.behavior === 'allow',
)
if (allAllowed) {
return {
behavior: 'allow',
updatedInput: input,
decisionReason: {
type: 'subcommandResults',
reasons: segmentResults,
},
}
}
// Collect suggestions from segments that need approval
const suggestions: PermissionUpdate[] = []
for (const [, result] of segmentResults) {
if (
result.behavior !== 'allow' &&
'suggestions' in result &&
result.suggestions
) {
suggestions.push(...result.suggestions)
}
}
const decisionReason = {
type: 'subcommandResults' as const,
reasons: segmentResults,
}
return {
behavior: 'ask',
message: createPermissionRequestMessage(BashTool.name, decisionReason),
decisionReason,
suggestions: suggestions.length > 0 ? suggestions : undefined,
}
}
/**
* Builds a command segment, stripping output redirections to avoid
* treating filenames as commands in permission checking.
* Uses ParsedCommand to preserve original quoting.
*/
async function buildSegmentWithoutRedirections(
segmentCommand: string,
): Promise<string> {
// Fast path: skip parsing if no redirection operators present
if (!segmentCommand.includes('>')) {
return segmentCommand
}
// Use ParsedCommand to strip redirections while preserving quotes
const parsed = await ParsedCommand.parse(segmentCommand)
return parsed?.withoutOutputRedirections() ?? segmentCommand
}
/**
* Wrapper that resolves an IParsedCommand (from a pre-parsed AST root if
* available, else via ParsedCommand.parse) and delegates to
* bashToolCheckCommandOperatorPermissions.
*/
export async function checkCommandOperatorPermissions(
input: z.infer<typeof BashTool.inputSchema>,
bashToolHasPermissionFn: (
input: z.infer<typeof BashTool.inputSchema>,
) => Promise<PermissionResult>,
checkers: CommandIdentityCheckers,
astRoot: Node | null | typeof PARSE_ABORTED,
): Promise<PermissionResult> {
const parsed =
astRoot && astRoot !== PARSE_ABORTED
? buildParsedCommandFromRoot(input.command, astRoot)
: await ParsedCommand.parse(input.command)
if (!parsed) {
return { behavior: 'passthrough', message: 'Failed to parse command' }
}
return bashToolCheckCommandOperatorPermissions(
input,
bashToolHasPermissionFn,
checkers,
parsed,
)
}
/**
* Checks if the command has special operators that require behavior beyond
* simple subcommand checking.
*/
async function bashToolCheckCommandOperatorPermissions(
input: z.infer<typeof BashTool.inputSchema>,
bashToolHasPermissionFn: (
input: z.infer<typeof BashTool.inputSchema>,
) => Promise<PermissionResult>,
checkers: CommandIdentityCheckers,
parsed: IParsedCommand,
): Promise<PermissionResult> {
// 1. Check for unsafe compound commands (subshells, command groups).
const tsAnalysis = parsed.getTreeSitterAnalysis()
const isUnsafeCompound = tsAnalysis
? tsAnalysis.compoundStructure.hasSubshell ||
tsAnalysis.compoundStructure.hasCommandGroup
: isUnsafeCompoundCommand_DEPRECATED(input.command)
if (isUnsafeCompound) {
// This command contains an operator like `>` that we don't support as a subcommand separator
// Check if bashCommandIsSafe_DEPRECATED has a more specific message
const safetyResult = await bashCommandIsSafeAsync_DEPRECATED(input.command)
const decisionReason = {
type: 'other' as const,
reason:
safetyResult.behavior === 'ask' && safetyResult.message
? safetyResult.message
: 'This command uses shell operators that require approval for safety',
}
return {
behavior: 'ask',
message: createPermissionRequestMessage(BashTool.name, decisionReason),
decisionReason,
// This is an unsafe compound command, so we don't want to suggest rules since we wont be able to allow it
}
}
// 2. Check for piped commands using ParsedCommand (preserves quotes)
const pipeSegments = parsed.getPipeSegments()
// If no pipes (single segment), let normal flow handle it
if (pipeSegments.length <= 1) {
return {
behavior: 'passthrough',
message: 'No pipes found in command',
}
}
// Strip output redirections from each segment while preserving quotes
const segments = await Promise.all(
pipeSegments.map(segment => buildSegmentWithoutRedirections(segment)),
)
// Handle as segmented command
return segmentedCommandPermissionResult(
input,
segments,
bashToolHasPermissionFn,
checkers,
)
}