Hooks: Deterministic Policy Enforcement for Claude Code Teams
The Probabilistic Trap
This is Part 3 of 5 in The Claude Code Enterprise Stack series. You've configured the hierarchy (Part 1) and documented your standards in CLAUDE.md (Part 2). Now comes enforcement.
You told Claude to always run tests before committing. Last week, it forgot. The week before, it ran tests but got impatient with a slow suite and skipped the final assertion. Yesterday, it decided the changes were "too small to need testing."
The problem isn't Claude. It's you. You're relying on probabilistic compliance.
Claude might follow your instructions. Claude might remember your standards. Claude might enforce your security rules. But "might" isn't good enough for compliance, security, or team standards. Policies need teeth. They need to fire every time. No exceptions. No judgment calls.
That's what hooks are for.
Hooks are the enforcement layer that makes your standards actually mandatory. They're the difference between "Claude, please run tests" and "tests run automatically, guaranteed."
Your hooks don't have false confidence. They execute. Period. No second thoughts. No judgment calls. No "this time I'll skip it" exceptions.
Probabilistic vs. Deterministic: The Core Tension
Let's be precise about what we're trading off.
Probabilistic approach: You document a standard in CLAUDE.md. "Always format Python files with Black after edits." Claude reads your CLAUDE.md. Claude understands the instruction. Claude chooses to follow it. Most of the time. Sometimes Claude gets distracted. Sometimes Claude deprioritizes it. Sometimes Claude doesn't realize the standard applies to this particular file.
Imagine if your linter worked this way. "ESLint, please enforce semicolons. Unless you're busy. Unless you think the code looks okay without them." You'd lose your mind.
Deterministic approach: A hook fires after every Edit to Python files. It runs Black automatically. No decision-making. No probabilistic compliance. It happens.
The math is simple. You have two options:
- Rely on Claude's attention and memory (imperfect, but fast)
- Add automation that runs every time (infallible, but requires setup)
For security, compliance, and team standards, option 2 wins. Period.
The Hook System: 12+ Key Events and When They Fire
Claude Code exposes a set of lifecycle points where you can inject automation. Not all of them are blocking—that's critical to understand. Most hooks can observe and react. One hook can stop the world.
Here's the complete set:
| Event | When It Fires | Can Block? | Best For |
|---|---|---|---|
PreToolUse | Before Claude uses any tool | Yes | Security gates, validation |
PostToolUse | After a tool succeeds | No | Audit logging, auto-formatting |
SessionStart | When session begins | No | Setup, initialization |
SessionEnd | When session ends | No | Cleanup, final logging |
UserPromptSubmit | User submits a prompt | No | Input validation, logging |
PermissionRequest | Claude requests permission | Yes | Custom approval workflows |
Stop | Claude finishes responding | No | Quality gates, output validation |
SubagentStart | A subagent starts | No | Logging, context setup |
SubagentStop | A subagent finishes | No | Logging, cleanup |
FileChanged | A file was edited | No | Auto-format, validation |
ConfigChange | Settings were modified | No | Audit logging |
Notification | Event notification | No | External integrations |
For SMBs, the important ones are clear: PreToolUse (the only one that blocks), PostToolUse (logging and automation), and FileChanged (convenience hooks). The others matter in specialized scenarios.
The Exit Code Semantics: This Is Non-Negotiable
Here's where most teams stumble.
Every hook is a script. That script exits with a code. The exit code determines what happens next.
- Exit 0: Allow. The action proceeds normally.
- Exit 2: Block. Claude cannot proceed. The error message from stderr appears in the UI.
- Exit 1: Non-blocking error. The action proceeds anyway.
This distinction is critical for security hooks.
If your security hook exits with code 1, it looks like it works. Claude sees the error. The hook appears to run. But the action proceeds. Zero enforcement. The hook is theater.
Every security-critical hook—blocking secrets, preventing destructive commands, enforcing reviews—must use exit 2. Not exit 1. Not exit 0. Exit 2. That choice is the difference between a policy that works and a policy that's broken.
When allowManagedHooksOnly is enabled (which you should do), user hooks are completely ignored and only managed hooks run. This prevents developers from accidentally installing a broken security hook from a marketplace plugin.
Five Copy-Pasteable Hook Examples
Let's move from theory to practice. Here are real hooks you can deploy this week.
Hook 1: Prevent Hardcoded Secrets (PreToolUse Block)
This hook fires before any Bash command. If the command contains suspicious patterns like password=, api_key=, or secret=, it blocks the command entirely.
#!/bin/bash
# Hook: Security gate - block hardcoded secrets in Bash commands
SUSPICIOUS_PATTERNS=("password=" "api_key=" "secret=" "token=" "AUTH=" "CREDENTIALS=")
# Get the bash command that Claude is about to run
if [[ ! -z "$CLAUDE_TOOL_ARGS" ]]; then
for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do
if echo "$CLAUDE_TOOL_ARGS" | grep -qi "$pattern"; then
echo "Error: Detected potential secret in command"
echo "Pattern: $pattern"
echo "Use environment variables or secrets manager instead"
exit 2 # BLOCK the action
fi
done
fi
exit 0 # ALLOW
Deploy this as .claude/hooks/security-check.sh (project-scoped) or in managed settings (organization-scoped). Every time Claude tries to run a Bash command, this fires. Catches the vast majority of common credential mistakes before they hit a PR.
Hook 2: Auto-Format Python Files (PostToolUse)
This hook fires after every Edit to a Python file. It automatically runs Black for formatting.
#!/bin/bash
# Hook: Post-edit auto-formatting for Python files
# Only trigger for Python files
if [[ "$CLAUDE_FILE_PATH" == *.py ]]; then
# Check if the file exists (it should, since it was just edited)
if [[ -f "$CLAUDE_FILE_PATH" ]]; then
# Run Black silently
black "$CLAUDE_FILE_PATH" 2>/dev/null
if [[ $? -eq 0 ]]; then
echo "Auto-formatted: $CLAUDE_FILE_PATH"
fi
fi
fi
exit 0 # Never block; this is convenience
Zero configuration from Claude. File gets edited, Black runs, developer doesn't think about it. That's deterministic.
Hook 3: Block Wrong Package Manager (PreToolUse Block)
Your project uses bun. Claude tries npm. This hook catches it:
#!/bin/bash
# Hook: Enforce bun for this project (block npm)
if [[ "$CLAUDE_TOOL_ARGS" == *"npm "* ]]; then
echo "Error: This project uses bun, not npm"
echo "Use: bun instead of npm"
exit 2 # BLOCK
fi
exit 0
Simple rule, massive productivity gain. No more debugging "why is npm not found?" conversations with Claude.
Hook 4: Enforce Tests Before PR (PreToolUse Block)
This hook matches when Claude tries to create a GitHub PR. It runs your test suite first:
#!/bin/bash
# Hook: Block PR creation if tests don't pass
if [[ "$CLAUDE_TOOL_NAME" == "mcp__github__create_pr" ]]; then
echo "Running tests before PR creation..."
if npm run test &>/dev/null; then
echo "Tests passed. Proceeding with PR."
exit 0 # ALLOW
else
echo "Error: Tests failed. PR blocked."
exit 2 # BLOCK
fi
fi
exit 0
Every PR generated by Claude goes through your test suite. No broken PRs slip through. Teams love this.
Hook 5: Add Dry-Run to Risky Commands (Input Modification)
Starting in Claude Code v2.0.10, PreToolUse hooks can modify inputs, not just block them. This is powerful:
#!/bin/bash
# Hook: Add --dry-run to production deploy commands
if [[ "$CLAUDE_TOOL_ARGS" == *"npm run deploy"* ]]; then
# Instead of blocking, we modify the command to add --dry-run
MODIFIED_ARGS="${CLAUDE_TOOL_ARGS} --dry-run"
# Return JSON decision
echo "{\"modifiedInput\": \"$MODIFIED_ARGS\"}"
exit 0
fi
exit 0
Claude tries to deploy to production. Your hook transparently adds --dry-run. The deploy runs, but safely. Claude doesn't even realize the hook ran. No friction. Complete safety.
Note: Input modification via hooks was introduced in Claude Code v2.0.10 and should be tested thoroughly in your environment before deploying to production.
Recursive Enforcement on Subagents
Here's a subtle but critical detail: hooks fire recursively.
When Claude spawns a subagent (via the @agent-name syntax), every tool the subagent uses also triggers your hooks. If you have a PreToolUse hook that blocks Bash(rm -rf), and a subagent tries to use that command, your hook fires and blocks the subagent too.
This prevents the escape hatch where a developer tries to work around your security rules by spinning up a subagent to "go around" the hooks.
With allowManagedHooksOnly: true, you ensure that user-installed plugins can't install their own hooks to bypass your security gates. Only managed hooks run. Controlled. Deterministic.
Governing Hooks Centrally
The power of hooks increases exponentially when you control them from the top.
In your managed settings, set:
{
"allowManagedHooksOnly": true,
"allowedHttpHookUrls": [
"https://audit-log.company.com/*",
"https://slack.company.com/webhooks/*"
],
"disableAllHooks": false
}
allowManagedHooksOnly: true is the lever. When you set it, Claude Code ignores all user-defined hooks, project hooks, and plugin hooks. Only hooks from your managed settings run.
This is crucial for enterprises. It prevents:
- A developer accidentally installing a malicious hook from the marketplace
- Rogue hooks from getting committed to a project repo
- Plugin hooks that conflict with your security policies
You can allowlist HTTP hook URLs to prevent data exfiltration via hooks. You can completely disable hooks with disableAllHooks: true if you need to lock down a sensitive environment.
Audit Logging via PostToolUse
For compliance, you need visibility into what Claude Code did.
Here's a minimal audit hook:
#!/bin/bash
# Hook: Audit all tool usage to centralized log
AUDIT_LOG="/var/log/claude-code-audit.log"
# Create entry with timestamp, tool name, args
ENTRY="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"tool\":\"$CLAUDE_TOOL_NAME\",\"user\":\"$(whoami)\",\"project\":\"$CLAUDE_PROJECT_DIR\"}"
echo "$ENTRY" >> "$AUDIT_LOG"
exit 0
For every tool Claude uses, an entry gets logged. Feed these logs into your SIEM. Boom—you have the audit trail enterprises require for SOC 2 compliance.
Practical Configuration
How do you wire hooks into Claude Code? Two ways.
Project-scoped (team-shared in git):
{
".claude/settings.json": {
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/security-check.sh"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/auto-format.sh"
}
]
}
]
}
}
}
Managed (enforced organization-wide):
{
"managed-settings.json": {
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/etc/claude-code/hooks/security-gate.sh"
}
]
}
]
}
}
}
The matcher syntax is flexible. Bash matches all Bash commands. Edit|Write matches both Edit and Write tools. mcp__* matches all MCP tools. mcp__github__* matches GitHub MCP tools specifically.
When Hooks Become Powerful: The Flywheel
Single hooks are nice. Combined hooks are where the real power emerges.
Week 1: Deploy a PreToolUse hook that blocks hardcoded secrets. You're done. The vast majority of credential leaks are now impossible.
Week 2: Add a PostToolUse hook that auto-formats code. Now every edit is automatically polished. No "please reformat" back-and-forth.
Week 3: Add a PreToolUse hook that enforces tests before PR. PRs now never arrive broken.
Week 4: Deploy managed-only hooks organization-wide. Inheritance is automatic. New repos get all three hooks instantly.
Month 2: You have saved your team hundreds of hours. You're not running code reviews looking for forgotten tests or formatting issues. You're not debugging credential leaks. The policies enforce themselves.
This is the leverage. One-time setup. Decades of return.
The Deterministic Guarantee
Here's what hooks give you that Claude's memory doesn't:
- No false confidence. Hooks execute or they don't. No "I forgot."
- No judgment calls. Hooks don't decide the code is "small enough" to skip. They run.
- No context dependency. Whether Claude is tired, focused, or distracted doesn't matter. Hooks fire.
- Auditability. You have a record of every hook execution. Compliance-ready.
- Team scaling. Deploy once, inherit by all. No per-developer configuration.
When you move from "Claude, please enforce this" to "Claude cannot violate this," you've moved from a collaborative tool to infrastructure.
Coming Next: Skills, Agents, and Private Marketplaces
With the configuration hierarchy (Post 1), CLAUDE.md standards (Post 2), and now deterministic hooks in place, you have the foundational layer for team governance.
In Part 4, we'll see how to package these hooks alongside skills and agents into a private marketplace. Imagine: every team member clones your repo. A single .claude/settings.json installs your security hooks, auto-formatting hooks, test enforcement hooks, and custom skills. New developers inherit your entire team's automation layer. No onboarding. No manual setup. Just clone, and go.
But first—try one hook this week. Pick one real problem you see Claude causing repeatedly. (Forgotten tests? Formatting issues? Wrong package manager?) Write the hook. See the relief when it just works.
That's the feeling you're building toward: policies that actually enforce themselves.
Series Navigation
- Post 1: Why Configuration Matters More Than Models
- Post 2: Building Your First CLAUDE.md
- Post 3: Hooks: Deterministic Policy Enforcement (you are here)
- Post 4: Skills, Agents, and Private Marketplaces
- Post 5: Measuring ROI and Scaling Beyond Day One
Skills, Agents, and Private Marketplaces: Scaling Team Capabilities
How to package team workflows into reusable skills, distribute specialized agents, and build a private marketplace that scales capabilities across your org without reinventing the wheel.
Building Your First CLAUDE.md: Team Standards as Code
CLAUDE.md is your team's shared playbook—the single file where coding standards, architecture decisions, and Claude Code behaviors live. Done right, it eliminates context-switching and makes Claude Code predictably effective across your entire team.
Why Your Claude Code Configuration Matters More Than Your Model Choice
Enterprise Claude Code isn't about picking the right model—it's about designing a configuration hierarchy that scales from individual workflows to organization-wide policy without friction.