Skip to content

dokipen/claude-approve

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

claude-approve

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.

Install

go install github.com/dokipen/claude-approve/cmd/claude-approve@latest

Or 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/

Quick Start

  1. 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"
  1. Add the hook to your Claude Code settings (.claude/settings.json or .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.

  1. Use Claude Code as normal. Matching tool calls will be auto-approved or denied based on your rules.

Rule Types

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).

Tool Matching

Built-in tools

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.

Generic tools (MCP, WebFetch, WebSearch, etc.)

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).

Configuration

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"

Audit Logging

Set audit_level to control what gets logged:

  • off — no logging
  • matched (default) — log only when a rule matches
  • all — 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.

/audit Skill

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:

  1. Locates your hooks config and reads the audit_file path from it (falls back to ~/.claude/claude-tool-audit.jsonl)
  2. Shows you the resolved log path and audit_level, then asks for confirmation before reading the file
  3. Parses the JSONL and displays a breakdown by decision (allow / deny / ask / passthrough)
  4. Groups prompt-causing entries (ask and passthrough) by tool and pattern, sorted by frequency
  5. Generates ready-to-paste [[allow]] TOML rules targeting the most frequent prompt patterns
  6. 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)"

CLI Commands

run — Hook mode

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.toml

validate — Check config

Validates 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.jsonl

test — Test a rule

Test 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)

How It Works

  1. Claude Code invokes claude-approve run before each tool call via the PreToolUse hook
  2. The hook reads the JSON payload from stdin containing the tool name and parameters
  3. Rules are evaluated in priority order (deny > ask > allow), with log rules collected separately
  4. The decision is returned as JSON to stdout:
    • allow — auto-approves the tool call
    • deny — blocks with a reason shown to Claude
    • ask — falls back to Claude Code's interactive prompt
    • No output — passthrough to normal permissions

Credits

Inspired by kornysietsma/claude-code-permissions-hook and the blog post describing the approach.

About

Claude Code PreToolUse hook for auto-approving, denying, or escalating tool calls based on TOML rules

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors