Critical Issue: Bash Pipe Permission Behavior is Unpredictable and Undocumented

Resolved 💬 3 comments Opened Dec 19, 2025 by ChadNedzlek Closed Dec 22, 2025

Critical Issue: Bash Pipe Permission Behavior is Unpredictable and Undocumented

The Core Problem

After hours of systematic testing, it is impossible to predict how Claude Code handles piped bash commands. The permission system's behavior with pipes is undocumented, inconsistent, and prevents users from having explicit security control.

What Should Be Documented

For a pattern like Bash(git status:*), users need to know:

  • ✅ Does it match git status | head?
  • ✅ Does it match git status | awk '{print $1}'?
  • ✅ Does it match git status | xargs echo?
  • ✅ What happens when the right side of the pipe is dangerous?

Currently: No documentation exists explaining this behavior.

Contradictory Test Results

After 35+ systematic tests, here's what we observed:

Approved Command Piped to Various Utilities

| Command | Result | Explanation |
|---------|--------|-------------|
| pwd \| head -1 | ✅ APPROVED | Works |
| pwd \| sed 's/a/b/' | ✅ APPROVED | Works |
| pwd \| awk '{print $1}' | ❌ DENIED | Why? Both pwd and awk behavior unclear |
| pwd \| grep foo | ✅ APPROVED | Works |
| pwd \| cut -d/ -f2 | ✅ APPROVED | Works |

Question: All of these start with approved pwd. Why does piping to awk fail but piping to sed works?

xargs Behavior (Completely Unpredictable)

| Command | Result | Explanation |
|---------|--------|-------------|
| echo test \| xargs echo Got: | ✅ APPROVED | Works |
| echo . \| xargs ls | ❌ DENIED | ls is approved, so why denied? |
| echo x \| xargs pwd | ❌ DENIED | pwd is approved, so why denied? |
| echo x \| xargs -I {} echo {} | ✅ APPROVED | Works with -I flag |

Question: Why does xargs work with echo but not with ls or pwd, when all three are approved commands?

Multiple Pipes

| Command | Result |
|---------|--------|
| echo \| grep \| sed | ✅ APPROVED |
| echo \| head \| awk | ❌ DENIED |
| pwd \| head \| tail | ✅ APPROVED |

Question: What determines if a multi-pipe command is approved?

Inconsistency Between Modes

Interactive Mode vs --print Mode

Same command, different behavior:

# Interactive mode
echo | awk '{print $1}'     # ❌ Required permission prompt

# --print mode
claude --print "Run: echo | awk '{print $1}'"  # ✅ Actually executed

Question: Why do permission rules differ between interactive and --print modes?

Impact on Security

Users cannot:

  1. Predict behavior - Don't know if a command will prompt or execute
  2. Audit permission surface - Can't determine what's actually approved
  3. Control pipeline composition - Can't allow git status | head while denying git status | awk
  4. Trust the system - Inconsistent behavior undermines confidence

What Users Need

1. Documented Pipe Matching Algorithm

Clear specification:

Pattern: Bash(command:*)

Pipe Behavior:
- command | <approved-utility>  → APPROVED (list approved utilities)
- command | <blocked-utility>   → DENIED (list blocked utilities)
- command | xargs <any-command> → Check if <any-command> is approved
- command && other              → DENIED (command boundary)
- command > file                → Requires file write permission

2. Pipeline-Aware Permission Patterns

Let users control both sides of pipes:

{
  "allow": [
    "Bash(git status:*)|Bash(head:*)",    // Explicit: git status piped to head is OK
    "Bash(git status:*)|Bash(grep:*)",    // Explicit: git status piped to grep is OK
    "Bash(dotnet build:*)|Bash(tail:*)"   // Explicit: dotnet build piped to tail is OK
  ],
  "deny": [
    "Bash(*)|Bash(awk:*)",                // Block awk on right side of ANY pipe
    "Bash(*)|Bash(perl:*)",               // Block perl on right side of ANY pipe
    "Bash(*)|Bash(xargs rm*)",            // Block dangerous xargs combinations
    "Bash(*)|Bash(sed -i*)",              // Block in-place sed anywhere
    "Bash(*)|Bash(* > *)"                 // Block file redirection
  ]
}

Problem: Currently if git status is approved, you have NO CONTROL over what it can be piped to. User cannot allow git status | head while denying git status | awk.

3. Permission Testing Tool

# Test if command would be approved without executing it
claude --test-permission "git status | head -5"
# Output: APPROVED
#   Matched rule: Bash(git status:*)
#   Pipe: head is in default approved list
#   Result: Command would execute without prompt

claude --test-permission "echo | awk '{print $1}'"
# Output: DENIED
#   Matched rule: Bash(echo:*) [default]
#   Pipe: awk is in default blocked list
#   Result: Would require permission prompt

# List all effective permissions including defaults
claude --list-permissions --include-defaults
# Output:
#   From settings.json:
#     - Bash(git status:*)
#     - Bash(pwd)
#
#   Built-in defaults:
#     - Bash(echo:*)
#     - Bash(cat:*)
#     - Bash(head:*)
#
#   Built-in blocks:
#     - Bash(awk:*) when not first command
#     - Bash(perl:*)
#     - Bash(sed:*) when first command

4. Consistent Behavior

Permission matching should work identically in:

  • Interactive mode
  • --print mode
  • Agent mode
  • Any other execution context

Currently, behavior differs between modes, which is confusing and undermines trust.

Expected Behavior

# User has: Bash(git status:*) in settings.json

git status              → ✅ APPROVED (matches pattern)
git status --short      → ✅ APPROVED (matches pattern with :*)
git status | head       → ✅ APPROVED (head in default safe list)
git status | awk        → ❌ DENIED (awk not in safe list)
echo | xargs git status → ❌ DENIED (git status not first command)

User understands exactly why each command was approved/denied.

Actual Behavior

# User has: Bash(git status:*) in settings.json

git status              → ✅ Works (as expected)
git status | head       → ✅ Works (why?)
pwd | sed 's/a/b/'      → ✅ Works (why? sed not in settings)
pwd | awk '{print $1}'  → ❌ Denied (why? awk vs sed difference?)
echo | xargs echo       → ✅ Works (why?)
echo | xargs pwd        → ❌ Denied (why? pwd is approved!)

User has no idea why different pipes behave differently.

Real-World Impact

Scenario: User wants to auto-approve dotnet build but examine output:

dotnet build            # Want: auto-approved
dotnet build | tail -20 # Want: auto-approved (safe filter)
dotnet build | sed ...  # Want: denied (transformation)
dotnet build | awk ...  # Want: denied (complex processing)

Current Reality: Cannot configure this. Pipe behavior is opaque and uncontrollable.

Request

Please provide:

  1. Documentation of complete permission matching algorithm (especially pipes)
  2. Testing tool to check permissions without executing (--test-permission)
  3. Pipeline-aware patterns to control left and right side of pipes separately
  4. Explicit mode to disable all built-in defaults
  5. Audit logging of executed commands

Without this, security-conscious users cannot safely use Claude Code with auto-approvals.

---

Priority: Critical - Security and trust issue
Category: Permission System, Documentation Gap
Affects: All users who configure custom permission rules

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗