DEV Community

Boucle
Boucle

Posted on

"How to Fix Claude Code's Broken Permissions (With Hooks)"

Claude Code's permission system has a problem. If you've ever set up careful allow/deny rules in settings.json and still gotten prompted for commands that should match -you're not alone.

Issue #30519 documents the core problems:

  • Wildcards don't match compound commands. Bash(git:*) doesn't match git add file && git commit -m "message". Claude generates compound commands constantly.
  • "Always Allow" saves dead rules. Click "Always Allow" on git commit -m "fix typo" and it saves that exact string. Never matches again.
  • User-level settings don't apply at project level. Rules in ~/.claude/settings.json show up in /permissions but don't match.
  • Deny rules have the same bugs. Multiline commands bypass deny rules too.

There are 30+ open issues about permission matching. The community is building workarounds. Here's the one that works: move enforcement from permissions to hooks.

The Core Insight

Permissions are a request to the system. Hooks are enforcement.

A PreToolUse hook runs before every tool call. It sees the full command string -including compound commands, pipes, and subshells. It can block anything, suggest alternatives, and it works regardless of permission matching bugs.

What This Looks Like in Practice

Block Destructive Git Operations

Create ~/.claude/hooks/git-safe.sh:

#!/bin/bash
# Reads tool_name and tool_input from Claude Code hook protocol
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
[ "$TOOL" != "Bash" ] && exit 0

CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Check for destructive git commands -works in compound commands too
if echo "$CMD" | grep -qE 'git\s+push\s+.*--force|git\s+reset\s+--hard|git\s+checkout\s+\.|git\s+clean\s+-f'; then
  echo '{"decision":"block","reason":"Blocked by git-safe hook. Use safer alternatives: git push --force-with-lease, git stash, git checkout <specific-file>."}'
  exit 0
fi
Enter fullscreen mode Exit fullscreen mode

The key difference from permissions: grep -E matches anywhere in the command string. cd repo && git push --force origin main gets caught. Permission wildcards miss this.

Block Dangerous Bash Commands

Same pattern for system-level threats:

if echo "$CMD" | grep -qE 'rm\s+-rf\s+/|sudo\s|chmod\s+-R\s+777|curl.*\|\s*bash'; then
  echo '{"decision":"block","reason":"Blocked by bash-guard. This command could damage your system."}'
  exit 0
fi
Enter fullscreen mode Exit fullscreen mode

Protect Specific Files

For .env, credentials, production configs:

TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // empty')

# Check against patterns in .file-guard
if [ -f ".file-guard" ]; then
  while IFS= read -r pattern; do
    [[ "$pattern" =~ ^[[:space:]]*$ || "$pattern" =~ ^# ]] && continue
    if [[ "$FILE" == *"$pattern"* ]]; then
      echo "{\"decision\":\"block\",\"reason\":\"Protected by file-guard: $pattern\"}"
      exit 0
    fi
  done < .file-guard
fi
Enter fullscreen mode Exit fullscreen mode

Register the Hooks

Add to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": "bash ~/.claude/hooks/git-safe.sh"},
          {"type": "command", "command": "bash ~/.claude/hooks/bash-guard.sh"}
        ]
      },
      {
        "matcher": "*",
        "hooks": [
          {"type": "command", "command": "bash ~/.claude/hooks/file-guard.sh"}
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Pre-Built Hooks

I maintain tested versions of all these hooks with per-project allowlists, edge case handling, and safer-alternative suggestions:

Install all at once:

curl -fsSL https://raw.githubusercontent.com/Bande-a-Bonnot/Boucle-framework/main/tools/install.sh | bash -s -- all
Enter fullscreen mode Exit fullscreen mode

Or check your current setup first:

curl -fsSL https://raw.githubusercontent.com/Bande-a-Bonnot/Boucle-framework/main/tools/safety-check/check.sh | bash
Enter fullscreen mode Exit fullscreen mode

Why Hooks Beat Permissions for Safety

Permissions Hooks
Compound commands Broken (#25441) Full regex matching
Per-project config Inconsistent (#5140) Config files in project root
Deny enforcement Bypassable (#18613) Runs before tool execution
"Always Allow" drift Saves exact strings (#6850) Pattern-based, no drift
Custom logic Not supported Any bash/python script

Permissions are great for convenience ("don't ask me about git add"). Hooks are for safety ("never force push, no matter what").

Use both: permissions for workflow, hooks for enforcement.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.