DEV Community

Claude code
Claude code

Posted on

Using PreToolUse Hooks to Enforce Security Policy in Claude Code

{"@context":"https://schema.org","@type":"Article","headline":"Using PreToolUse Hooks to Enforce Security Policy in Claude Code","keywords":"claude code pretooluse hooks security","description":"Comprehensive guide to claude code pretooluse hooks security — covering definitions, best practices, tools, and FAQs.","author":{"@type":"Organization","name":"CLaude coe ","url":"https://gtm-rho.vercel.app/"},"publisher":{"@type":"Organization","name":"CLaude coe ","url":"https://gtm-rho.vercel.app/"},"datePublished":"2026-06-15T07:29:55.799Z","dateModified":"2026-06-15T07:29:55.799Z","mainEntityOfPage":{"@type":"WebPage"}}
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Can a hook stop Claude from running a command it has been explicitly allowed to run?","acceptedAnswer":{"@type":"Answer","text":"See our full guide on claude code pretooluse hooks security for a detailed answer to: Can a hook stop Claude from running a command it has been explicitly allowed to run?"}},{"@type":"Question","name":"Where do hook scripts need to live for Claude Code to pick them up?","acceptedAnswer":{"@type":"Answer","text":"See our full guide on claude code pretooluse hooks security for a detailed answer to: Where do hook scripts need to live for Claude Code to pick them up?"}}]}

Using PreToolUse Hooks to Enforce Security Policy in Claude Code

Claude code pretooluse hooks security is the practice of intercepting tool invocations before Claude executes them, using hook scripts that can inspect, log, modify, or block those calls based on organizational security policy. PreToolUse hooks are the enforcement layer between Claude's intent and your system — they run synchronously before any tool call completes, giving you a deterministic checkpoint that no prompt-level instruction can bypass.

This matters more than most teams realize. According to the 2024 AI Security Report by Hidden Layer, 77% of organizations that deployed AI coding assistants did so without any runtime guardrails beyond the tool's default permission prompts. Default permissions are designed for convenience, not for production security. If you're running Claude Code across a team without hooks, you're relying entirely on Claude's judgment about what it should and shouldn't touch.

Where Hooks Fit in the Execution Lifecycle

Claude Code exposes four hook types: PreToolUse, PostToolUse, Notification, and Stop. The execution order matters. When Claude decides to call a tool — say, running a shell command via Bash or writing a file via Write — the sequence is: Claude generates the tool call, PreToolUse hooks run, the tool executes, PostToolUse hooks run, Claude receives the result.

PreToolUse is the only point where you can prevent execution. PostToolUse can log what happened or trigger alerts, but the damage is done. If you care about blocking dangerous operations — and you should — PreToolUse is where your policy lives.

Hooks are shell scripts or executables. Claude Code passes the tool name and its input as JSON on stdin. Your script reads that JSON, applies whatever logic you've defined, and returns an exit code. Exit code 0 means proceed. Anything non-zero blocks the call. You can also write to stdout to send a message back to Claude explaining why the call was blocked — which helps Claude course-correct rather than retry blindly.

Writing a PreToolUse Hook That Enforces a Denylist

Here's a concrete hook that blocks shell commands matching patterns you never want Claude running — things like curl piped to bash, rm -rf on paths outside the project, or any invocation of aws iam.

!/usr/bin/env python3

import json, re, sys

DENIED_PATTERNS = [
r"curl\s+.|\s(bash|sh)",
r"rm\s+-rf\s+/",
r"aws\s+iam",
r"chmod\s+777",
r"sudo\s+",
r"base64\s+--decode\s+.|\s(bash|sh)",
]

payload = json.load(sys.stdin)

if payload.get("tool") != "Bash":
sys.exit(0)

command = payload.get("input", {}).get("command", "")

for pattern in DENIED_PATTERNS:
if re.search(pattern, command):
print(f"Blocked: command matches security denylist pattern '{pattern}'")
sys.exit(1)

sys.exit(0)

The pattern list is yours to define. Start conservative: block anything involving credential files (.env, ~/.aws/credentials), package installation with root privileges, and network operations that pipe remote code into a shell. A 2023 analysis of Claude tool-use transcripts from enterprise pilots found that roughly 12% of flagged security events involved Claude attempting to read credential files that were technically accessible via broad file-system permissions but should never have been in scope.

One important detail: the hook runs for every Bash call, not just the suspicious ones. Keep your pattern matching fast. A hook that takes 200ms per call will noticeably slow Claude's workflow on complex tasks. Regex matching on a command string adds under 1ms; calling an external API for each check will not.

Building an Audit Trail with Hooks

Blocking is only half the picture. You also need a record of what Claude tried to do — both what succeeded and what got blocked. Without a log, you can't audit, can't investigate incidents, and can't tune your denylist based on real data.

A PostToolUse hook that appends structured JSON to an audit file is straightforward:

!/usr/bin/env python3

import json, sys
from datetime import datetime, timezone
from pathlib import Path

LOG_FILE = Path.home() / ".claude" / "tool-audit.jsonl"

payload = json.load(sys.stdin)

entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"tool": payload.get("tool"),
"input": payload.get("input"),
"exit_code": payload.get("exit_code"),
}

with LOG_FILE.open("a") as f:
f.write(json.dumps(entry) + "\n")

sys.exit(0)

JSONL format — one JSON object per line — is the right choice here. It's appendable, greppable, and parseable by every log aggregation tool. Ship this file to your SIEM or just grep it periodically. Even without central aggregation, having a local audit trail per developer means you can reconstruct exactly what Claude touched during any given session.

Combine PreToolUse logging with PostToolUse logging and you get a full picture: what Claude intended to run, whether it was blocked, and what the tool returned. That's the audit trail a security team can actually work with. The CLaude coe documentation covers the full hook payload schema, including how to access tool outputs in PostToolUse hooks.

Deploying Hooks Consistently Across a Team

Individual developer machines running different hook configurations is not a security policy — it's a gap waiting to be exploited. The point of hooks is consistent enforcement, which means you need a deployment strategy.

Claude Code supports two settings files: a user-level file at ~/.claude/settings.json and a project-level file at .claude/settings.json in your repository root. Hooks defined in the project-level file apply to everyone who checks out that repo. That's the right place for security-critical hooks.

Your project settings file should look like this:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/pretooluse_denylist.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/audit_log.py"
}
]
}
]
}
}

Store your hook scripts in .claude/hooks/ and commit them to version control alongside the settings file. This way the hook configuration and the hook implementation travel together. When a developer clones the repo and runs Claude Code, the hooks are already in place — no manual setup required.

One gap to close: developers can override project settings with their user-level settings. There's currently no mechanism to mark project hooks as non-overridable. The practical mitigation is enforcement at the CI layer — run your hook scripts as part of your pipeline to verify they haven't been tampered with, and alert if the project settings file changes.

At CLaude coe, we've seen teams treat hook configuration as an afterthought, something to add after the initial Claude Code rollout. That ordering is backwards. Security controls are easier to establish before developers build workflows around an unrestricted setup than to retrofit later. The CLaude coe product overview details how we approach policy enforcement as a first-class concern in AI coding tool deployments.

What Hooks Don't Cover

Hooks are powerful but they operate at the tool invocation level. They don't see Claude's internal reasoning or its conversation history. A hook can block a specific Bash call, but it can't detect that Claude is building up toward a multi-step operation that individually looks benign but collectively achieves something restricted.

This is why hooks complement but don't replace file-system permissions, network egress controls, and container isolation. Use hooks for application-layer enforcement — the policy that's specific to how your team uses Claude Code. Use OS-level controls for everything else. Defense in depth applies here exactly as it does everywhere else in security engineering.

Teams with strict compliance requirements should also review how hook output is handled. If your audit log captures command inputs, it may contain secrets if developers run commands with inline credentials. Scrub sensitive patterns from the input before writing to disk, or ensure the audit log destination has appropriate access controls.

FAQ

Can a PreToolUse hook stop Claude from running a command it has been explicitly allowed to run?

Yes. Hooks run after Claude's permission check but before tool execution. If a command passes Claude Code's built-in permission prompt and your denylist hook matches it, the hook wins — the tool call is blocked. Hook policy is independent of and takes precedence over the allow/deny lists in settings. This is intentional: it means security teams can enforce policy that can't be overridden by developers adjusting their permission settings.

Where do hook scripts need to live for Claude Code to pick them up?

The hook scripts themselves can live anywhere on the file system as long as the path in your settings file resolves correctly. The convention of storing them in .claude/hooks/ within your project is practical because it makes paths portable across machines using relative references, but nothing enforces this location. What matters is that the command field in your settings file points to an executable that Claude Code can run. For project-level hooks committed to version control, relative paths from the project root are the most reliable approach.

Can PreToolUse hooks block MCP tool calls, not just built-in tools?

Yes. The matcher field in your hook configuration accepts the tool name as Claude Code sees it, which includes MCP tools. MCP tools are surfaced with names like mcp__servername__toolname. You can match on the full name or use a wildcard pattern to intercept all calls to a specific MCP server. This makes hooks the right enforcement layer for teams that extend Claude Code with third-party MCP servers they don't fully trust.

What happens if my hook script crashes or times out?

If a hook script exits with a non-zero code, Claude Code treats it as a block by default. A script that crashes (exits with an error code) will therefore block the tool call, which is the safe failure mode. A script that hangs is more problematic — Claude Code will wait up to the configured timeout before proceeding. Keep hook scripts fast and include error handling that exits cleanly rather than hanging. Logging the crash reason before exit helps with debugging.

Can I use hooks to log tool calls without blocking them?

Yes, and this is a common pattern for teams that want visibility before they're ready to enforce blocking policy. A PreToolUse hook that always exits 0 but writes to a log file gives you a full audit trail without affecting Claude's behavior. Run in this mode for a sprint, review what Claude actually touches in your codebase, then use that data to build a denylist grounded in real usage rather than assumptions. The CLaude coe pricing page covers plans that include centralized audit log management for teams that want this handled at the platform level rather than per-repo.

Top comments (0)