SuperAPI AI Directory

hooks-permission-request-hook

General & other

View repository

Updated 10-15 23:49

About this skill

/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