hookwise 1.0.6

Intelligent permission gating for AI coding assistants
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# hookwise

Intelligent permission gating for Claude Code.

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org)

## 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](https://github.com/epiphytic/hookwise/releases/latest):

```bash
# macOS (Apple Silicon)
curl -L https://github.com/epiphytic/hookwise/releases/latest/download/hookwise-aarch64-apple-darwin.tar.gz \
  | tar xz -C /usr/local/bin

# macOS (Intel)
curl -L https://github.com/epiphytic/hookwise/releases/latest/download/hookwise-x86_64-apple-darwin.tar.gz \
  | tar xz -C /usr/local/bin

# Linux (x86_64)
curl -L https://github.com/epiphytic/hookwise/releases/latest/download/hookwise-x86_64-unknown-linux-gnu.tar.gz \
  | tar xz -C /usr/local/bin
```

### From source

Requires Rust 1.75+:

```bash
cargo install --path .
```

Or build a release binary directly:

```bash
cargo build --release
# Binary at target/release/hookwise
```

### Install as a Claude Code plugin

After the binary is in your PATH, install the plugin:

```bash
claude plugin add /path/to/hookwise
```

This registers two hooks automatically:

- **`PreToolUse`** -- runs `hookwise check` on every tool call
- **`UserPromptSubmit`** -- runs `hookwise session-check` to prompt for role registration

It also provides slash commands (`/hookwise register`, `disable`, `enable`, `switch`, `status`).

## Quick Start

### Initialize in a repository

```bash
cd your-repo
hookwise init
```

This creates `.hookwise/` with default `policy.yml`, `roles.yml`, and empty rule files.

### Register a role

```bash
# From a terminal
hookwise register --session-id "$SESSION_ID" --role coder

# Or via environment variable (CI/scripted use)
export HOOKWISE_ROLE=coder
```

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 `ask` so 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.

```bash
echo '{"session_id":"abc","tool_name":"Bash","tool_input":{"command":"pytest"}}' \
  | hookwise check
# {"hookSpecificOutput":{"permissionDecision":"allow"}}
```

### Session check

Called on `UserPromptSubmit`. Outputs a registration prompt if the session is unregistered.

```bash
hookwise session-check
```

### Registration

```bash
# Register a session with a role
hookwise register --session-id <id> --role <role> \
  [--task <description>] [--prompt-file <path>]

# Disable hookwise for a session
hookwise disable --session-id <id>

# Re-enable after disable
hookwise enable --session-id <id>
```

### Queue mode (human interface)

```bash
# List pending permission decisions
hookwise queue

# Approve or deny a pending decision
hookwise approve <id>
hookwise deny <id>

# Cache as "ask" instead of allow/deny
hookwise approve <id> --always-ask

# Codify as a persistent rule
hookwise approve <id> --add-rule --scope project
```

### Monitoring

```bash
# Stream decisions in real time
hookwise monitor

# View cache hit rates and decision distribution
hookwise stats
```

### Cache management

```bash
# Rebuild vector indexes from rules
hookwise build

# Clear cached decisions
hookwise invalidate --role <role>
hookwise invalidate --scope project
hookwise invalidate --all
```

### Overrides

Set explicit permission overrides that take priority over cached LLM decisions.

```bash
hookwise override --role tester --command "docker compose up" --allow
hookwise override --role coder --tool Write --file ".claude/*" --ask
hookwise override --role coder --command "npm publish" --deny --scope project
```

### Initialization and scanning

```bash
# Initialize .hookwise/ in a repo
hookwise init

# Pre-commit secret scan on staged files
hookwise scan --staged .hookwise/rules/
```

## Configuration

### policy.yml

Project-level policy: sensitive paths, confidence thresholds, and behavioral settings.

```yaml
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](#roles) for the built-in set. Add custom roles here:

```yaml
roles:
  data-engineer:
    description: |
      Manages data pipelines, ETL scripts, and database migrations.
    paths:
      allow_write: ["pipelines/**", "migrations/**", "sql/**"]
      deny_write: ["src/**", "tests/**", "docs/**"]
      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:

```bash
cargo build --release
claude plugin add /path/to/hookwise
```

The plugin registers two hooks automatically:

- **`PreToolUse`** -- runs `hookwise check` on every tool call
- **`UserPromptSubmit`** -- runs `hookwise session-check` to 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:

```bash
hookwise register \
  --session-id "$WORKER_SESSION_ID" \
  --role tester \
  --task "Run pytest suite for auth module" \
  --prompt-file /tmp/.hookwise-prompt-$WORKER_SESSION_ID
```

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:

1. **Verify the plugin is installed**: Run `claude plugin list` and confirm `hookwise` appears.
2. **Check hooks.json**: The plugin directory should contain a `hooks.json` with `PreToolUse` and `UserPromptSubmit` entries. If missing, reinstall with `claude plugin add /path/to/hookwise`.
3. **Confirm the binary is in PATH**: Run `which hookwise` -- if it returns nothing, add the install directory to your PATH.
4. **Check for errors**: Run `hookwise check` manually with sample input to see if the binary starts correctly:
   ```bash
   echo '{"session_id":"test","tool_name":"Bash","tool_input":{"command":"ls"}}' \
     | hookwise check
   ```

### Session registration timeout

If you see "session not registered" errors or the hook blocks after 5 seconds:

1. **Use env var fallback**: Set `HOOKWISE_ROLE=coder` (or your desired role) as an environment variable. This bypasses the interactive registration flow.
2. **Check registration file permissions**: The registration state is stored under `.hookwise/`. Ensure the current user has read/write access.
3. **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`:

1. **Check file permissions**: Ensure the socket file is readable/writable by the current user.
2. **Stale socket**: If a previous session crashed, a stale socket may remain. Remove it manually: `rm /tmp/hookwise-*.sock` and restart.
3. **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:

1. **Adjust entropy threshold**: In `.hookwise/policy.yml`, increase the Shannon entropy threshold above the default 4.0:
   ```yaml
   sanitization:
     entropy_threshold: 4.5
   ```
2. **Check what triggered it**: Run `hookwise scan --staged` to see which patterns matched. The sanitizer uses three layers (aho-corasick prefixes, regex patterns, entropy) -- the output indicates which layer flagged the string.
3. **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:

```bash
hookwise build
```

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

1. Fork the repository
2. Create a feature branch
3. Run `cargo test` and `cargo clippy` before submitting
4. Ensure `hookwise scan --staged` passes (no secrets in committed files)
5. Open a pull request

## License

MIT