# Securing Claude Code Hooks and Preventing Dangerous Commands: A Complete Guide

> Learn to secure Claude Code hooks and prevent dangerous commands. This guide explains how to use validation scripts to block malicious prompts, shell commands, and leaked secrets for enhanced security.

- Repository: [Luong NGUYEN/claude-howto](https://github.com/luongnv89/claude-howto)
- Tags: how-to-guide
- Published: 2026-03-30

---

**Claude Code hooks provide an event-driven framework to intercept and block destructive shell commands, malicious user prompts, and leaked secrets by returning exit code 2 from validation scripts.**

The `luongnv89/claude-howto` repository demonstrates how to implement a defense-in-depth security layer for Claude Code using its 25 built-in hook events. By configuring validation scripts in `~/.claude/settings.json` or project-level [`.claude/settings.json`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.json), you can enforce security policies that prevent dangerous operations before they execute. This guide covers the architectural patterns and concrete implementations found in the `06-hooks/` directory.

## How Claude Code Hooks Work

Claude Code's hook system operates as a middleware layer that inspects tool invocations and user interactions through JSON payloads passed via stdin.

### Event-Driven Security Architecture

The system fires hooks at deterministic execution points:

- **PreToolUse**: Executes before any tool runs, allowing you to block dangerous Bash commands or file operations
- **UserPromptSubmit**: Intercepts user prompts before processing, enabling content filtering
- **PostToolUse**: Runs after tool completion to scan outputs or generated files for secrets
- **Stop**: Captures session termination for audit logging

According to the [`06-hooks/README.md`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/README.md), hooks receive a JSON payload on **stdin** and must exit with a numeric code where `0` allows the operation and `2` blocks it. Optional JSON on **stdout** can modify the session context or provide additional warnings.

### Configuration Hierarchy and Matcher Syntax

Settings merge from multiple sources in priority order:

1. Global: `~/.claude/settings.json`
2. Project: [`.claude/settings.json`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.json)
3. Local: [`.claude/settings.local.json`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.local.json)
4. Managed policies and plugin-scoped [`hooks/hooks.json`](https://github.com/luongnv89/claude-howto/blob/main/hooks/hooks.json)

Each hook entry uses a `matcher` to determine applicability: exact strings, regex patterns, wildcards (`*`), or MCP-specific patterns. As documented in the repository, all matching hooks for a given event execute in parallel, with identical commands automatically deduplicated.

## Blocking Dangerous Commands with PreToolUse Hooks

The `PreToolUse` event is your primary defense against destructive shell operations. The reference implementation in [`06-hooks/validate-bash.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/validate-bash.py) demonstrates pattern-based blocking:

```python
#!/usr/bin/env python3

# File: 06-hooks/validate-bash.py

import json, sys, re

BLOCKED = [
    (r"\brm\s+-rf\s+/", "Blocking dangerous rm -rf /"),
    (r"\bsudo\s+rm",    "Blocking sudo rm command"),
]

data = json.load(sys.stdin)
if data.get("tool_name") != "Bash":
    sys.exit(0)

cmd = data.get("tool_input", {}).get("command", "")
for pat, msg in BLOCKED:
    if re.search(pat, cmd):
        print(msg, file=sys.stderr)
        sys.exit(2)  # 2 = blocking error

sys.exit(0)

```

Configuration requires adding the hook to your settings file:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\""
          }
        ]
      }
    ]
  }
}

```

This pattern ensures that commands matching `rm -rf /` or `sudo rm` never reach the shell, as the exit code 2 signals Claude to halt execution and surface the error message.

## Validating User Prompts Before Submission

Malicious or accidental instructions often enter through the prompt itself. The `UserPromptSubmit` hook acts as a content filter before Claude processes the request.

The [`06-hooks/validate-prompt.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/validate-prompt.py) implementation inspects the `user_prompt` field for dangerous patterns:

```python
#!/usr/bin/env python3

# File: 06-hooks/validate-prompt.py

import json, sys, re

BLOCKED = [
    (r"delete\s+(all\s+)?database", "Dangerous: database deletion"),
    (r"rm\s+-rf\s+/", "Dangerous: root deletion"),
]

data = json.load(sys.stdin)
prompt = data.get("user_prompt") or data.get("prompt", "")

for pat, msg in BLOCKED:
    if re.search(pat, prompt, re.IGNORECASE):
        out = {"decision": "block", "reason": f"Blocked: {msg}"}
        print(json.dumps(out))
        sys.exit(0)
sys.exit(0)

```

Register this validator in your configuration:

```json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py\""
          }
        ]
      }
    ]
  }
}

```

Unlike `PreToolUse`, prompt validation returns a JSON object indicating the block decision, providing explicit reasoning that appears in the Claude interface.

## Scanning Generated Code for Secrets

Post-execution validation prevents credential leaks in generated files. The `PostToolUse` hook with matchers for `Write` and `Edit` tools scans content before it reaches the filesystem.

The [`06-hooks/security-scan.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/security-scan.py) example targets hardcoded credentials:

```python
#!/usr/bin/env python3

# File: 06-hooks/security-scan.py

import json, sys, re

SECRETS = [
    (r"password\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded password"),
    (r"api[_-]?key\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded API key"),
]

data = json.load(sys.stdin)
if data.get("tool_name") not in ["Write", "Edit"]:
    sys.exit(0)

content = data["tool_input"].get("content") or data["tool_input"].get("new_string", "")
path = data["tool_input"].get("file_path", "")
warnings = [msg for pat, msg in SECRETS if re.search(pat, content, re.I)]

if warnings:
    out = {
        "hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "additionalContext": f"Security warnings for {path}: " + "; ".join(warnings)
        }
    }
    print(json.dumps(out))
sys.exit(0)

```

Configure this scanner to run after file modifications:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py\""
          }
        ]
      }
    ]
  }
}

```

This approach catches secrets immediately after generation without interfering with the write operation itself, surfacing warnings through the `additionalContext` field.

## Auto-Adapting Permission Rules Safely

The [`06-hooks/auto-adapt-mode.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/auto-adapt-mode.py) demonstrates machine learning-style permission adaptation that generalizes safe patterns while maintaining strict deny-lists for dangerous operations.

Key security features of this implementation:

- **Baseline safe rules**: Seeds permissions with read-only operations like `Read(*)`, `Bash(git status:*)`, and `Bash(ls:*)`
- **Dangerous pattern exclusion**: Explicitly blocks `rm -rf`, `git push --force`, `sudo`, SQL `DROP` statements, and pipe-to-shell curl commands
- **Command generalization**: Transforms specific approved commands into wildcard patterns (e.g., `Bash(git status)` becomes `Bash(git status:*)`)

```python

# Excerpt from 06-hooks/auto-adapt-mode.py

DANGEROUS = [
    r"rm\s+(-[a-zA-Z]*r[a-zA-Z]*|--recursive)",
    r"git\s+push\s+(-[a-zA-Z]*f|--force)",
    r"sudo\b",
    r"DROP\s+(TABLE|DATABASE)",
    r"curl\s+.*\|\s*(bash|sh)",
]

```

The script modifies `~/.claude/settings.json` to append safe rules automatically, but only after checking against the `DANGEROUS` regex list. This prevents the system from learning destructive behaviors even when they appear in successful tool executions.

## Implementation and Best Practices

### Directory Structure and Setup

Organize hooks in a version-controlled directory structure:

```bash
mkdir -p ~/.claude/hooks
cp 06-hooks/*.py ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.py

```

Reference implementations in the repository include [`format-code.sh`](https://github.com/luongnv89/claude-howto/blob/main/format-code.sh) for automatic linting and [`context-tracker.py`](https://github.com/luongnv89/claude-howto/blob/main/context-tracker.py) for token usage monitoring, illustrating how to compose multiple validators for the same event.

### Security-First Configuration Patterns

Follow these guidelines from the [`06-hooks/README.md`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/README.md) to prevent privilege escalation:

- **Quote shell variables**: Always use `"$CLAUDE_PROJECT_DIR"` instead of unquoted `$CLAUDE_PROJECT_DIR` to prevent word-splitting attacks
- **Whitelist environment variables**: For HTTP hooks, explicitly define `allowedEnvVars` to prevent credential leakage via environment interpolation
- **Validate before trusting**: Apply regex validation to all `tool_input` fields before allowing execution
- **Test in isolation**: Pipe sample JSON to your hook script directly (`echo '{}' | python3 hook.py`) before deploying

The repository includes a [`.pre-commit-config.yaml`](https://github.com/luongnv89/claude-howto/blob/main/.pre-commit-config.yaml) that runs [`security-scan.py`](https://github.com/luongnv89/claude-howto/blob/main/security-scan.py) and [`format-code.sh`](https://github.com/luongnv89/claude-howto/blob/main/format-code.sh) before Git commits, ensuring hooks themselves pass security checks.

## Summary

- **Hook architecture**: Claude Code provides 25 events including `PreToolUse`, `UserPromptSubmit`, and `PostToolUse` that receive JSON via stdin and use exit code 2 to block operations
- **Command validation**: The [`validate-bash.py`](https://github.com/luongnv89/claude-howto/blob/main/validate-bash.py) example in [`06-hooks/validate-bash.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/validate-bash.py) blocks destructive patterns like `rm -rf /` before execution
- **Prompt filtering**: [`validate-prompt.py`](https://github.com/luongnv89/claude-howto/blob/main/validate-prompt.py) intercepts dangerous user instructions using regex matching against the `user_prompt` field
- **Secret detection**: [`security-scan.py`](https://github.com/luongnv89/claude-howto/blob/main/security-scan.py) scans written content for hardcoded passwords and API keys using `PostToolUse` hooks
- **Safe automation**: [`auto-adapt-mode.py`](https://github.com/luongnv89/claude-howto/blob/main/auto-adapt-mode.py) learns permission rules from approved executions while maintaining explicit deny-lists for dangerous commands
- **Configuration**: Store hooks in `.claude/hooks/` and reference them in `~/.claude/settings.json` or project-level [`.claude/settings.json`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.json) using the `$CLAUDE_PROJECT_DIR` environment variable

## Frequently Asked Questions

### How do Claude Code hooks prevent arbitrary code execution?

Hooks act as a mandatory access control layer that inspects tool invocations before the Claude process executes them. By validating the `command` field in `PreToolUse` events and exiting with code 2 when dangerous patterns like `sudo` or `rm -rf` are detected, the system blocks the operation entirely and surfaces an error to the user without reaching the shell.

### What is the difference between PreToolUse and PostToolUse hooks?

`PreToolUse` fires before a tool executes and can prevent the operation by returning exit code 2, making it ideal for blocking dangerous commands. `PostToolUse` fires after successful completion and cannot block the operation (it always uses exit code 0), but can add warnings or audit entries through the `additionalContext` field in the JSON stdout response, making it suitable for secret scanning and logging.

### Can hooks access environment variables and network resources?

Command hooks run inside Claude's sandboxed environment with access to safe variables like `$CLAUDE_PROJECT_DIR`. HTTP hooks can make network calls, but require explicit `allowedEnvVars` configuration to access environment variables, preventing accidental credential leakage. The repository's examples use Python scripts that read from stdin and write to stdout without requiring network access.

### Where should I store hook scripts for team-wide use?

Store hook scripts in a version-controlled `.claude/hooks/` directory within your project repository, then reference them using `"$CLAUDE_PROJECT_DIR/.claude/hooks/script.py"` in your [`.claude/settings.json`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.json). For personal protection across all projects, place scripts in `~/.claude/hooks/` and reference them in `~/.claude/settings.json`. The `luongnv89/claude-howto` repository provides reference implementations that can be copied directly into these locations.