Skip to content

Instantly share code, notes, and snippets.

@michaelewens
Last active February 9, 2026 16:58
Show Gist options
  • Select an option

  • Save michaelewens/ad65a0b08c14023f425b593ce1e9c860 to your computer and use it in GitHub Desktop.

Select an option

Save michaelewens/ad65a0b08c14023f425b593ce1e9c860 to your computer and use it in GitHub Desktop.
Claude Code PreToolUse hook: auto-approve safe Bash commands (ls, cp from screenshots)
#!/bin/bash
# PreToolUse hook: auto-approve safe Bash commands
# Input: JSON on stdin with tool_name and tool_input.command
# Output: JSON with hookSpecificOutput.permissionDecision = "allow" to approve
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# Always allow ls (read-only, safe)
if echo "$COMMAND" | grep -qE '^ls\b'; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "ls is read-only"
}
}'
exit 0
fi
# Allow cp FROM screenshots folder (may be preceded by cd ... &&)
CP_PART=$(echo "$COMMAND" | sed 's/^cd [^&]* && //')
if echo "$CP_PART" | grep -qE '^cp\b' && echo "$CP_PART" | grep -q '/Documents/screenshots/'; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "cp from screenshots folder"
}
}'
exit 0
fi
# Everything else: fall through to normal permission check
exit 0

Claude Code + macOS Screenshots: Complete Setup Guide

Tell Claude "look at the latest screenshot" and it just works — no pasting images, no copying file paths. It finds the most recent screenshot, reads it (Claude is multimodal), and can copy it into your project. Use cases: OCR, UI review, saving images into slides, checking designs, reading error messages.

Three things need to be configured to make this seamless. Each solves a different problem.

The Three Problems

Problem Cause Solution
Claude doesn't know where screenshots are No default behavior CLAUDE.md instructions
Permission prompts on Read/Glob No allow rules settings.json rules
Permission prompts on cp Bash(cp *) patterns break with paths containing spaces PreToolUse hook
cp fails with "No such file" macOS uses invisible Unicode U+202F before AM/PM in filenames Glob patterns in cp command (CLAUDE.md instructions)

Step 1: CLAUDE.md Instructions

Add this to your CLAUDE.md (global ~/.claude/CLAUDE.md or per-project). Replace the screenshots path with yours.

### Screenshots

**Folder:** `/Users/YOURNAME/Documents/screenshots/`

**When I say "recent screenshot", "latest screenshot", "look at the screenshot", or similar:**
1. **Glob** with pattern `*` and path `/Users/YOURNAME/Documents/screenshots/` to find the most recent file (sorted by modification time, most recent first)
2. **Read** that file to view it

**When copying a screenshot to another location:**
1. **Glob** to find the filename (never guess the exact timestamp)
2. **Bash cp** using glob patterns to match the file — do NOT use the full quoted filename

    # Correct: glob pattern avoids Unicode issue
    cp /Users/YOURNAME/Documents/screenshots/Screenshot*10.16*AM.png dest.png

    # Wrong: quoted exact path fails because macOS uses U+202F (narrow no-break space)
    # between time and AM/PM — Claude writes a regular space, so cp can't find the file
    cp "/Users/YOURNAME/Documents/screenshots/Screenshot 2026-02-09 at 10.16.30 AM.png" dest.png

**Why globs are mandatory:** macOS screenshot filenames contain a Unicode narrow no-break space (U+202F) before AM/PM. The Glob tool resolves this correctly, but when the path is passed as a quoted string to Bash cp, the Unicode character is replaced with a regular space, causing "No such file or directory."

What this does: Tells Claude where your screenshots live and how to find/copy them. Without this, Claude has no idea where to look.

macOS tip: You can change where screenshots are saved with defaults write com.apple.screencapture location ~/Documents/screenshots && killall SystemUIServer. This keeps them out of your Desktop clutter.

Step 2: settings.json Permission Rules

Add these to ~/.claude/settings.json under permissions.allow:

{
  "permissions": {
    "allow": [
      "Glob(*)",
      "Read(~/Documents/screenshots/**)"
    ]
  }
}

What this does: Lets Claude find and read screenshots without prompting. Glob(*) allows file discovery anywhere (it's read-only and safe). Read(~/Documents/screenshots/**) allows viewing screenshot images.

Note: Use the tilde ~ form for Read rules, not the full /Users/YOURNAME/... path. Claude Code matches Read permissions against tilde paths.

Step 3: PreToolUse Hook for cp Commands

This is the piece that took the most debugging. Claude Code's Bash(cp *) permission pattern in settings.json does not work for commands with long quoted paths containing spaces. No pattern variation fixes this — it's a fundamental limitation of the pattern matcher.

The solution: a PreToolUse hook that inspects the command and auto-approves safe operations.

Install the hook script

Save allow-screenshot-cp.sh (included in this gist) to ~/.claude/scripts/ and make it executable:

mkdir -p ~/.claude/scripts
# Save the script (see allow-screenshot-cp.sh in this gist)
chmod +x ~/.claude/scripts/allow-screenshot-cp.sh

Wire it up in settings.json

Add this under hooks in ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/YOURNAME/.claude/scripts/allow-screenshot-cp.sh",
            "timeout": 5000
          }
        ]
      }
    ]
  }
}

What the hook does

  • Reads the Bash command from stdin (JSON with tool_input.command)
  • Auto-approves all ls commands (read-only, always safe)
  • Auto-approves cp commands where the source is in /Documents/screenshots/
  • Handles chained commands like cd /screenshots && cp ... by stripping the cd prefix first
  • Everything else falls through to normal permission checks

Security note

The first version used \bcp\b (match cp anywhere in the command) instead of ^cp\b (must start with cp). That's dangerous — a command like rm -rf /important && cp /Documents/screenshots/foo bar would match both checks and auto-approve the rm. The fix: strip only a leading cd ... && prefix, then require the remainder to start with cp. This means cd /path && cp ... works, but rm ... && cp ... doesn't.

Test it

# Should output approval JSON:
echo '{"tool_name":"Bash","tool_input":{"command":"cp /Users/me/Documents/screenshots/Screenshot*10.16*AM.png dest.png"}}' | ~/.claude/scripts/allow-screenshot-cp.sh

# Should also approve (cd + cp pattern):
echo '{"tool_name":"Bash","tool_input":{"command":"cd /Users/me/Documents/screenshots && cp Screenshot*10.16*AM.png dest.png"}}' | ~/.claude/scripts/allow-screenshot-cp.sh

# Should output nothing (falls through — rm is not allowed):
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /important"}}' | ~/.claude/scripts/allow-screenshot-cp.sh

# Should also fall through (rm chained with cp — not safe):
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /important && cp /Documents/screenshots/foo bar"}}' | ~/.claude/scripts/allow-screenshot-cp.sh

How It Works End-to-End

When you say "look at the latest screenshot":

  1. Claude reads CLAUDE.md, knows to check the screenshots folder
  2. Glob(*, path=screenshots/) finds the most recent file — auto-approved by Glob(*)
  3. Read(screenshot.png) displays the image — auto-approved by Read(~/Documents/screenshots/**)

When you say "copy that screenshot into my slides folder":

  1. Glob resolves the filename and timestamp
  2. ls destination/ checks the target exists — auto-approved by PreToolUse hook
  3. cp /path/Screenshot*10.16*AM.png dest.png copies the file — auto-approved by PreToolUse hook (matches screenshots folder)

Zero permission prompts. Zero "file not found" errors.

Why Bash(cp *) Doesn't Work

The settings.json permission pattern Bash(cp *) is supposed to match any cp command. It works for simple cases like cp file1 file2. But it silently fails for:

cp "/Users/me/Documents/screenshots/Screenshot 2026-02-09 at 10.16.30 AM.png" "/Users/me/project/dest.png"

We tested every variation:

Pattern Result
Bash(cp *) Still prompted
Bash(cp) Still prompted
Bash(cp **) Still prompted

The pattern matcher can't handle quoted paths with spaces. PreToolUse hooks bypass this entirely because they parse the actual command string with real tools (jq + grep) instead of relying on glob matching.

Requirements

  • macOS (the Unicode issue is macOS-specific)
  • jq installed (brew install jq or it may already be at /usr/bin/jq)
  • Claude Code CLI
# Claude Code: Auto-approve safe Bash commands with PreToolUse hooks
## The Problem
Claude Code's `Bash(cp *)` permission patterns in `settings.json` don't reliably match commands with quoted paths containing spaces. On macOS, this means screenshot operations always prompt for permission.
**Bonus problem:** macOS screenshot filenames contain U+202F (Narrow No-Break Space) before AM/PM. Claude writes a regular space when constructing `cp` commands, so exact quoted paths fail with "No such file or directory" even after permission is granted.
## The Solution
A PreToolUse hook that:
- Auto-approves all `ls` commands (read-only, always safe)
- Auto-approves `cp` commands sourcing from the screenshots folder
- Falls through to normal permission checks for everything else
## Setup
### 1. Save the hook script
Save `allow-screenshot-cp.sh` to `~/.claude/scripts/` and make it executable:
```bash
chmod +x ~/.claude/scripts/allow-screenshot-cp.sh
```
### 2. Add the hook to settings.json
Add this to your `~/.claude/settings.json` under `hooks`:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/.claude/scripts/allow-screenshot-cp.sh",
"timeout": 5000
}
]
}
]
}
}
```
### 3. Tell Claude how to handle screenshots
Add this to your `CLAUDE.md`:
```markdown
When copying screenshots, use glob patterns in the cp command (not quoted exact paths).
macOS filenames contain Unicode U+202F before AM/PM that breaks exact path matching.
✅ cp /Users/me/Documents/screenshots/Screenshot*10.16*AM.png dest.png
❌ cp "/Users/me/Documents/screenshots/Screenshot 2026-02-09 at 10.16.30 AM.png" dest.png
```
## Why not just use settings.json?
`Bash(cp *)` in settings.json allow rules doesn't work for commands with long quoted paths containing spaces. Neither does `Bash(cp)`. The permission pattern matcher is too fragile for real-world file operations. PreToolUse hooks bypass this limitation entirely.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment