hookwise
Intelligent permission gating for Claude Code.
Overview
Claude Code's built-in permission system is per-session and binary: approve or deny each tool call, every time. In multi-agent environments -- agent teams, swarms, parallel workers -- this creates permission fatigue (hundreds of prompts) and zero institutional memory (every session starts cold).
hookwise solves both problems with a learned permission policy that gets smarter over time. It sits between Claude Code and your tools as a PreToolUse hook, running a fast decision cascade that resolves most tool calls in microseconds from cache. Only genuinely novel or ambiguous operations reach a human.
Decisions are cached as sanitized JSONL, checked into git, and shared across contributors. New team members inherit the project's permission baseline on clone.
Installation
From GitHub releases
Download the prebuilt binary for your platform from the latest release:
# macOS (Apple Silicon)
|
# macOS (Intel)
|
# Linux (x86_64)
|
From source
Requires Rust 1.75+:
Or build a release binary directly:
# Binary at target/release/hookwise
Install as a Claude Code plugin
After the binary is in your PATH, install the plugin:
This registers two hooks automatically:
PreToolUse-- runshookwise checkon every tool callUserPromptSubmit-- runshookwise session-checkto prompt for role registration
It also provides slash commands (/hookwise register, disable, enable, switch, status).
Quick Start
Initialize in a repository
This creates .hookwise/ with default policy.yml, roles.yml, and empty rule files.
Register a role
# From a terminal
# Or via environment variable (CI/scripted use)
Once the plugin is installed, your first prompt will trigger an interactive role selection automatically.
How It Works
Every tool call passes through a 7-stage cascade. Each stage either resolves the decision or escalates to the next:
Tool call -> Sanitize -> Path Policy -> Cache -> Token Jaccard -> Embedding HNSW -> LLM -> Human
~5us ~1us ~100ns ~500ns ~1-5ms ~1-2s variable
| Tier | Mechanism | Latency | Resolves when |
|---|---|---|---|
| -- | Secret sanitization | ~5us | Always runs first; redacts secrets before any lookup |
| 0 | Path policy (globset) | ~1us | File path matches a role's allow/deny glob |
| 1 | Exact cache match | ~100ns | Sanitized command seen before (allow/deny auto-resolve; ask escalates to human) |
| 2a | Token Jaccard similarity | ~500ns | Token overlap with cached entry exceeds threshold (default 0.7) |
| 2b | Embedding HNSW similarity | ~1-5ms | Semantically similar command was previously decided |
| 3 | LLM supervisor agent | ~1-2s | Novel command evaluated against policy by supervisor |
| 4 | Human-in-the-loop | variable | LLM confidence too low, or ask state requires human judgment |
Every decision at tiers 3 and 4 feeds back into tiers 1, 2a, and 2b. The system converges toward full autonomy over time.
Tri-State Decisions
Three decision states, not two:
- allow -- permit the tool call, cached, auto-resolves on future matches
- deny -- block the tool call, cached, auto-resolves on future matches
- ask -- always prompt a human, cached as
askso it never auto-resolves
ask is for operations that should always get human eyes: writing to .claude/, .env, settings files, or any operation the user wants to stay aware of.
Precedence: DENY > ASK > ALLOW > silent
Roles
Twelve built-in roles across three categories. Each role combines deterministic path globs (tier 0) with a natural language description (tier 3 LLM).
Implementation roles
Write to specific code/config directories.
| Role | Writes to | Denied from |
|---|---|---|
coder |
src/, lib/, project config (Cargo.toml, package.json, etc.) |
tests/, docs/, .github/, *.tf |
tester |
tests/, test-fixtures/, *.test.*, *_test.go, test configs |
src/, lib/, docs/, .github/ |
integrator |
*.tf, *.tfvars, terraform/, infra/, pulumi/, helm/, ansible/ |
src/, lib/, tests/, docs/ |
devops |
.github/, Dockerfile*, docker-compose*, .*rc, tool version files |
src/, lib/, tests/, docs/ |
Knowledge roles
Read the codebase, write artifacts to docs/ subdirectories.
| Role | Writes to | Denied from |
|---|---|---|
researcher |
docs/research/ |
src/, lib/, tests/, .github/ |
architect |
docs/architecture/, docs/adr/ |
src/, lib/, tests/, .github/ |
planner |
docs/plans/ |
src/, lib/, tests/, .github/ |
reviewer |
docs/reviews/ (not security/) |
src/, lib/, tests/, .github/ |
security-reviewer |
docs/reviews/security/ |
src/, lib/, tests/, .github/ |
docs |
docs/, *.md, *.aisp |
src/, lib/, tests/, .github/ |
Full-access roles
Unrestricted file access for leads and debugging.
| Role | Writes to | Denied from |
|---|---|---|
maintainer |
** |
(none) |
troubleshooter |
** |
(none) |
Knowledge roles produce artifacts that implementation roles consume:
researcher -> architect -> planner -> coder/tester -> reviewer -> maintainer
Projects can define custom roles in .hookwise/roles.yml.
CLI Reference
hookwise <command> [options]
Hook mode
Called by Claude Code on every PreToolUse event. Reads hook payload from stdin as JSON, outputs a permission decision to stdout.
|
# {"hookSpecificOutput":{"permissionDecision":"allow"}}
Session check
Called on UserPromptSubmit. Outputs a registration prompt if the session is unregistered.
Registration
# Register a session with a role
# Disable hookwise for a session
# Re-enable after disable
Queue mode (human interface)
# List pending permission decisions
# Approve or deny a pending decision
# Cache as "ask" instead of allow/deny
# Codify as a persistent rule
Monitoring
# Stream decisions in real time
# View cache hit rates and decision distribution
Cache management
# Rebuild vector indexes from rules
# Clear cached decisions
Overrides
Set explicit permission overrides that take priority over cached LLM decisions.
Initialization and scanning
# Initialize .hookwise/ in a repo
# Pre-commit secret scan on staged files
Configuration
policy.yml
Project-level policy: sensitive paths, confidence thresholds, and behavioral settings.
sensitive_paths:
ask_write:
- ".claude/**"
- ".hookwise/**"
- ".env*"
- "**/.env*"
- ".git/hooks/**"
confidence:
org: 0.9
project: 0.7
user: 0.6
roles.yml
Role definitions with path policies. See Roles for the built-in set. Add custom roles here:
roles:
data-engineer:
description: |
Manages data pipelines, ETL scripts, and database migrations.
paths:
allow_write:
deny_write:
allow_read:
Storage layout
<repo>/
.hookwise/
policy.yml # Project policy (checked into git)
roles.yml # Role definitions (checked into git)
rules/ # Cached decisions (checked into git)
allow.jsonl
deny.jsonl
ask.jsonl
.index/ # Vector indexes (.gitignored, rebuilt locally)
.user/ # Personal preferences (.gitignored)
~/.config/hookwise/
config.yml # Global configuration
org/<org-name>/ # Org-wide rules
user/ # Personal cross-project rules
Rules are sanitized JSONL -- no secrets, human-readable, diffable, reviewable in PRs.
Scope hierarchy
Four scopes with strict precedence:
| Scope | What it governs | Where it lives |
|---|---|---|
| Org | Security floor for all repos | ~/.config/hookwise/org/<org>/ |
| Project | Project-specific permissions | <repo>/.hookwise/rules/ |
| User | Personal preferences | ~/.config/hookwise/user/ |
| Role | Task-scoped least privilege | Set at registration time |
DENY > ASK > ALLOW at every level. A deny at any scope is authoritative.
Plugin Setup
hookwise ships as a Claude Code plugin. After building:
The plugin registers two hooks automatically:
PreToolUse-- runshookwise checkon every tool callUserPromptSubmit-- runshookwise session-checkto prompt for role registration
It also provides slash commands:
| Command | Description |
|---|---|
/hookwise register |
Pick a role interactively |
/hookwise disable |
Opt out for this session |
/hookwise enable |
Re-enable after disable |
/hookwise switch |
Change role mid-session |
/hookwise status |
Show current role, path policy, cache stats |
Agent team setup
In multi-agent environments, the team lead registers each worker after spawning:
The LLM supervisor agent communicates with worker hooks over a Unix domain socket at /tmp/hookwise-<team-id>.sock.
Troubleshooting
Hook not firing
If hookwise is not intercepting tool calls:
- Verify the plugin is installed: Run
claude plugin listand confirmhookwiseappears. - Check hooks.json: The plugin directory should contain a
hooks.jsonwithPreToolUseandUserPromptSubmitentries. If missing, reinstall withclaude plugin add /path/to/hookwise. - Confirm the binary is in PATH: Run
which hookwise-- if it returns nothing, add the install directory to your PATH. - Check for errors: Run
hookwise checkmanually with sample input to see if the binary starts correctly:|
Session registration timeout
If you see "session not registered" errors or the hook blocks after 5 seconds:
- Use env var fallback: Set
HOOKWISE_ROLE=coder(or your desired role) as an environment variable. This bypasses the interactive registration flow. - Check registration file permissions: The registration state is stored under
.hookwise/. Ensure the current user has read/write access. - Register explicitly: Run
hookwise register --session-id "$SESSION_ID" --role <role>before starting your session.
Permission denied on socket
The LLM supervisor agent communicates over a Unix domain socket at /tmp/hookwise-<team-id>.sock:
- Check file permissions: Ensure the socket file is readable/writable by the current user.
- Stale socket: If a previous session crashed, a stale socket may remain. Remove it manually:
rm /tmp/hookwise-*.sockand restart. - tmpdir restrictions: On some systems,
/tmp/has restrictive permissions. Check your OS security settings (e.g., macOS sandboxing).
Secret false positives
If the sanitizer is flagging non-secret strings:
- Adjust entropy threshold: In
.hookwise/policy.yml, increase the Shannon entropy threshold above the default 4.0:sanitization: entropy_threshold: 4.5 - Check what triggered it: Run
hookwise scan --stagedto see which patterns matched. The sanitizer uses three layers (aho-corasick prefixes, regex patterns, entropy) -- the output indicates which layer flagged the string. - Add allowlist entries: For known safe patterns that repeatedly trigger false positives, add them to the allowlist in
policy.yml.
Vector index needs rebuild
If similarity search returns stale or no results after editing rule files:
This rebuilds the HNSW index from the current JSONL rule files. The index is stored in .hookwise/.index/ (gitignored) and must be rebuilt locally after cloning or pulling new rules.
Contributing
- Fork the repository
- Create a feature branch
- Run
cargo testandcargo clippybefore submitting - Ensure
hookwise scan --stagedpasses (no secrets in committed files) - Open a pull request
License
MIT