( input: z.infer<typeof BashTool.inputSchema>, context: ToolUseContext, getCommandSubcommandPrefixFn = getCommandSubcommandPrefix, )
| 1661 | * The main implementation to check if we need to ask for user permission to call BashTool with a given input |
| 1662 | */ |
| 1663 | export async function bashToolHasPermission( |
| 1664 | input: z.infer<typeof BashTool.inputSchema>, |
| 1665 | context: ToolUseContext, |
| 1666 | getCommandSubcommandPrefixFn = getCommandSubcommandPrefix, |
| 1667 | ): Promise<PermissionResult> { |
| 1668 | let appState = context.getAppState() |
| 1669 | |
| 1670 | // 0. AST-based security parse. This replaces both tryParseShellCommand |
| 1671 | // (the shell-quote pre-check) and the bashCommandIsSafe misparsing gate. |
| 1672 | // tree-sitter produces either a clean SimpleCommand[] (quotes resolved, |
| 1673 | // no hidden substitutions) or 'too-complex' — which is exactly the signal |
| 1674 | // we need to decide whether splitCommand's output can be trusted. |
| 1675 | // |
| 1676 | // When tree-sitter WASM is unavailable OR the injection check is disabled |
| 1677 | // via env var, we fall back to the old path (legacy gate at ~1370 runs). |
| 1678 | const injectionCheckDisabled = isEnvTruthy( |
| 1679 | process.env.CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK, |
| 1680 | ) |
| 1681 | // GrowthBook killswitch for shadow mode — when off, skip the native parse |
| 1682 | // entirely. Computed once; feature() must stay inline in the ternary below. |
| 1683 | const shadowEnabled = feature('TREE_SITTER_BASH_SHADOW') |
| 1684 | ? getFeatureValue_CACHED_MAY_BE_STALE('tengu_birch_trellis', true) |
| 1685 | : false |
| 1686 | // Parse once here; the resulting AST feeds both parseForSecurityFromAst |
| 1687 | // and bashToolCheckCommandOperatorPermissions. |
| 1688 | let astRoot = injectionCheckDisabled |
| 1689 | ? null |
| 1690 | : feature('TREE_SITTER_BASH_SHADOW') && !shadowEnabled |
| 1691 | ? null |
| 1692 | : await parseCommandRaw(input.command) |
| 1693 | let astResult: ParseForSecurityResult = astRoot |
| 1694 | ? parseForSecurityFromAst(input.command, astRoot) |
| 1695 | : { kind: 'parse-unavailable' } |
| 1696 | let astSubcommands: string[] | null = null |
| 1697 | let astRedirects: Redirect[] | undefined |
| 1698 | let astCommands: SimpleCommand[] | undefined |
| 1699 | let shadowLegacySubs: string[] | undefined |
| 1700 | |
| 1701 | // Shadow-test tree-sitter: record its verdict, then force parse-unavailable |
| 1702 | // so the legacy path stays authoritative. parseCommand stays gated on |
| 1703 | // TREE_SITTER_BASH (not SHADOW) so legacy internals remain pure regex. |
| 1704 | // One event per bash call captures both divergence AND unavailability |
| 1705 | // reasons; module-load failures are separately covered by the |
| 1706 | // session-scoped tengu_tree_sitter_load event. |
| 1707 | if (feature('TREE_SITTER_BASH_SHADOW')) { |
| 1708 | const available = astResult.kind !== 'parse-unavailable' |
| 1709 | let tooComplex = false |
| 1710 | let semanticFail = false |
| 1711 | let subsDiffer = false |
| 1712 | if (available) { |
| 1713 | tooComplex = astResult.kind === 'too-complex' |
| 1714 | semanticFail = |
| 1715 | astResult.kind === 'simple' && !checkSemantics(astResult.commands).ok |
| 1716 | const tsSubs = |
| 1717 | astResult.kind === 'simple' |
| 1718 | ? astResult.commands.map(c => c.text) |
| 1719 | : undefined |
| 1720 | const legacySubs = splitCommand(input.command) |
no test coverage detected