DEV Community

Anand Rathnas
Anand Rathnas

Posted on

How I Tamed Claude Code with Pre-Tool Hooks (And You Should Too)

I've been building jo4.io - a URL shortener with analytics - using Claude Code as my AI pair programmer. It's been fantastic for productivity, but I quickly discovered that Claude can be a bit... enthusiastic about running commands.

The Problem

While debugging a stats tracking bug in jo4, Claude was happily:

  1. Running cd /some/path commands, messing up my terminal's working directory
  2. Attempting git commit and git push without my review
  3. Sometimes running destructive git commands like git reset --hard

These aren't bugs - Claude is just doing what it thinks is helpful. But in a production codebase, I need control over what gets committed and pushed.

The Solution: Claude Code Hooks

Claude Code has a powerful but underutilized feature: PreToolUse hooks. These let you intercept tool calls (like Bash commands) and block them before execution.

Here's my setup:

~/.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/block-git.sh",
            "timeout": 10
          },
          {
            "type": "command",
            "command": "~/.claude/block-cd.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

~/.claude/block-cd.sh

This blocks standalone cd commands while allowing subshell patterns:

#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
trimmed_command=$(echo "$command" | sed 's/^[[:space:]]*//')

# Block standalone cd, allow (cd /path && command)
if [[ "$trimmed_command" =~ ^cd[[:space:]] ]]; then
  cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "BLOCKED: Use subshell pattern: (cd /path && command)"
  }
}
EOF
  exit 0
fi

echo "{}"
exit 0
Enter fullscreen mode Exit fullscreen mode

~/.claude/block-git.sh

This blocks git commit, push, and destructive operations:

#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')

if [[ $command == *"git commit"* ]] || \
   [[ $command == *"git push"* ]] || \
   [[ $command == *"git reset --hard"* ]] || \
   [[ $command == *"git rebase"* ]] || \
   [[ $command == *"--force"* && $command == *"git"* ]]; then
  cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "BLOCKED: git commit/push requires explicit user approval"
  }
}
EOF
  exit 0
fi

echo "{}"
exit 0
Enter fullscreen mode Exit fullscreen mode

How It Works

When Claude tries to run a blocked command, it sees:

PreToolUse:Bash hook blocking error: BLOCKED: git commit/push requires explicit user approval
Enter fullscreen mode Exit fullscreen mode

Claude then adapts - it'll prepare the commit message and tell me what it would commit, letting me run the actual command myself.

Real World Example

Today while fixing a bug in jo4's analytics service, Claude found that HttpServletRequest was being passed to an @Async method - causing stats to silently fail. It refactored the code, wrote tests, and then said:

"The fix is complete. When you're ready, you can commit with: git commit -m 'Fix stats tracking by extracting request data before async'"

Instead of just committing and pushing (which some AI tools do), it handed control back to me. I reviewed the changes, ran the tests, and committed when I was satisfied.

Why This Matters

  1. Safety: No accidental force pushes or hard resets
  2. Review: I always see what's being committed
  3. Learning: Claude explains what it would do, teaching me in the process
  4. Trust: I can give Claude more autonomy on read operations while keeping write operations gated

Setup Instructions

  1. Create the hooks directory: mkdir -p ~/.claude
  2. Create settings.json and the shell scripts above
  3. Make scripts executable: chmod +x ~/.claude/*.sh
  4. Restart Claude Code

What's Next?

I'm considering adding hooks for:

  • Blocking rm -rf on certain directories
  • Requiring confirmation for database migrations
  • Logging all commands to an audit file

The hook system is incredibly flexible. You can match specific tools, check command content, and return custom error messages.


Are you using Claude Code hooks? I'd love to hear about your setup in the comments!

Building jo4.io - a modern URL shortener with advanced analytics. Check it out at jo4.io

Top comments (0)