[BUG] Bash Tool Incorrectly Processes Glob Patterns When Used With Pipes

Resolved 💬 7 comments Opened Aug 15, 2025 by Rokurolize Closed Aug 18, 2025

Environment

  • Platform (select one):
  • [x] Anthropic API
  • [ ] AWS Bedrock
  • [ ] Google Vertex AI
  • [ ] Other: <!-- specify -->
  • Claude CLI version: 1.0.81 (Claude Code)<!-- output of claude --version -->
  • Operating System: WSL2 Ubuntu 22.04 on Windows 11<!-- e.g. macOS 14.3, Windows 11, Ubuntu 22.04 -->
  • Terminal: Bash<!-- e.g. iTerm2, Terminal App -->

Bug Description

<!-- A clear and concise description of the bug -->
The Bash tool in Claude Code incorrectly processes glob patterns (wildcards like *.md, **/*.py, file[123].txt, {a,b,c}) when they appear in commands that contain pipes (|). Instead of expanding the glob pattern, the tool literally outputs the string "glob", causing commands to fail with "cannot access 'glob': No such file or directory" errors.

This bug affects ALL glob patterns but ONLY when pipes are present in the command. The same glob patterns work correctly when used without pipes, indicating this is a command parsing/reconstruction issue specific to piped commands.

Steps to Reproduce

  1. Save the test commands to /tmp/test_glob_bug.txt (file provided below)
  2. In a new Claude Code session, send this exact prompt:

``
You do this Bash tool test - @/tmp/test_glob_bug.txt
``

  1. Claude Code will read the file and attempt to execute each command using its Bash tool
  2. Observe that glob patterns with pipes fail while the same patterns without pipes work

Test file contents (/tmp/test_glob_bug.txt):

# Setup test environment
mkdir -p /tmp/glob_test && cd /tmp/glob_test
touch file1.md file2.md file3.txt
mkdir -p dir && touch dir/script1.py dir/script2.py

# Test 1: Wildcard without pipe (works) ✅
ls *.md

# Test 2: Wildcard with pipe (fails - becomes "glob") ❌
ls *.md | wc -l

# Test 3: Path wildcard with pipe (fails - becomes "glob") ❌  
ls dir/*.py | wc -l

# Test 4: Character class with pipe (fails - remains literal) ❌
ls file[12].md | wc -l

# Test 5: Brace expansion with pipe (fails - remains literal) ❌
ls file{1,2}.md | wc -l

# Test 6: Workaround using bash -c (works) ✅
bash -c 'ls *.md | wc -l'

# Cleanup
cd /tmp && rm -rf glob_test

Expected Behavior

<!-- What you expected to happen -->
When running ls *.md | wc -l:

  • The glob pattern *.md should be expanded to match all .md files
  • The ls command should list the matching files
  • The output should be piped to wc -l
  • The result should be 2 (counting the two .md files)

This is standard bash behavior that works correctly in:

  • Native bash shell
  • Claude Code versions 1.0.60 and earlier
  • Claude Code when using the bash -c workaround

Actual Behavior

<!-- What actually happened -->

Excerpt from conversation file:

> /hooks 
  ⎿  All hooks have been disabled

> You do this Bash tool test - @/tmp/test_glob_bug.txt
  ⎿  Read ../../../../../tmp/test_glob_bug.txt (25 lines)

● I'll run these test commands to demonstrate the glob expansion behavior with pipes in the Bash tool.

● Bash(mkdir -p /tmp/glob_test && cd /tmp/glob_test && touch file1.md file2.md file3.txt && mkdir -p dir
       && touch dir/script1.py dir/script2.py && pwd)
  ⎿  /tmp/glob_test

● Bash(cd /tmp/glob_test && ls *.md)
  ⎿  file1.md
     file2.md

● Bash(cd /tmp/glob_test && ls *.md | wc -l)
  ⎿  0                                                  
     ls: cannot access 'glob': No such file or directory

● Bash(cd /tmp/glob_test && ls dir/*.py | wc -l)
  ⎿  0                                                  
     ls: cannot access 'glob': No such file or directory

● Bash(cd /tmp/glob_test && ls file[12].md | wc -l)
  ⎿  0                                                         
     ls: cannot access 'file[12].md': No such file or directory

● Bash(cd /tmp/glob_test && ls file{1,2}.md | wc -l)
  ⎿  0                                                          
     ls: cannot access 'file{1,2}.md': No such file or directory

● Bash(cd /tmp/glob_test && bash -c 'ls *.md | wc -l')
  ⎿  2

● Bash(cd /tmp && rm -rf glob_test)
  ⎿  (No content)

● The tests confirm a bug in the Bash tool: glob patterns are incorrectly converted to the literal
  string "glob" when used with pipes. The workaround is to wrap commands in bash -c 'command' when glob
  expansion with pipes is needed.

When running ls *.md | wc -l:

  • The glob pattern *.md is incorrectly replaced with the literal string "glob"
  • The command becomes effectively ls glob | wc -l
  • ls fails with error: ls: cannot access 'glob': No such file or directory
  • The pipe to wc -l counts 0 lines from the error output
  • The result is 0 with an error message on stderr

This affects multiple glob pattern types:

  • Simple globs: *.md → becomes glob
  • Path globs: scripts/*.py → becomes glob
  • Character classes: file[12].md → remains as literal string file[12].md
  • Brace expansion: file{1,2}.md → remains as literal string file{1,2}.md
  • Range expansion: file{1..3}.md → remains as literal string file{1..3}.md

Additional Context

<!-- Add any other context about the problem here, such as screenshots, logs, etc. -->

Root Cause Analysis

The bug occurs in the command reconstruction function (FMB):

} else if (t$0(G)) {
  Z.push(G.op);  // BUG: pushes "glob" instead of G.pattern
}

The shell-quote correctly parses glob patterns into tokens like:

{ op: 'glob', pattern: '*.md' }

However, the reconstruction only uses the op field ("glob") instead of the pattern field that contains the actual glob pattern.

Bug History

  • Introduced: v1.0.61 (along with a similar stderr redirection bug)
  • Partially fixed: v1.0.77 (stderr bug fixed, glob bug remains)
  • Still present: v1.0.81 (current version)
  • Affects: All versions from v1.0.61 to at least v1.0.81

Impact Assessment

This bug impacts common command-line workflows:

  1. File counting/statistics: ls *.py | wc -l (count Python files)
  2. File filtering: ls *.log | grep error (search in log files)
  3. Build processes: find . -name "*.js" | xargs eslint (lint all JS files)
  4. Data processing: cat *.csv | awk '{sum+=$1} END {print sum}' (aggregate CSV data)
  5. Batch operations: ls *.jpg | while read f; do convert "$f" "${f%.jpg}.png"; done

The bug forces users to use workarounds for basic shell operations, reducing productivity and requiring knowledge of the bug's existence.

Workarounds

Wrap in bash -c:

bash -c 'ls *.md | wc -l'

Proposed Fix (?)

Modify the reconstruction function to use the pattern field:

} else if (t$0(G)) {
  Z.push(G.pattern || G.op);  // Use pattern if available, fallback to op
}

Test Coverage Recommendations

To prevent regression, test cases should include:

  • Simple globs with pipes: *.txt | grep foo (becomes "glob")
  • Path globs with pipes: dir/*.py | wc -l (becomes "glob")
  • Character classes with pipes: file[0-9].txt | sort (remains literal)
  • Brace expansion with pipes: file{a,b,c}.txt | head (remains literal)
  • Range expansion with pipes: file{1..5}.txt | wc -l (remains literal)
  • Multiple globs: *.md *.txt | sort
  • Nested globs: **/*.js | grep function
  • Globs with spaces in paths: "my dir"/*.txt | wc -l

References

  • Similar issue with stderr redirection (fixed in v1.0.77): The same parsing/reconstruction bug affected 2>&1 redirections
  • Related code location: Function FMB in cli.js, token reconstruction logic

Reproducibility

This bug is reproducible across all affected versions (v1.0.61-v1.0.81) and occurs consistently whenever glob patterns are used with pipes in the Bash tool.

Hook script

Hook command: python3 /home/ubuntu/.claude/hooks/bash_glob_validator_v81.py

bash_glob_validator_v81.py:

#!/usr/bin/env python3
"""
Claude Code Hook: Bash Glob Expansion Bug Validator (v1.0.81)
=====================================================================
This hook validates bash commands for the glob expansion bug that remains
unfixed in Claude Code v1.0.81.

FIXED IN v1.0.77+ (NOT handled by this hook):
- Standard error redirection (2>&1, 1>&2, etc.) ✅
- File descriptor redirections with /dev/null ✅
- Combined redirections (&>, >&, etc.) ✅

STILL BROKEN IN v1.0.81 (handled by this hook):
- Glob expansion (*.md becomes literal "glob") ❌
- Character class patterns ([abc], [0-9]) remain literal ❌
- Brace expansion ({a,b}, {1..10}) remain literal ❌

NOTE: This validator is conservative - it may flag quoted patterns.
Use '# claude-bug-ok' to bypass when you verify a command is safe.
"""

import json
import re
import sys


def _validate_bash_operators(command: str) -> tuple[bool, str]:
    """
    Check if command contains shell operator patterns that are STILL BROKEN in v1.0.81.

    This validator is intentionally conservative and may flag quoted patterns.

    Args:
        command: The bash command to validate

    Returns:
        (is_valid, error_message) tuple

    """
    # Allow explicit bypass
    if "# claude-bug-ok" in command or "# claude-stderr-ok" in command:
        return True, ""

    # Check if already safely wrapped in bash -c with quotes
    cmd_stripped = command.strip()
    if cmd_stripped.startswith("bash -c "):
        rest = cmd_stripped[8:].strip()
        if (rest.startswith("'") and rest.endswith("'")) or (rest.startswith('"') and rest.endswith('"')):
            return True, ""

    # Must have a pipe for the bug to trigger
    if "|" not in command:
        return True, ""

    # Conservative check - doesn't parse quotes perfectly

    # Patterns that become "glob" when piped
    glob_patterns = [
        (r"\*", "wildcard (*)"),
        (r"\?", "question mark (?)"),
    ]

    # Patterns that remain literal when piped
    literal_patterns = [
        (r"\[[^\]]+\]", "character class ([...])"),
        (r"\{[^}]+\}", "brace expansion ({...})"),
    ]

    # Check for problematic patterns (conservative - flags even quoted patterns)
    for pattern_str, description in glob_patterns:
        if re.search(pattern_str, command):
            escaped_command = command.replace("'", "'\\''")
            safe_command = f"bash -c '{escaped_command}'"

            return False, (
                f"⚠️ Claude Code v1.0.81 bug: {description} becomes literal 'glob' with pipe\n"
                f"The pattern will be replaced with the string 'glob' causing an error.\n\n"
                f"Workaround:\n"
                f"{safe_command}\n\n"
                f"Or add '# claude-bug-ok' comment to bypass this check"
            )

    for pattern_str, description in literal_patterns:
        if re.search(pattern_str, command):
            escaped_command = command.replace("'", "'\\''")
            safe_command = f"bash -c '{escaped_command}'"

            return False, (
                f"⚠️ Claude Code v1.0.81 bug: {description} remains literal with pipe\n"
                f"The pattern won't be expanded and will be treated as a literal filename.\n\n"
                f"Workaround:\n"
                f"{safe_command}\n\n"
                f"Or add '# claude-bug-ok' comment to bypass this check"
            )

    return True, ""


def main() -> None:
    try:
        input_data = json.load(sys.stdin)
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
        sys.exit(1)

    tool_name = input_data.get("tool_name", "")
    if tool_name != "Bash":
        sys.exit(0)

    tool_input = input_data.get("tool_input", {})
    command = tool_input.get("command", "")

    if not command:
        sys.exit(0)

    # Validate the command for REMAINING shell operator issues
    is_valid, error_message = _validate_bash_operators(command)

    if not is_valid:
        print(error_message, file=sys.stderr)
        # Exit code 2 blocks tool call and shows stderr to Claude
        sys.exit(2)


if __name__ == "__main__":
    main()

View original on GitHub ↗

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