/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 |
Auto Mode vs PermissionRequest Hook
Auto mode (Claude Code 2.1.83+) routes most approve/deny decisions through a classifier model. It overlaps with — but does not replace — a PermissionRequest hook. Choose this skill when you need any of:
- Deterministic, auditable rules — the classifier's probabilistic answer is unsuitable for compliance contexts
- Project-specific deny lists that go beyond the default trust set
- Hard 0% false-positive guarantees for specific commands (the hook is exact, the classifier is not)
- Pre-approve narrow patterns so they skip the classifier round-trip and avoid latency/token cost
Hooks coexist with auto mode — they fire alongside the classifier. See .claude/rules/auto-mode.md for the full auto-mode model and how it interacts with allow rules and subagents.
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).
Use the Script Template from REFERENCE.md. Adapt it by:
- Including only sections for detected languages (remove
{{ if ... }}markers) - Including only selected categories if
--categoryflags were provided - Removing all template comments (
{{ ... }}) - If
--strictis 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).
Use the Test Script Specification from REFERENCE.md. 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
- Make both scripts executable:
chmod +x <hook-path> <test-path> - Create
.claude/directory if needed for settings.json - Run the test script to verify all test cases pass
- 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:
- Suggest committing the new files:
scripts/permission-request.sh scripts/test-permission-hook.sh .claude/settings.json - If
--strictwas NOT used, mention the flag for environments where unknown commands should be denied - Explain how to add custom rules — edit the APPROVE/DENY sections of the generated script
- Remind about
CLAUDE_HOOKS_DISABLE_PERMISSION_REQUEST=1to toggle the hook off temporarily - 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 |