Claude Code Bash Patterns
Status: Production Ready ✅ Last Updated: 2025-11-07 Dependencies: Claude Code CLI (latest version) Official Docs: https://docs.claude.com/en/docs/claude-code/tools
Quick Start (10 Minutes)
1. Understanding the Bash Tool
The Bash tool is Claude Code's primary interface for executing command-line operations. Unlike specialized tools (Read, Grep, Glob), the Bash tool provides direct shell access for complex workflows.
Key Characteristics:
- Session Persistence: Commands run in a persistent bash session within a conversation
- Environment Inheritance: Inherits environment variables and working directory
- Output Limit: Truncates output at 30,000 characters
- Default Timeout: 2 minutes (configurable up to 10 minutes)
When to Use Bash Tool:
- ✅ Running CLI tools (git, npm, wrangler, gh, etc.)
- ✅ Command chaining (sequential operations)
- ✅ Process orchestration (build, test, deploy)
- ✅ Environment setup and management
When NOT to Use Bash Tool:
- ❌ Reading files → Use Read tool instead
- ❌ Searching file patterns → Use Glob tool instead
- ❌ Searching content → Use Grep tool instead
- ❌ Editing files → Use Edit tool instead
2. Basic Command Patterns
# Single command
npm install
# Sequential with && (stops on first failure)
npm install && npm run build
# Sequential with ; (continues regardless)
npm install ; npm run build
# Parallel execution (make multiple Bash tool calls)
# Call 1: git status
# Call 2: git diff
# Call 3: git log
Golden Rule: Use && when you care about failures, ; when you don't, and parallel calls when operations are independent.
3. Configure Your First Hook
Hooks let you run code before/after tool execution. Here's a simple audit logger:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"[$(date -Iseconds)] $(echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.tool_input.command')\" >> ~/.claude/bash-audit.log"
}
]
}
]
}
}
Save this to ~/.claude/settings.json to log every bash command with timestamp.
The Five Core Patterns
Pattern 1: Command Chaining
When to Use: Sequential operations where each depends on previous success
Syntax: command1 && command2 && command3
Example: Build and deploy workflow
npm install && npm run build && npx wrangler deploy
Why It Matters:
- Stops on first failure (prevents cascading errors)
- Maintains clean error messages (know exactly what failed)
- Saves tokens (no need to check status between commands)
Anti-Pattern: Using ; when you care about failures
# ❌ Wrong: Continues even if install fails
npm install ; npm run build
# ✅ Correct: Stops if install fails
npm install && npm run build
Advanced: Conditional execution with ||
# Run tests, or echo failure message
npm test || echo "Tests failed, not deploying"
# Try npm ci, fall back to npm install
npm ci || npm install
Pattern 2: Parallel Execution
When to Use: Independent operations that can run simultaneously
How: Make multiple Bash tool calls in a single message
Example: Git workflow pre-commit analysis
# Claude makes 3 parallel Bash calls in one message:
Call 1: git status
Call 2: git diff --staged
Call 3: git log -5 --oneline
Benefits:
- ~40% faster than sequential (no waiting between calls)
- Reduces context usage (all results arrive together)
- Better user experience (appears instant)
Important: Only parallelize truly independent operations. If Call 2 depends on Call 1's output, run sequentially.
Pattern 3: HEREDOC for Multi-Line Content
When to Use: Git commits, file creation, complex strings with newlines
Syntax: cat <<'EOF' ... EOF
Example: Git commit with detailed message
git commit -m "$(cat <<'EOF'
feat(auth): Add JWT verification middleware
Implement custom JWT template support for Clerk auth.
Extracts email and metadata claims for user context.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
Why Single Quotes: <<'EOF' prevents variable expansion in the content. Use <<"EOF" if you want variables to expand.
Common Mistake: Forgetting quotes around $() wrapper
# ❌ Wrong: Newlines lost
git commit -m $(cat <<'EOF'
Line 1
Line 2
EOF
)
# ✅ Correct: Preserves newlines
git commit -m "$(cat <<'EOF'
Line 1
Line 2
EOF
)"
Pattern 4: Output Capture and Processing
When to Use: Need to process command output before using it
Pattern: Command substitution with $()
Example: Get current branch name
BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Current branch: $BRANCH"
Example: Conditional logic based on output
if git diff --quiet; then
echo "No changes detected"
else
echo "Changes detected, running tests..."
npm test
fi
Limitation: Output truncated at 30,000 characters. For large outputs:
# Limit output
npm test 2>&1 | head -100
# Or save to file
npm test > test-results.txt && tail -50 test-results.txt
Pattern 5: Conditional Execution
When to Use: Different actions based on conditions
Pattern: Test commands with && / ||
Example: Run tests only if files changed
git diff --quiet || npm test
Example: Different commands based on file existence
[ -f package-lock.json ] && npm ci || npm install
Example: Multi-condition logic
if [ -f pnpm-lock.yaml ]; then
pnpm install
elif [ -f yarn.lock ]; then
yarn install
else
npm install
fi
Hooks: Advanced Automation
Hooks are shell commands or Claude prompts that run before/after tool execution. They're your security guards, cleanup crew, and automation helpers.
PreToolUse: The Security Guard
Purpose: Runs before tool execution, can block or modify behavior
Exit Codes:
0= Allow execution1= Block with generic error2= Block with custom error message (from stderr)
Use Case 1: Block Dangerous Commands
File: ~/.claude/hooks/dangerous-command-guard.py
#!/usr/bin/env python3
import json
import sys
import re
# Read hook input from stdin
data = json.load(sys.stdin)
command = data.get('tool_input', {}).get('command', '')
# Dangerous patterns to block
DANGEROUS = [
r'rm\s+-rf\s+/', # Delete root
r'dd\s+if=', # Disk operations
r'mkfs\.', # Format filesystem
r':()\{.*\}:', # Fork bomb
r'sudo\s+rm', # Sudo delete
r'git\s+push.*--force.*main', # Force push to main
]
for pattern in DANGEROUS:
if re.search(pattern, command):
print(f"BLOCKED: Dangerous command pattern '{pattern}'", file=sys.stderr)
sys.exit(2)
# Allow execution
sys.exit(0)
Settings Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/dangerous-command-guard.py"
}
]
}
]
}
}
Use Case 2: Log All Bash Commands
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"[$(date -Iseconds)] $(echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.tool_input.command')\" >> ~/.claude/bash-audit.log"
}
]
}
Result: Every bash command logged with timestamp to ~/.claude/bash-audit.log
Use Case 3: Enforce Package Manager
Check for lockfile and block wrong package manager:
#!/bin/bash
# File: ~/.claude/hooks/package-manager-enforcer.sh
COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.tool_input.command')
if [ -f pnpm-lock.yaml ] && echo "$COMMAND" | grep -qE '^(npm|yarn) '; then
echo "ERROR: This repo uses p