[FEATURE] Permission system should evaluate piped commands independently, not as a single string

Status Fixed / completed
Maintainer reply ✓ Yes — bcherny
Activity 8 comments · opened Mar 11, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

The current Bash permission pattern matching treats an entire pipeline as one string, meaning Bash(tee:*) doesn't match something | tee logfile because the command doesn't start with tee. This is both a UX problem (the "don't ask again" suggestion is wrong) and a conceptual one.
The core issue: piping is data routing, not execution risk. The risk surface is in what each individual command does. rg foo | tee out.txt is not more dangerous than rg foo — tee just redirects stdout to a file.
Related: #29967 documents the symptom; this is the underlying design issue.

Proposed Solution

each subcommand in a pipeline should be evaluated independently against the allowlist. Only prompt if any individual subcommand isn't covered. This matches the actual threat model (which commands run, not how their output is connected).

Alternative Solutions

n/a

Priority

Low - Nice to have

Feature Category

Interactive mode (TUI)

Use Case Example

n/a

Additional Context

_No response_

View original on GitHub ↗

8 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/16561
  2. https://github.com/anthropics/claude-code/issues/29967
  3. https://github.com/anthropics/claude-code/issues/11775

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

ZebZeb13 · 5 months ago

I'd like to extend this request beyond pipes to all shell operators: &&, ;, and ||.

My use case:

I have Bash(git *) in my global allow list. When Claude Code runs a single git command, it's auto-allowed as expected. But when it chains commands like:

```bash
git stash pop && git add docker/dynamodb/files.json && git commit --amend --no-edit

I get a permission prompt — even though all three sub-commands individually match Bash(git *).

This happens frequently because Claude Code itself generates chained commands for good reasons:

  • cd doesn't persist between Bash calls, so cd dir && command is mandatory
  • Sequential dependencies like git add && git commit only make sense together
  • Chaining reduces round-trips and token usage

The current behavior creates a frustrating gap: the permission system correctly blocks chains containing unauthorized commands (good), but doesn't allow chains where every sub-command is already authorized.

Proposed behavior:

Split on shell operators (&&, ;, ||, |), check each sub-command against the permission rules independently, and auto-allow the full chain only if every part matches. This would unify the handling for both pipes and logical operators.

This is essentially the inverse of the fix in #270 — that issue ensured users are prompted for all commands in a chain, this one asks that the allow rules be evaluated with the same granularity.

suchapalaver · 5 months ago

Still seeing this on v2.1.79. Both cargo and tail are in my global settings.json allow list, but cargo clippy --all-targets --all-features -- -D warnings 2>&1 | tail -30 still prompts for approval. Running cargo clippy without the pipe goes through fine.

The fix claimed in #1271 (v1.0.110) doesn't appear to have fully resolved this.

yurukusa · 5 months ago

/tmp/gh-comment-body.txt

yurukusa · 5 months ago

Hooks already evaluate piped commands independently:

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
CLEAN=$(echo "$COMMAND" | sed 's/#.*$//' | tr '\n' ' ')
PARTS=$(echo "$CLEAN" | sed 's/\s*&&\s*/\n/g; s/\s*||\s*/\n/g; s/\s*;\s*/\n/g; s/\s*|\s*/\n/g')
ALL_SAFE=true
while IFS= read -r part; do
    part=$(echo "$part" | sed 's/^\s*//;s/\s*$//')
    [ -z "$part" ] && continue
    BASE=$(echo "$part" | awk '{print $1}')
    case "$BASE" in
        cd|ls|cat|head|tail|grep|rg|find|sort|uniq|cut|tr|awk|sed|jq|tee|xargs|wc) ;;
        echo|printf|true|false|test|env|date) ;;
        git|npm|node|python*) ;;
        *) ALL_SAFE=false; break ;;
    esac
done <<< "$PARTS"
if [ "$ALL_SAFE" = true ]; then
    jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"All compound components individually safe"}}'
fi
exit 0

Each component (ls, grep, sort, wc) is evaluated independently. If all are in your allow list, the entire compound command is auto-approved. No "single string" evaluation bug.

olexiy-dmytrash · 5 months ago

Another real-world example hitting this on v2.1.81, Windows 11 (Git Bash):

az monitor app-insights query --app my-func --resource-group my-rg   --analytics-query "traces | where timestamp > ago(5m) | take 20"   -o json 2>&1 | python -c "import sys,json; ..."

Both Bash(az:*) and Bash(python:*) are in global settings.json, yet the compound command prompts for approval every time.

This pattern (CLI tool producing JSON piped to python/jq for formatting) is extremely common in Azure/cloud workflows. Having to approve it manually on every invocation significantly slows down development.

+1 for evaluating each subcommand in a pipeline independently.

bcherny collaborator · 4 months ago

Thanks for the report. This is a duplicate of #29967 — consolidating tracking there.

bcherny collaborator · 14 days ago

This is how permissions already work: Claude Code splits Bash commands on shell operators (|, |&, &&, ||, ;, &, newlines) and checks each piece against your rules on its own. So rg foo | tee out.txt is allowed when both Bash(rg *) and Bash(tee *) are covered, and choosing "don't ask again" saves a separate rule for each piece that needed approval rather than the whole pipeline string.

Docs: https://code.claude.com/docs/en/permissions#compound-commands

If you're seeing a specific pipeline still prompt with matching rules for every segment, that would be a bug; please open a new issue with the exact command and rules. Closing this one as already available.

🤖 Generated with Claude Code