CLAUDE LABJP
FABLE5 — Claude Fable 5 launches (Jun 9): the first generally available Mythos-class model, beyond Opus, with 1M-token context, 128k output, and always-on adaptive thinkingFREE-WINDOW — Fable 5 is included free on Pro, Max, Team, and Enterprise through Jun 22; usage credits required from Jun 23. API pricing is $10/$50 per MTokSAFEGUARDS — Fable 5 falls back to Opus 4.8 on high-risk topics (under 5% of sessions); the unrestricted Mythos 5 is limited to vetted organizationsIPO — Anthropic confidentially files for an IPO (Jun 1), with a reported $65B raise, $965B valuation, and $47B annualized revenueBILLING — 3 days to the Jun 15 change: Agent SDK, headless Claude Code, GitHub Actions, and third-party agents move to API-rate monthly creditsPLATFORM — Claude Developer Platform adds Managed Agents scheduled deployments, vault env credentials, and session thread webhook eventsFABLE5 — Claude Fable 5 launches (Jun 9): the first generally available Mythos-class model, beyond Opus, with 1M-token context, 128k output, and always-on adaptive thinkingFREE-WINDOW — Fable 5 is included free on Pro, Max, Team, and Enterprise through Jun 22; usage credits required from Jun 23. API pricing is $10/$50 per MTokSAFEGUARDS — Fable 5 falls back to Opus 4.8 on high-risk topics (under 5% of sessions); the unrestricted Mythos 5 is limited to vetted organizationsIPO — Anthropic confidentially files for an IPO (Jun 1), with a reported $65B raise, $965B valuation, and $47B annualized revenueBILLING — 3 days to the Jun 15 change: Agent SDK, headless Claude Code, GitHub Actions, and third-party agents move to API-rate monthly creditsPLATFORM — Claude Developer Platform adds Managed Agents scheduled deployments, vault env credentials, and session thread webhook events
Articles/Claude Code
Claude Code/2026-06-12Intermediate

Stopping Claude Code Before It Runs a Destructive Command — a PreToolUse Screening Hook

How I wired a PreToolUse hook that screens every Bash command Claude Code is about to run: the settings.json setup, the Python screening script, what exit code 2 really does, and the false positives I tuned out over several weeks.

Claude Code140Hooks5PreToolUsepermissions2safety3

Late one night, I had handed a long refactoring session to Claude Code and moved on to other work. Scrolling back through the execution log, I stopped cold. An rm command meant to clear build artifacts had been written in a way that, depending on how a variable expanded, could have targeted the project root instead. Nothing was lost. But I suddenly felt the absence of something I should have had all along: a mechanical check that inspects commands before they run.

As an indie developer, I have no colleague reviewing the commands an agent queues up. The hours I delegate to Claude Code were only going to grow, so that night I built a screening layer with a PreToolUse hook. A few weeks in, it has stepped in front of real mistakes more than once, and the design has held up well. Here is the setup, the actual script, and what I learned tuning it.

Why the permission prompt alone stopped being enough

Claude Code already asks for permission before risky actions, so a second layer might seem redundant. Two things changed my mind.

First, auto-approval is a normal part of long sessions in practice. You allowlist Bash, or you run with confirmations relaxed, because stopping to click "approve" forty times defeats the point of delegation. The moment you do that, the assumption "a human reads every command first" quietly disappears.

Second, confirmation fatigue is real. After approving dozens of harmless commands in a row, I stopped reading the dialogs. Human attention is a poor safety mechanism precisely where it matters most. A hook is the opposite: it reads every command with the same level of attention, at 2 a.m., on the hundredth invocation.

Where PreToolUse hooks intervene

PreToolUse fires immediately before Claude Code executes a tool. The command you register receives JSON on standard input. For the Bash tool, that payload includes tool_name and a tool_input object carrying the exact command string about to run.

The exit code semantics are the part worth memorizing:

  • exit 0 — allow the execution
  • exit 2 — block it, and feed whatever you printed to stderr back to Claude
  • any other non-zero — treated as a hook error, but does not block

That "only 2 blocks" rule is easy to miss. My first version returned exit 1, which meant my screening script ran for days while blocking precisely nothing. If a pattern matches, return 2. Settling that first saves a confusing debugging session later.

The minimal settings.json wiring

For a per-project setup, the configuration lives in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/check_bash.py"
          }
        ]
      }
    ]
  }
}

The matcher filters by tool name. Leaving it empty would run the hook before every tool call, including file edits, and screening those is a much harder false-positive problem. Scoping to Bash keeps the surface small and the rules easy to reason about.

Because the file can be committed to the repository, the same screening applies on any machine that clones it. I ran the hook in a single repository for about two weeks before promoting it to ~/.claude/settings.json, where it now covers every project.

The screening script itself

The screening logic is a deliberately modest denylist: only operations that cannot be undone.

#!/usr/bin/env python3
# .claude/hooks/check_bash.py
import json
import re
import sys
 
BLOCK_PATTERNS = [
    (r"rm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+(/|~|\$HOME)(\s|$)",
     "recursive deletion of root or home directories is not allowed"),
    (r"git\s+push\s+(?!.*--force-with-lease).*--force",
     "use --force-with-lease instead of a bare force push"),
    (r"chmod\s+(-R\s+)?777",
     "chmod 777 is not allowed; grant specific permissions instead"),
    (r"curl\s+[^|;]*\|\s*(ba|z)?sh",
     "piping downloaded scripts directly into a shell is not allowed"),
    (r">\s*/dev/sd[a-z]",
     "writing directly to block devices is not allowed"),
]
 
def main() -> int:
    data = json.load(sys.stdin)
    if data.get("tool_name") != "Bash":
        return 0
    command = data.get("tool_input", {}).get("command", "")
    for pattern, reason in BLOCK_PATTERNS:
        if re.search(pattern, command):
            print(f"blocked by check_bash.py: {reason}", file=sys.stderr)
            return 2
    return 0
 
if __name__ == "__main__":
    sys.exit(main())

I also prototyped the inverse — an allowlist that blocks everything not explicitly approved — and abandoned it within a week. The range of commands an agent legitimately needs is wider than I expected, and until the allowlist matures, the agent can barely function. Starting from a denylist that only prevents fatal damage, then adding a pattern each time something makes me flinch, has proven far more sustainable for a solo setup.

What Claude does after being blocked

The most interesting discovery came right after deployment. Whatever the script prints to stderr under exit 2 becomes feedback Claude actually reads.

When the hook blocked a git push --force, Claude picked up the phrase "use --force-with-lease" from the rejection message, rewrote the command on its own, and ran the safer version. A well-written rejection message works less like a wall and more like a signpost.

Once I understood that, I rewrote every message to say what to do instead, not just what was forbidden. A bare blocked response tends to send Claude guessing at workarounds, which makes its behavior harder to predict — the opposite of what a safety layer is for.

Adjustments after a few weeks of daily use

Three things needed tuning once the hook met real workloads:

  • False positives on temp directories. The first pattern also caught recursive deletes under $TMPDIR during test runs. Narrowing the match to literal /, ~, and $HOME targets fixed it without weakening the protection that matters.
  • A block log. Every rejection now appends a line to ~/.claude/hooks.log. Reviewing it weekly gives me evidence for both adding patterns and removing ones that never fire — the log, not intuition, drives the rule set.
  • Keeping the script fast. The hook runs before every single Bash call, so any startup weight is felt immediately. No external command calls, no network access, no clever caching. Plain regex checks are more than enough.

Splitting responsibilities with permission deny rules

Claude Code also supports declarative deny rules in its permission settings, and a recent update added glob patterns to them. I tried pushing everything into deny rules to retire the script, then settled on a division of labor instead:

  • Statically decidable bans (no writes to a protected directory, a command that should never run) → permission deny rules
  • Context-dependent judgments (argument combinations, redirect targets, anything where I want the rejection message to steer Claude toward an alternative) → the PreToolUse hook

Deny rules are fast and declarative; hooks are expressive. Routing the simple cases to deny rules keeps the screening script short, which in turn keeps it auditable — the configuration I trust most is the one I can re-read in thirty seconds.

Start with a single file

The whole mechanism begins working the moment one script exists and settings.json gains a few lines. Start with two or three deny patterns, run it for a week, and let the block log tell you what to add. That order matters: you get your first "it caught something" moment before false positives have a chance to sour you on the idea.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-05-06
react-native-permissions vs Expo Permissions API — Claude Code
Should you use react-native-permissions or the Expo permissions API? This guide clarifies the decision criteria and shows practical iOS/Android implementation patterns using Claude Code, including common pitfalls and Privacy Manifest requirements.
Claude Code2026-05-05
Building a Zero-Touch Code Review Environment with Claude Code Hooks
Learn how to use Claude Code's hook system to automatically review code on every tool execution. Covers PostToolUse, Stop hooks, and the pitfalls to avoid when implementing PreToolUse blockers.
Claude Code2026-04-27
Claude Code Hooks: A Complete Field Guide to All 8 Hook Types and How to Pick the Right One
Claude Code hooks are powerful, but most people give up before figuring out which event does what. Here's the field guide I wish I had when I started — six months of running hooks in production, distilled.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →