[BUG] Bash Tool Incorrectly Processes Glob Patterns When Used With Pipes
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
- Save the test commands to
/tmp/test_glob_bug.txt(file provided below) - In a new Claude Code session, send this exact prompt:
````
You do this Bash tool test - @/tmp/test_glob_bug.txt
- Claude Code will read the file and attempt to execute each command using its Bash tool
- 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
*.mdshould be expanded to match all.mdfiles - The
lscommand 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 -cworkaround
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
*.mdis incorrectly replaced with the literal string "glob" - The command becomes effectively
ls glob | wc -l lsfails with error:ls: cannot access 'glob': No such file or directory- The pipe to
wc -lcounts 0 lines from the error output - The result is
0with an error message on stderr
This affects multiple glob pattern types:
- Simple globs:
*.md→ becomesglob - Path globs:
scripts/*.py→ becomesglob - Character classes:
file[12].md→ remains as literal stringfile[12].md - Brace expansion:
file{1,2}.md→ remains as literal stringfile{1,2}.md - Range expansion:
file{1..3}.md→ remains as literal stringfile{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:
- File counting/statistics:
ls *.py | wc -l(count Python files) - File filtering:
ls *.log | grep error(search in log files) - Build processes:
find . -name "*.js" | xargs eslint(lint all JS files) - Data processing:
cat *.csv | awk '{sum+=$1} END {print sum}'(aggregate CSV data) - 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>&1redirections - 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()
This issue has 7 comments on GitHub. Read the full discussion on GitHub ↗