A Claude Code PreToolUse hook that auto-approves, denies, or escalates tool calls based on configurable TOML rules.
Instead of clicking "allow" on every safe command or worrying about dangerous ones slipping through, define rules once and let claude-approve handle permission decisions automatically.
go install github.com/dokipen/claude-approve/cmd/claude-approve@latestOr build from source:
git clone https://github.com/dokipen/claude-approve.git
cd claude-approve
go build -o claude-approve ./cmd/claude-approve/
# Move to somewhere on your PATH
mv claude-approve /usr/local/bin/- Create a config file at
~/.claude/hooks-config.toml:
[[deny]]
tool = "Bash"
command_regex = "^rm .*-rf"
reason = "Dangerous recursive delete"
[[allow]]
tool = "Bash"
command_regex = "^(git|flutter|dart|go) "
command_exclude_regex = "&&|;|\\||`"
reason = "Standard dev commands without shell chaining"
[[allow]]
tool = "Read"
file_path_regex = ".*"
reason = "Allow all reads"- Add the hook to your Claude Code settings (
.claude/settings.jsonor.claude/settings.local.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "claude-approve run --config ~/.claude/hooks-config.toml"
}
]
}
]
}
}Note: Use
"matcher": ".*"to send all tool calls through claude-approve. Tools without matching rules will passthrough to Claude Code's normal permission system.
- Use Claude Code as normal. Matching tool calls will be auto-approved or denied based on your rules.
Rules are evaluated in priority order: deny > ask > allow > log.
| Type | Effect |
|---|---|
deny |
Block the tool call. Claude sees the reason and adjusts. |
ask |
Prompt the user via Claude Code's built-in permission dialog. |
allow |
Auto-approve the tool call silently. |
log |
Write an audit entry but don't affect the permission decision. |
If no rule matches, the tool call falls through to Claude Code's normal permission system (passthrough).
These tools have structured input matching via command_regex / file_path_regex:
| Tool | Match fields |
|---|---|
Bash |
command_regex, command_exclude_regex |
Read |
file_path_regex, file_path_exclude_regex |
Edit |
file_path_regex, file_path_exclude_regex |
Write |
file_path_regex, file_path_exclude_regex |
Grep |
file_path_regex, file_path_exclude_regex |
Glob |
file_path_regex, file_path_exclude_regex |
Each rule matches on:
- Include regex: the tool call must match this pattern
- Exclude regex (optional): if the tool call also matches this, the rule is skipped
This "allow X but not if it also matches Y" pattern prevents command injection while permitting legitimate operations.
Use tool_regex to match any tool by name pattern. This is useful for MCP servers, web tools, and other tools that don't have structured input fields:
# Allow all tools from a specific MCP server
[[allow]]
tool_regex = "^mcp__workshop__"
reason = "Workshop MCP tools"
# Allow web tools
[[allow]]
tool_regex = "^Web(Fetch|Search)$"
reason = "Web tools"
# Deny a specific MCP tool
[[deny]]
tool_regex = "^mcp__dangerous__delete"
reason = "Dangerous MCP operation"Adding command_regex, command_exclude_regex, file_path_regex, or file_path_exclude_regex to a tool_regex rule is a configuration error — these constraints are not supported for generic tools. Use tool = (exact match) for structured tools like Bash, Read, Edit, Write, Update, Grep, and Glob.
Each rule must have exactly one of tool (exact match) or tool_regex (regex match).
Full example at examples/hooks-config.toml.
[audit]
audit_file = "/tmp/claude-tool-audit.jsonl"
audit_level = "matched" # off | matched | all
# Deny dangerous commands
[[deny]]
tool = "Bash"
command_regex = "^rm .*-rf"
reason = "Dangerous recursive delete"
# Auto-approve safe commands (no shell chaining)
[[allow]]
tool = "Bash"
command_regex = "^(git|flutter|dart|go) "
command_exclude_regex = "&&|;|\\||`|\\$\\("
reason = "Dev commands without chaining"
# Auto-approve MCP tools from trusted servers
[[allow]]
tool_regex = "^mcp__workshop__"
reason = "Workshop MCP tools"
# Auto-approve web tools
[[allow]]
tool_regex = "^Web(Fetch|Search)$"
reason = "Web tools"
# Prompt for confirmation on lock files
[[ask]]
tool = "Edit"
file_path_regex = "\\.lock$"
reason = "Lock files need confirmation"
# Audit all bash commands
[[log]]
tool = "Bash"
command_regex = ".*"
reason = "Audit all bash"Set audit_level to control what gets logged:
off— no loggingmatched(default) — log only when a rule matchesall— log every tool call, even passthrough
Each line in the audit file is a JSON object:
{"timestamp":"2026-03-01T14:22:29Z","tool_name":"Bash","tool_input":"git status","rule_type":"allow","rule_tool":"Bash","rule_reason":"Dev commands","decision":"allow"}Fields:
| Field | Description |
|---|---|
timestamp |
RFC3339 UTC timestamp |
tool_name |
Claude Code tool that was invoked |
tool_input |
Summarized input (command, file path, etc.) |
rule_type |
Rule type that matched: allow, deny, ask |
rule_tool |
tool value from the matched rule |
rule_tool_regex |
tool_regex value from the matched rule |
rule_reason |
reason string from the matched rule |
decision |
Final decision: allow, deny, ask, passthrough |
log_reasons |
Reasons from any [[log]] rules that fired (array, omitted if none) |
A recommended log path is ~/.claude/claude-tool-audit.jsonl. The file is created automatically if it doesn't exist; its parent directory is created with mode 0700.
The /audit skill analyzes an existing audit log and recommends [[allow]] rules to reduce how often Claude Code prompts for permission. It is a Claude Code slash command — run it inside a Claude Code session.
/audit
What it does:
- Locates your hooks config and reads the
audit_filepath from it (falls back to~/.claude/claude-tool-audit.jsonl) - Shows you the resolved log path and
audit_level, then asks for confirmation before reading the file - Parses the JSONL and displays a breakdown by decision (
allow/deny/ask/passthrough) - Groups prompt-causing entries (
askandpassthrough) by tool and pattern, sorted by frequency - Generates ready-to-paste
[[allow]]TOML rules targeting the most frequent prompt patterns - Offers to append the recommended rules directly to your config file
Example session:
/audit
# Audit log: /home/user/.claude/claude-tool-audit.jsonl (audit_level: matched)
# 248 total entries — allow: 195, deny: 12, ask: 8, passthrough: 33
#
# Top prompt-causing patterns:
# Pattern | Tool | Count | % of Prompts
# ---------|-------|-------|-------------
# npm | Bash | 18 | 43.9%
# .lock | Edit | 8 | 19.5%
# ...
#
# Recommended rules — would eliminate ~26 of 41 prompts (63%):
# [[allow]]
# tool = "Bash"
# command_regex = "^npm( |$)"
# reason = "Allow npm commands (18 prompts eliminated)"
Reads Claude Code's JSON payload from stdin, evaluates rules, writes the permission decision to stdout.
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | claude-approve run --config ~/.claude/hooks-config.tomlValidates the TOML config and reports rule counts.
claude-approve validate --config ~/.claude/hooks-config.toml
# config OK: 3 deny, 5 allow, 2 ask, 1 log rules
# audit: level=matched, file=/tmp/claude-tool-audit.jsonlTest how a specific tool call would be evaluated without running as a hook.
claude-approve test --config ~/.claude/hooks-config.toml --tool Bash --input '{"command":"rm -rf /"}'
# tool: Bash
# decision: deny
# reason: Dangerous recursive delete
# matched: deny rule (tool=Bash, command_regex=^rm .*-rf)- Claude Code invokes
claude-approve runbefore each tool call via thePreToolUsehook - The hook reads the JSON payload from stdin containing the tool name and parameters
- Rules are evaluated in priority order (deny > ask > allow), with log rules collected separately
- The decision is returned as JSON to stdout:
allow— auto-approves the tool calldeny— blocks with a reason shown to Claudeask— falls back to Claude Code's interactive prompt- No output — passthrough to normal permissions
Inspired by kornysietsma/claude-code-permissions-hook and the blog post describing the approach.