claude-gatekeeper
A fast PreToolUse permission hook for Claude Code that replaces glob-based permissions arrays in settings.json with PCRE2-compatible regex rules.
Why
Claude Code's built-in permission globs (Bash(git add:*)) can't match env-prefixed commands like FOO=bar git commit, pipe chains, or complex argument patterns. Regex can.
claude-gatekeeper evaluates every tool call against a layered set of regex rules and returns allow, deny, or abstains (passes to the user). Deny always wins. When a tool call is denied, Claude sees the reason and can adjust its approach.
Install
From a marketplace
/plugin marketplace add jim80net/claude-plugins
/plugin install claude-gatekeeper@jim80net-plugins
Pre-built binaries for Linux, macOS, and Windows (amd64/arm64) are auto-downloaded from GitHub Releases on first run. Default rules are auto-copied to ~/.claude/gatekeeper.toml on first run.
Windows (PowerShell): If you're on native Windows without Git Bash, edit hooks/hooks.json and change the command to:
powershell -NoProfile -ExecutionPolicy Bypass -File ${CLAUDE_PLUGIN_ROOT}/bin/run.ps1
Local development
git clone https://github.com/jim80net/claude-gatekeeper.git
cd claude-gatekeeper
make build
claude --plugin-dir .
From a GitHub release
Download a pre-built archive from Releases, extract it, and point Claude Code at the extracted directory:
claude --plugin-dir /path/to/claude-gatekeeper
How it works
- Claude Code invokes the gatekeeper before each tool call, sending JSON on stdin.
- On first run, the shipped
gatekeeper.toml is auto-copied to ~/.claude/gatekeeper.toml if it doesn't exist.
- Rules are loaded from:
- Global config —
~/.claude/gatekeeper.toml (auto-installed on first run)
- Project config —
.claude/gatekeeper.toml
- Each rule has a
tool regex (matched against the tool name) and an input regex (matched against the command/file path/URL).
- Deny always wins: if any deny rule matches, the call is blocked and Claude is told why.
- If any allow rule matches (and no deny), the call is auto-approved.
- If nothing matches (or no config exists), the gatekeeper abstains and Claude Code prompts you.
Default rules
The shipped gatekeeper.toml (auto-installed to ~/.claude/gatekeeper.toml on first run) denies:
| Category | Examples |
|---|
| Destructive git | git reset --hard, git clean -f, git push --force, git commit --amend, git branch -D |
| Push to main/master | Explicit (git push origin main) and implicit (on main branch, run git push) |
| Recursive delete | rm -r, rm -rf |
| sed/awk | Forces the Edit tool instead |
| Destructive SQL | DROP, TRUNCATE, DELETE FROM |
| npm | Use pnpm instead (commented out by default — uncomment to enable) |
| Credential files | .env, .envrc, *key.json, id_rsa, .pem, credentials |
And allows:
| Category | Examples |
|---|
| Version control | git, gh |
| Containers | docker, docker-compose |
| Python | python, uv, pip, pytest |
| Go | go build, go test, golangci-lint |
| JavaScript/TypeScript | node, npx, pnpm, eslint, vitest |
| Build systems | make, cargo, gradle, mvn |
| Infrastructure | terraform, kubectl, helm, aws, gcloud |
| Shell utilities | ls, find, mkdir, curl, diff, wc, jq, openssl, timeout |
| Non-Bash tools | Read, Edit, Write, Glob, Grep, Agent, WebFetch |
Configuration
Rule format
[[rules]]
tool = 'Bash' # PCRE2 regex matching tool_name
input = 'git\s+reset\s+--hard' # PCRE2 regex matching the primary input
decision = "deny" # "allow" or "deny"
reason = "Destructive: git reset" # Shown to Claude on deny
Preconditions (shell checks)
For rules that need runtime context (e.g., checking the current git branch):
[[rules]]
tool = 'Bash'
input = '\bgit\s+push\b(?!.*\b(main|master)\b)'
precondition = 'git branch --show-current'
precondition_match = '^(main|master)$'
decision = "deny"
reason = "Implicit push to main/master"
The precondition command runs only when tool and input both match. It has a 5-second timeout.
Env-prefix aware variants
Commands like FOO=bar git commit bypass anchored patterns. The defaults include commented-out variants:
# Default (anchored):
input = '(?:^|[|;&]\s*)git\s'
# Env-prefix aware (uncomment to enable):
# input = '(?:^|(\w+=\S+\s+)*)git\s'
Config layering
| File | Scope |
|---|
~/.claude/gatekeeper.toml | All projects (global — auto-installed on first run) |
.claude/gatekeeper.toml | Per-project (appended to global) |