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-pluginThis skill is limited to using the following tools:
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`.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
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 |