[BUG] "Always Allow" permission never matches compound Bash commands with quoted paths, pipes, and .exe on Windows

Status Open
Reported on v2.1.50
Maintainer reply None cached
Activity 7 comments · opened Feb 22, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Selecting "Always Allow" for a compound Bash command (quoted path with spaces + pipe + && + .exe) writes an entry to settings.local.json, but Claude Code continues to prompt for permission on every subsequent execution.

Command that triggers the issue:

"C:/Program Files/LLVM/bin/clang.exe" -std=c11 -Wall -g trigex.c -o trigex.exe 2>&1 | tail -5 && ./trigex.exe 2>&1

This command has multiple characteristics that likely each contribute to matching failure:

  1. Executable path contains spaces → must be quoted with double quotes
  2. Contains pipe operator (|) and output redirection (2>&1)
  3. Compound command chained with &&
  4. Uses .exe extension on Windows

What Should Happen?

After selecting "Always Allow" once, the same command should be auto-approved on all future executions without prompting again.

Steps to Reproduce

  1. Ask Claude Code to run:

``
"C:/Program Files/LLVM/bin/clang.exe" -std=c11 -Wall -g trigex.c -o trigex.exe 2>&1 | tail -5 && ./trigex.exe 2>&1
``

  1. When prompted, select "Always Allow"
  2. Verify the permission was written to .claude/settings.local.json
  3. Ask Claude Code to run the exact same command again
  4. Observe: permission prompt appears again despite the entry in settings.local.json

Evidence

  • settings.local.json is updated after step 2 (the permission IS written — unlike #16762)
  • The written permission entry is likely the full literal command string (see #21845 for the pattern-vs-literal issue)
  • The literal string fails to re-match on the next execution, presumably because:
  • The .exe path resolver expands the quoted path before pattern matching (#27537 hypothesis)
  • And/or the compound command (|, &&, 2>&1) is not parsed consistently between storage and matching

Related Issues

  • #27537: .exe pattern matching fails on Windows (single command, no pipes tested)
  • #21845: Permission allow-list stores full literal commands instead of patterns (macOS)
  • #16762: "Always Allow" not persisted to settings at all (different root cause — writing fails)

This report extends all three: on Windows, compound commands with a quoted .exe path and pipes are stored as literals (#21845 behavior), AND the .exe path-with-spaces matching fails (#27537 behavior), making it impossible to use "Always Allow" for any realistic compile-and-run workflow.

Error Messages/Logs

No error shown — Claude Code silently re-prompts without indication that the stored permission failed to match.

Claude Model

claude-sonnet-4-6

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.50 (Claude Code)

Platform

Anthropic API

Operating System

Windows 10 Pro (Build 10.0.19045)

Terminal/Shell

Windows Terminal / Git Bash (bash)

Additional Information

This is particularly disruptive for iterative development workflows (e.g., compile + run cycles) where the same compound command is executed many times per session. The only current workaround is manually approving every single invocation.

View original on GitHub ↗

6 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/27537
  2. https://github.com/anthropics/claude-code/issues/16561
  3. https://github.com/anthropics/claude-code/issues/25441

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

jaried · 6 months ago

Thank you for the triage! However, this issue is not a duplicate of the three listed:

vs #27537 (Bash permission pattern fails to match commands with .exe extension):

  • #27537 covers a simple .exe command (e.g., mytool.exe /arg). My issue involves a compound command combining a quoted path with spaces, pipe operator (|), output redirection (2>&1), and && chaining — none of which are tested in #27537.

vs #16561 (Feature: Parse compound Bash commands and match each component against permissions):

  • #16561 is a feature request for a new parsing behavior. My issue is a bug: the existing "Always Allow" mechanism silently writes to settings.local.json but the entry never matches on subsequent executions — the user is never told the stored rule is ineffective.

vs #25441 (Bash permission wildcards don't match multiline/heredoc commands):

  • #25441 is about heredoc/multi-line constructs. My issue is about a single-line compound command using |, &&, and 2>&1 with a quoted path containing spaces.

The distinct failure scenario here is the combination:

"C:/Program Files/LLVM/bin/clang.exe" -std=c11 ... 2>&1 | tail -5 && ./trigex.exe 2>&1
  • Quoted executable path with spaces
  • Pipe operator and &&
  • .exe extension on Windows

Each factor alone may have a related issue; the combination causing silent write-then-fail is the new ground this issue covers.

yurukusa · 5 months ago

Hooks bypass all the "Always Allow" pattern matching bugs (quoted paths, pipes, .exe on Windows):

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
CLEAN=$(echo "$COMMAND" | sed 's/^\s*//; s/\.exe\b//gi; s/"//g' | 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}' | sed 's|.*/||')  # basename (handles paths)
    case "$BASE" in
        cd|ls|dir|cat|head|tail|grep|find|stat|wc|echo|printf|test) ;;
        git|npm|npx|node|python|python3|pip|make|cargo) ;;
        sort|uniq|cut|tr|awk|sed|jq|tee|xargs) ;;
        mkdir|touch|pwd|which|where|type) ;;
        *) ALL_SAFE=false; break ;;
    esac
done <<< "$PARTS"
if [ "$ALL_SAFE" = true ]; then
    jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"Allowed (handles Windows paths/pipes/quotes)"}}'
fi
exit 0

The hook strips .exe extensions, normalizes quotes, and uses basename to handle full paths — so "C:\Program Files\Git\bin\git.exe" status matches the same as git status.

jeffAuston · 4 months ago

Adding data from a multi-session workflow. We built a PermissionRequest hook that logs every permission prompt to a database.

35 prompts in 2 days for source ../venv/bin/activate && python3 <script> across 3 concurrent sessions:

| Session | Prompts | Period |
|---------|---------|--------|
| Session 1 | 26 | 1 day |
| Session 2 | 8 | 2 days |
| Session 3 | 1 | 1 day |

Both Bash(source:*) and Bash(python3:*) are in the allowlist. The approval does not persist within a session — the same exact command re-prompts multiple times in the same session.

This is the most common compound command in our workflow (every test run, every research script). At 3 concurrent sessions, it's 10-15 unnecessary prompts per day.

Originally filed as #42563 — closing that as a duplicate of this issue.

mkim0710 · 4 months ago

Adding more reproduction cases from a systematic audit of 4 sessions (~200 allow patterns):

5. Command substitution ($(...))

echo "RESUME_MARKER_260402T$(date +%H%M%S)"

Bash(echo *) is in the allow list.

6. Long SSH with embedded heredoc

/c/Windows/System32/OpenSSH/ssh.exe mkim@100.76.113.12 "mkdir -p ~/.tmux/scripts && cat > ~/.tmux.conf << 'TMUXEOF' ..."

Bash(/c/Windows/System32/OpenSSH/ssh.exe *) is in the allow list. Confirmed in a brand-new session — not mid-session cache.

7. Redirects on otherwise-allowed commands

ls -la ~/.codex/ 2>/dev/null | head -5
git -C "D:/D_Repositories/MHdotfiles" push 2>&1
stat -c '%Y %n' "D:/D_Repositories/..."

All three base commands (ls, git, stat) are in the allow list with * wildcard.

Environment

  • Claude Code v2.1.87, Desktop app, Windows 10, acceptEdits mode
  • ~200 allow patterns + PreToolUse hook
  • Tested across 4 sessions over 3 days

Closing my duplicate #43531 in favor of this issue.

Domiii · 3 months ago

Same here, but on Linux.

I can easily repro this.

Do you want us to debug this? I'm happy to work out a PR if you guys are even open to accepting any.

Showing cached comments. Read the full discussion on GitHub ↗