7.0 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 333 | Permission prompt for chained bash commands only shows the triggering sub-command, hiding the rest of the chain from the user |
Surface the full chained command in the bash permission prompt
Problem Statement
When the model runs a chained bash command (e.g. cd /var/www/html && rm -rf *), the permission system splits the chain, picks the most restrictive sub-command (rm -rf *), and prompts the user about that sub-command alone.
The prompt reads Current agent requested bash command 'rm -rf *'. Allow this command? — with no hint that a cd into a critical path preceded it.
The user approves what looks like a harmless relative delete, not realising it wipes a web root.
The full original command is already passed into formatAskPrompt as the input argument, but the bash branch ignores it.
Goals
- Append the full original command to the bash ask prompt when it differs from the matched sub-command, so the user sees the whole chain before approving.
- Suppress the suffix when the sub-command and full command are identical (no chain), keeping single-command prompts unchanged.
- Keep the change isolated to
formatAskPrompt's bash branch.
Non-Goals
- No change to chain splitting, sub-command selection, or the "most restrictive" analysis (
evaluate-bash-command-chainsmachinery is correct and stays as-is). - No change to the MCP or generic-tool branches of
formatAskPrompt. - No change to the denial-message path (
denial-messages.ts) — only theaskprompt is in scope. - No new config field or schema change.
Background
formatAskPrompt (src/permission-prompts.ts) builds the user-facing approval prompt.
Its signature already accepts input?: unknown (the raw tool input), used today only by the MCP and generic-tool branches via the ToolPreviewFormatter.
The bash branch reads result.command (the selected sub-command) and result.matchedPattern/result.commandContext (for the qualifier) but never reads input.
The call site in src/handlers/gates/tool.ts already forwards the raw input as tcc.input, so no wiring change is needed — the full command is reachable inside the bash branch right now.
src/common.ts already exports the two helpers the issue references:
toRecord(value: unknown): Record<string, unknown>— coerces a non-object/array to{}.getNonEmptyString(value: unknown): string | null— returns a trimmed non-empty string ornull.
Constraint from the package skill: default to least privilege and keep prompts reviewable.
Adding chain context strengthens the user's ability to make an informed ask decision, consistent with that priority.
Design Overview
Inside the existing if (result.toolName === "bash") branch:
subCommand=result.command ?? ""(unchanged — the matched sub-command).fullCommand=getNonEmptyString(toRecord(input).command)— the raw command string from the tool input, ornull.fullCommandInfo=(full command: '<fullCommand>')only whenfullCommandis non-null andfullCommand !== subCommand; otherwise empty.- Return
${subject} requested bash command '${subCommand}'${qualifierInfo}${fullCommandInfo}. Allow this command?.
Ordering: qualifierInfo (matched-pattern / nested-context note) stays immediately after the sub-command, and fullCommandInfo follows it, before the terminal . Allow this command?.
Edge cases:
inputisundefined(existing bash tests pass it asundefined) →toRecord(undefined)is{},fullCommandisnull, no suffix. Existing tests stay green.input.commandequals the sub-command (single, non-chained command) → suffix suppressed.input.commandis missing, empty, or non-string →getNonEmptyStringreturnsnull, no suffix.input.commanddiffers from sub-command (real chain) → suffix appended with the full chain.
Resulting prompt for the issue's repro:
Current agent requested bash command 'rm -rf .' (full command: 'echo "hello" && rm -rf .'). Allow this command?
No type or signature change — only the bash branch body changes.
Module-Level Changes
src/permission-prompts.ts- Add
import { getNonEmptyString, toRecord } from "./common";(top-level import). - In the bash branch of
formatAskPrompt, read the full command frominputand appendfullCommandInfowhen it differs from the sub-command.
- Add
test/permission-prompts.test.ts- Add tests covering: chain → suffix present; single command (input === sub-command) → no suffix;
inputundefined → no suffix; missing/emptycommand→ no suffix.
- Add tests covering: chain → suffix present; single command (input === sub-command) → no suffix;
No schema, config, README, docs/configuration.md, or architecture-doc changes — this is a behavior-preserving prompt-text fix with no new surface or field.
Test Impact Analysis
This is a localized bug fix, not an extraction, so the extraction-specific analysis is light:
- New tests enabled: the full-command-context behavior is newly testable purely at the
formatAskPromptunit level — no new seam is required becauseinputis already a parameter. - Redundant tests: none.
Existing bash tests pass
inputasundefined; they continue to assert the un-suffixed prompt and remain valid as the "no chain context" case. - Tests that must stay: all existing
formatAskPromptbash/MCP/tool tests stay as-is — they pin the surrounding branches and the qualifier ordering this change must not disturb.
TDD Order
- Red → Green → Commit —
test/permission-prompts.test.ts, bash full-command context.- Red: add a test that a chained
input({ command: 'echo "hello" && rm -rf .' }) withresult.command = "rm -rf ."produces a prompt containing(full command: 'echo "hello" && rm -rf .'). - Add companion tests: identical sub-command and full command → no
full command:suffix;inputundefined→ no suffix;input.commandmissing/empty → no suffix; qualifier + full-command ordering ('rm -rf .' (matched 'rm *') (full command: '...')). - Green: implement the bash-branch change in
src/permission-prompts.ts(import helpers, computefullCommandInfo). - Commit:
fix: surface full chained command in bash permission prompt (#333).
- Red: add a test that a chained
The production change and its tests land in one cycle because the change is a single branch edit with no intermediate state.
Risks and Mitigations
- Risk: appending the full command when no chain exists would make every single-command prompt noisier.
Mitigation: the
fullCommand !== subCommandguard suppresses the suffix for non-chained commands; a dedicated test pins this. - Risk: a non-string or missing
input.commandcould throw or printundefined. Mitigation:toRecord+getNonEmptyStringnormalise both cases tonull; tests cover undefined and missingcommand. - Risk: disturbing the qualifier ordering relied on by existing tests.
Mitigation:
fullCommandInfois appended strictly afterqualifierInfo; the existing nested-context test plus a new ordering test pin the layout.
Open Questions
None. The issue's proposed change is unambiguous and the helpers it references already exist.