Help us improve
Share bugs, ideas, or general feedback.
From hooks-plugin
Generates PermissionRequest hooks that auto-approve safe operations, auto-deny dangerous ones, and tailor rules to detected project stack. Safer alternative to --dangerouslySkipPermissions for manual permission mode.
npx claudepluginhub laurigates/claude-plugins --plugin hooks-pluginHow this skill is triggered — by the user, by Claude, or both
Slash command
/hooks-plugin:hooks-permission-request-hookhaikuThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generate a `PermissionRequest` hook that auto-approves safe operations, auto-denies dangerous ones, and passes everything else through for user decision. A safer, project-aware alternative to `--dangerouslySkipPermissions`.
Configures Claude Code hooks for lifecycle events like PreToolUse, SessionStart, and automation use cases such as formatting enforcement and permission control.
Manages Claude Code repository hooks for adding, configuring, troubleshooting, enabling/disabling, and enforcing rules. Delegates to docs-management for official documentation.
Guides creation of markdown-based Hookify rules to block dangerous bash commands, warn on risky file edits, and enforce behavioral guardrails in Claude Code.
Share bugs, ideas, or general feedback.
Generate a PermissionRequest hook that auto-approves safe operations, auto-denies dangerous ones, and passes everything else through for user decision. A safer, project-aware alternative to --dangerouslySkipPermissions.
| Use this skill when... | Use auto mode instead when... | Use /hooks:hooks-configuration instead when... |
|---|---|---|
| You need deterministic, auditable approve/deny rules | AI-powered classification is sufficient | Configuring other hook types (PreToolUse, Stop, SessionStart) |
| Enterprise compliance requires explicit rule lists | Built-in block rules cover your needs | Need general hooks knowledge or debugging |
| Custom project-specific policies beyond auto mode defaults | You want zero-config permission handling | Writing entirely custom hook logic from scratch |
| You need a test harness to validate approve/deny behavior | You accept ~0.4% false positive rate | Understanding hook lifecycle events |
Auto mode's Tier 3 classifier provides AI-powered permission classification that largely replaces the approve/deny logic this skill generates. Auto mode includes 20+ built-in block categories (force-pushes, mass deletions, credential harvesting, etc.) and handles safe operations automatically.
This skill remains valuable for:
Both can coexist — PermissionRequest hooks provide a secondary layer when auto mode is enabled. See .claude/rules/auto-mode.md for details.
Detect project stack:
find . -maxdepth 1 \( -name 'package-lock.json' -o -name 'yarn.lock' -o -name 'pnpm-lock.yaml' -o -name 'bun.lockb' -o -name 'poetry.lock' -o -name 'uv.lock' -o -name 'Cargo.lock' -o -name 'go.sum' -o -name 'Gemfile.lock' \)find . -maxdepth 1 \( -name 'package.json' -o -name 'pyproject.toml' -o -name 'requirements.txt' -o -name 'Cargo.toml' -o -name 'go.mod' -o -name 'Gemfile' \)find .claude -maxdepth 1 -name 'settings.json' -type ffind . -maxdepth 2 -type d -name 'scripts'jq --versionjq -r '.hooks.PermissionRequest // empty' .claude/settings.jsonParse these from $ARGUMENTS:
| Flag | Default | Description |
|---|---|---|
--strict | off | Deny unrecognized Bash commands by default instead of passing through to user |
--category <name> | all | Include only specific rule categories. Repeatable. Values: git, test, lint, build, gh, deny |
Execute this workflow:
Identify languages and tooling from the context above.
Language detection:
| File Present | Language | Package Manager (from lockfile) |
|---|---|---|
package.json | Node.js | npm (package-lock.json), yarn (yarn.lock), pnpm (pnpm-lock.yaml), bun (bun.lockb) |
pyproject.toml / requirements.txt | Python | poetry (poetry.lock), uv (uv.lock), pip (fallback) |
Cargo.toml | Rust | cargo |
go.mod | Go | go modules |
Gemfile | Ruby | bundler |
Report detected stack to user before generating.
Create the script at scripts/permission-request.sh (or .claude/hooks/permission-request.sh if no scripts/ directory exists).
Script template — adapt per detected stack and selected categories:
#!/usr/bin/env bash
# PermissionRequest hook — auto-approve safe operations, auto-deny dangerous ones
# Generated by /hooks:permission-request-hook
#
# Toggle: set CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1 to skip
#
# Decisions:
# approve → tool runs without user prompt
# deny → tool blocked, reason shown to Claude
# (no output) → user prompted as normal (passthrough)
set -euo pipefail
# Toggle off
[ "${CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST:-}" = "1" ] && exit 0
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
approve() { echo "{\"decision\": \"approve\", \"reason\": \"$1\"}"; exit 0; }
deny() { echo "{\"decision\": \"deny\", \"reason\": \"$1\"}"; exit 0; }
# ══════════════════════════════════════════════════════════════
# AUTO-APPROVE: Safe, read-only operations
# ══════════════════════════════════════════════════════════════
# Non-Bash tools that are always safe
case "$TOOL_NAME" in
Read|Glob|Grep) approve "Read-only tool" ;;
esac
# Only process Bash commands below
[ "$TOOL_NAME" != "Bash" ] && exit 0
[ -z "$COMMAND" ] && exit 0
{{ if category includes 'git' or all categories }}
# ── Git: read-only operations ──
if echo "$COMMAND" | grep -Eq '^\s*git\s+(status|log|diff|branch|remote|show|blame|shortlog|describe|ls-files|rev-parse|rev-list|stash\s+list|tag\s+-l|fetch)\b'; then
approve "Read-only git operation"
fi
{{ endif }}
{{ if category includes 'test' or all categories }}
# ── Test runners ──
{{ if Node.js detected }}
if echo "$COMMAND" | grep -Eq '^\s*(npm\s+test|npx\s+(vitest|jest)|bun\s+test|node\s+--test)\b'; then
approve "Test execution"
fi
{{ endif }}
{{ if Python detected }}
if echo "$COMMAND" | grep -Eq '^\s*(pytest|python\s+-m\s+pytest)\b'; then
approve "Test execution"
fi
{{ endif }}
{{ if Rust detected }}
if echo "$COMMAND" | grep -Eq '^\s*cargo\s+test\b'; then
approve "Test execution"
fi
{{ endif }}
{{ if Go detected }}
if echo "$COMMAND" | grep -Eq '^\s*go\s+test\b'; then
approve "Test execution"
fi
{{ endif }}
{{ if Ruby detected }}
if echo "$COMMAND" | grep -Eq '^\s*bundle\s+exec\s+(rspec|rake\s+test)\b'; then
approve "Test execution"
fi
{{ endif }}
# Generic make test
if echo "$COMMAND" | grep -Eq '^\s*make\s+test\b'; then
approve "Test execution"
fi
{{ endif }}
{{ if category includes 'lint' or all categories }}
# ── Linters and formatters (check/read-only mode) ──
{{ if Node.js detected }}
if echo "$COMMAND" | grep -Eq '^\s*(npx\s+(biome\s+check|eslint|prettier\s+--check)|bun\s+run\s+(lint|check|format))\b'; then
approve "Linter/formatter check"
fi
if echo "$COMMAND" | grep -Eq '^\s*tsc\s+--noEmit\b'; then
approve "Type check"
fi
{{ endif }}
{{ if Python detected }}
if echo "$COMMAND" | grep -Eq '^\s*(ruff\s+check|mypy|pyright)\b'; then
approve "Linter/type check"
fi
{{ endif }}
{{ if Rust detected }}
if echo "$COMMAND" | grep -Eq '^\s*cargo\s+(clippy|fmt\s+--check)\b'; then
approve "Linter/formatter check"
fi
{{ endif }}
{{ if Go detected }}
if echo "$COMMAND" | grep -Eq '^\s*(golangci-lint\s+run|go\s+vet)\b'; then
approve "Linter check"
fi
{{ endif }}
{{ endif }}
{{ if category includes 'build' or all categories }}
# ── Build commands ──
{{ if Node.js detected }}
if echo "$COMMAND" | grep -Eq '^\s*(npm\s+run\s+build|bun\s+run\s+build)\b'; then
approve "Build command"
fi
{{ endif }}
{{ if Rust detected }}
if echo "$COMMAND" | grep -Eq '^\s*cargo\s+build\b'; then
approve "Build command"
fi
{{ endif }}
{{ if Go detected }}
if echo "$COMMAND" | grep -Eq '^\s*go\s+build\b'; then
approve "Build command"
fi
{{ endif }}
if echo "$COMMAND" | grep -Eq '^\s*make\s+(build|all)?\s*$'; then
approve "Build command"
fi
{{ endif }}
{{ if category includes 'gh' or all categories }}
# ── GitHub CLI: read operations ──
if echo "$COMMAND" | grep -Eq '^\s*gh\s+(pr\s+(view|checks|list|diff)|issue\s+(view|list)|run\s+(view|list))\b'; then
approve "GitHub CLI read operation"
fi
{{ endif }}
# ── Package info queries ──
{{ if Node.js detected }}
if echo "$COMMAND" | grep -Eq '^\s*npm\s+ls\b'; then
approve "Package info query"
fi
{{ endif }}
{{ if Rust detected }}
if echo "$COMMAND" | grep -Eq '^\s*cargo\s+tree\b'; then
approve "Package info query"
fi
{{ endif }}
{{ if Go detected }}
if echo "$COMMAND" | grep -Eq '^\s*go\s+list\b'; then
approve "Package info query"
fi
{{ endif }}
{{ if Python detected }}
if echo "$COMMAND" | grep -Eq '^\s*(pip\s+list|pip\s+show)\b'; then
approve "Package info query"
fi
{{ endif }}
# ══════════════════════════════════════════════════════════════
# AUTO-DENY: Dangerous operations
# ══════════════════════════════════════════════════════════════
{{ if category includes 'deny' or all categories }}
# Destructive filesystem operations on root or home
# shellcheck disable=SC2016
if echo "$COMMAND" | grep -Eq 'rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)(/\s|/\*|~/|\$HOME)'; then
deny "Destructive operation on root or home directory"
fi
# Force push to protected branches
if echo "$COMMAND" | grep -Eq 'git\s+push\s+.*--force.*\s(main|master)\b'; then
deny "Force push to protected branch"
fi
# Insecure permissions
if echo "$COMMAND" | grep -Eq 'chmod\s+777\b'; then
deny "Insecure permissions (chmod 777)"
fi
# Piped network execution
if echo "$COMMAND" | grep -Eq '(curl|wget)\s.*\|\s*(bash|sh|zsh)'; then
deny "Piped network execution (curl|bash)"
fi
# Fork bombs
if echo "$COMMAND" | grep -Eq ':\(\)\s*\{.*:\|:.*\}'; then
deny "Fork bomb detected"
fi
# Block device writes
if echo "$COMMAND" | grep -Eq '(dd\s+.*of=/dev/|>\s*/dev/sd)'; then
deny "Direct block device write"
fi
# Filesystem formatting
if echo "$COMMAND" | grep -Eq '^\s*mkfs\b'; then
deny "Filesystem format operation"
fi
# Destructive git clean at repo root
if echo "$COMMAND" | grep -Eq '^\s*git\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d'; then
deny "Destructive git clean"
fi
# Database destructive operations
if echo "$COMMAND" | grep -Eiq '(psql|mysql|sqlite3).*\b(DROP\s+(DATABASE|TABLE)|TRUNCATE)\b'; then
deny "Database destructive operation"
fi
{{ endif }}
{{ if --strict }}
# ══════════════════════════════════════════════════════════════
# STRICT MODE: Deny unrecognized Bash commands
# ══════════════════════════════════════════════════════════════
deny "Unrecognized command (strict mode). Disable with CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1"
{{ endif }}
# ══════════════════════════════════════════════════════════════
# PASS THROUGH: Everything else requires user decision
# ══════════════════════════════════════════════════════════════
exit 0
Adapt the template by:
{{ if ... }} markers)--category flags were provided{{ ... }})--strict is set, include the strict mode catch-all deny at the endCreate scripts/test-permission-hook.sh (or .claude/hooks/test-permission-hook.sh to match the hook location).
The test script must include:
A test_case function that:
expected_decision, description, tool_name, tool_input_jsonjq -n"passthrough")Test cases for each included category:
| Category | Approve Tests | Deny Tests |
|---|---|---|
| Always | Read tool, Glob tool, Grep tool | — |
git | git status, git log, git diff, git branch, git fetch, git stash list | — |
test | Per detected stack (e.g., npm test, pytest, cargo test) | — |
lint | Per detected stack (e.g., npx biome check, ruff check) | — |
build | Per detected stack (e.g., npm run build, cargo build) | — |
gh | gh pr view 123, gh issue list | — |
deny | — | rm -rf /, rm -rf ~/, rm -rf $HOME, git push --force origin main, chmod 777, curl | bash, wget | sh, dd of=/dev/sda, mkfs, git clean -fdx, DROP DATABASE |
| Always | — | — |
| Passthrough | — | npm install express, echo hello, Write tool |
--strict | — | some-unknown-cmd --flag (deny in strict mode) |
N passed, N failed, N total with exit 1 on any failureSet HOOK_SCRIPT to the actual generated hook script path. Include test cases only for detected stacks and selected categories. Remove all {{ ... }} template markers.
.claude/settings.jsonRead existing .claude/settings.json if it exists. Merge the PermissionRequest hook — preserve all existing configuration.
If a PermissionRequest hook already exists, ask the user whether to:
Configuration to merge:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/scripts/permission-request.sh\"",
"timeout": 10
}
]
}
]
}
}
Use timeout: 10 (10 seconds). Use empty matcher "" to match all tools. Adjust path if script is in .claude/hooks/ instead of scripts/.
chmod +x <hook-path> <test-path>.claude/ directory if needed for settings.jsonAfter generating the hook:
scripts/permission-request.sh
scripts/test-permission-hook.sh
.claude/settings.json
--strict was NOT used, mention the flag for environments where unknown commands should be deniedCLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1 to toggle the hook off temporarily"" catches all tools; suggest narrowing to "Bash" if only Bash commands need filtering| Field | Type | Description |
|---|---|---|
session_id | string | Current session ID |
tool_name | string | Tool being invoked (Bash, Write, Edit, Read, etc.) |
tool_input | object | Tool-specific input (.command for Bash, .file_path for Write/Edit) |
permission_type | string | Always "tool_use" |
description | string | Human-readable description of the operation |
| Decision | JSON | Effect |
|---|---|---|
| Approve | {"decision":"approve","reason":"..."} | Tool runs without user prompt |
| Deny | {"decision":"deny","reason":"..."} | Tool blocked, reason shown to Claude |
| Passthrough | Exit 0 with no output | User prompted as normal |
| Context | Approach |
|---|---|
| Quick setup, all categories | /hooks:permission-request-hook |
| Strict mode (deny unknown commands) | /hooks:permission-request-hook --strict |
| Only git and test rules | /hooks:permission-request-hook --category git --category test |
| Only deny rules (block dangerous ops) | /hooks:permission-request-hook --category deny |
| Test the hook manually | echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | bash scripts/permission-request.sh |
| Disable hook temporarily | CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1 |
| Item | Value |
|---|---|
| Hook event | PermissionRequest |
| Script location | scripts/permission-request.sh or .claude/hooks/permission-request.sh |
| Test script | scripts/test-permission-hook.sh |
| Settings location | .claude/settings.json |
| Timeout | 10 seconds |
| Matcher | "" (all tools) |
| Toggle | CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1 |
| Decisions | approve, deny, passthrough (no output) |
| Categories | git, test, lint, build, gh, deny |