feat(tools): shell-first exploration with a bash-backed Bash tool - #59
Conversation
Drop list_files/search_files in favour of rg/find/ls via the shell tool, auto-approve and parallelise read-only commands, rename execute_command to Bash (legacy name still accepted), and run commands in bash (Git Bash on Windows) instead of $SHELL. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧪 PR Review is completed: Well-built shell-first redesign with a thoughtful conservative classifier, but the approval gate has two bypasses (fd --exec=… value-attached flags, uniq INPUT OUTPUT) and the new no-prompt path allows reading any file on the system, not just the workspace. Reviewed src/prompts/system.ts, src/utils/shell.ts, src/tools/schemas/index.ts, src/tools/schemas/bash.ts, src/tools/index.ts, src/tools/executors/executeCommand.ts, src/core/hooks.ts, test/read-only-command.test.ts, package.json: no issues found.
Skipped files
CHANGELOG.md: Skipped file patternREADME.md: Skipped file patterndocs/HOOKS.md: Skipped file pattern
⬇️ Low Priority Suggestions (2)
src/tools/readOnlyCommand.ts (2 suggestions)
Location:
src/tools/readOnlyCommand.ts(Lines 23-27)🔴 Security
Issue: The
fdunsafe-flag regex is anchored with$, but fd (clap-based) accepts=-attached and short-attached value syntax:fd --exec=rm .andfd --exec-batch=rm .do not match^(--exec|--exec-batch)$and are classified read-only — arbitrary command execution with no approval prompt and in the parallel batch. The same$-anchor gap exists forsort -oout.txt,tree -ofile, andgit diff -Oorderfile(getopt/clap attached short values).Fix: Drop the end anchor for value-taking flags so any attached form matches (no other flag of these tools shares these prefixes, so there are no false positives).
Impact: Closes an arbitrary-command-execution bypass of the approval gate.
- fd: /^(-x|-X|--exec|--exec-batch)$/, - rg: /^(--pre|--pre-glob|--hostname-bin)(=|$)/, - sort: /^(-o|--output)(=|$)/, - tree: /^-o$/, - git: /^(--output|--ext-diff|--textconv|-O|--open-files-in-pager)(=|$)/, + fd: /^(-x|-X|--exec|--exec-batch)/, + rg: /^(--pre|--pre-glob|--hostname-bin)/, + sort: /^(-o|--output)/, + tree: /^-o/, + git: /^(--output|--ext-diff|--textconv|-O|--open-files-in-pager)/,Location:
src/tools/readOnlyCommand.ts(Lines 11-11)🟠 Security
Issue:
uniqis listed as read-only, but coreutilsuniq [OPTION]... [INPUT [OUTPUT]]writes to the OUTPUT positional argument:uniq a boverwritesbwith no redirection and no unsafe flag, so it passes the classifier and silently overwrites an arbitrary file without an approval prompt (e.g.uniq x ~/.bashrc).Fix: Per this file's own policy ("a false positive would run something unreviewed"), remove
uniqfrom the allow-list; piped... | uniq | sortusage just falls back to prompting.Impact: Eliminates an unreviewed file-overwrite primitive.
- "pwd", "echo", "sort", "uniq", "cut", "tr", "nl", "du", "df", "which", "basename", "dirname", "realpath", + "pwd", "echo", "sort", "cut", "tr", "nl", "du", "df", "which", "basename", "dirname", "realpath",
| const readOnly = !isDangerous && isReadOnlyCommand(String(args.command ?? "")) | ||
| needsApproval = isDangerous || !(readOnly || this.sessionApproveCommands || this.options.autoApproveSafeCommands) |
There was a problem hiding this comment.
🟠 Security / NEEDS DISCUSSION
Issue: The old logic prompted for every command in default mode; the new readOnly short-circuit means cat, head, git show, etc. never prompt — but isReadOnlyCommand does not scope paths to the workspace. cat ~/.ssh/id_rsa, cat ~/.aws/credentials, or rg secrets /etc run silently, pulling sensitive files outside the workspace into model context, from where they can be exfiltrated via web_fetch (a realistic prompt-injection chain, since tool results/file contents are untrusted input). The system prompt only says "prefer commands scoped to the workspace" — it is not enforced.
Fix (discussion): Consider requiring approval when a classified-read-only command references paths outside the workspace (home-dir shorthand, absolute paths outside cwd), or limiting the no-prompt fast path to workspace-relative invocations. This is a policy decision worth settling before merge rather than a one-line patch.
Impact: Prevents silent reads of credentials/system files outside the project.
| const readOnly = !isDangerous && isReadOnlyCommand(String(args.command ?? "")) | |
| needsApproval = isDangerous || !(readOnly || this.sessionApproveCommands || this.options.autoApproveSafeCommands) |
Summary
list_files/search_files; the model searches and lists withrg,find,ls,gitvia the shell tool (like Claude Code). The system prompt teaches the patterns.&&of these, no redirects or command substitution) skip approval and run in parallel. Anything unrecognised still prompts (src/tools/readOnlyCommand.ts).execute_commandtoBash. Old name still works for resumed sessions and hook matchers.$SHELL(zsh/sh fallback on POSIX; Git Bash on Windows,cmd.exeonly as a last resort). The prompt states the shell.Notes
list_files/search_filesexecutors are left in place for resumed sessions; they can be deleted in a follow-up.bench/would be the place to compare.Test plan
tsc --noEmitnode --import tsx --test test/*.test.ts(85 pass)🤖 Generated with Claude Code