skills/laurigates/claude-plugins/hooks-permission-request-hook

hooks-permission-request-hook

SKILL.md

/hooks:permission-request-hook

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.

When to Use This Skill

Use this skill when... Use /hooks:hooks-configuration instead when...
You want auto-approve/deny rules for Claude Code permissions Configuring other hook types (PreToolUse, Stop, SessionStart)
Replacing --dangerouslySkipPermissions with targeted rules Need general hooks knowledge or debugging
Setting up project-specific permission automation Writing entirely custom hook logic from scratch
You need a test harness to validate approve/deny behavior Understanding hook lifecycle events

Context

Detect project stack:

  • Lockfiles: !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' \)
  • Project files: !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' \)
  • Existing settings: !find .claude -maxdepth 1 -name 'settings.json' -type f
  • Existing hooks dir: !find . -maxdepth 2 -type d -name 'scripts'
  • jq available: !jq --version
  • Existing PermissionRequest hooks: !jq -r '.hooks.PermissionRequest // empty' .claude/settings.json

Parameters

Parse 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

Execution

Execute this workflow:

Step 1: Detect project stack

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.

Step 2: Generate the hook script

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:

  1. Including only sections for detected languages (remove {{ if ... }} markers)
  2. Including only selected categories if --category flags were provided
  3. Removing all template comments ({{ ... }})
  4. If --strict is set, include the strict mode catch-all deny at the end

Step 3: Generate the test script

Create scripts/test-permission-hook.sh (or .claude/hooks/test-permission-hook.sh to match the hook location).

The test script must include:

  1. A test_case function that:

    • Takes expected_decision, description, tool_name, tool_input_json
    • Constructs PermissionRequest JSON with jq -n
    • Pipes to the hook script, captures stdout
    • Parses decision (empty output = "passthrough")
    • Prints PASS/FAIL with color codes and tracks counts
  2. 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)
  1. A summary line: N passed, N failed, N total with exit 1 on any failure

Set HOOK_SCRIPT to the actual generated hook script path. Include test cases only for detected stacks and selected categories. Remove all {{ ... }} template markers.

Step 4: Configure .claude/settings.json

Read 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:

  • Replace the existing PermissionRequest hook
  • Add alongside the existing hook (both will run)
  • Abort and keep existing configuration

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

Step 5: Finalize

  1. Make both scripts executable: chmod +x <hook-path> <test-path>
  2. Create .claude/ directory if needed for settings.json
  3. Run the test script to verify all test cases pass
  4. Report summary:
    • List files created/modified
    • Show number of approve rules, deny rules
    • Show test results (pass/fail count)

Post-Actions

After generating the hook:

  1. Suggest committing the new files:
    scripts/permission-request.sh
    scripts/test-permission-hook.sh
    .claude/settings.json
    
  2. If --strict was NOT used, mention the flag for environments where unknown commands should be denied
  3. Explain how to add custom rules — edit the APPROVE/DENY sections of the generated script
  4. Remind about CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1 to toggle the hook off temporarily
  5. Note that empty matcher "" catches all tools; suggest narrowing to "Bash" if only Bash commands need filtering

PermissionRequest Schema

Input (via stdin)

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

Output (via stdout)

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

Agentic Optimizations

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

Quick Reference

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
Weekly Installs
2
GitHub Stars
13
First Seen
1 day ago
Installed on
openclaw2
gemini-cli2
github-copilot2
codex2
kimi-cli2
cursor2