Skip to content

Commit 441525b

Browse files
authored
feat: terminal overlay for permission prompts + ask rule support (#15)
* feat(schema): add v2 with ask[] array support * feat(add): support ask rules via --ask flag and write-rule.sh ask list * feat(list/remove/suggest): ask rule support across CLI commands * feat(hook): ask decision path with document-order match precedence * feat(overlay): terminal multiplexer dialog skeleton for permission prompts * feat(hook): overlay invocation with mode-based auto-allow short-circuit * feat(overlay): /passthru:overlay command to toggle overlay on/off * docs: document overlay + ask rules, bump v0.5.0 * fix(test): use sanitized PATH instead of broken symlinks for CI multiplexer masking
1 parent b668992 commit 441525b

38 files changed

Lines changed: 5199 additions & 246 deletions

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "passthru",
3-
"version": "0.4.3",
3+
"version": "0.5.0",
44
"description": "Regex-based permission rules for Claude Code via hooks",
55
"owner": {
66
"name": "nnemirovsky"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "passthru",
3-
"version": "0.4.3",
3+
"version": "0.5.0",
44
"description": "Regex-based permission rules for Claude Code via hooks",
55
"license": "MIT"
66
}

CLAUDE.md

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ commands/
1616
remove.md /passthru:remove slash command (wraps scripts/remove-rule.sh)
1717
verify.md /passthru:verify slash command (prompt-based)
1818
log.md /passthru:log slash command (prompt-based)
19+
overlay.md /passthru:overlay slash command (wraps scripts/overlay-config.sh)
1920
hooks/
20-
hooks.json registers PreToolUse + PostToolUse + PostToolUseFailure
21-
(timeout 10s each, matcher "*") and SessionStart
22-
(timeout 5s, no matcher) handlers
21+
hooks.json registers PreToolUse (timeout 75s, matcher "*"), PostToolUse +
22+
PostToolUseFailure (timeout 10s each, matcher "*"), and
23+
SessionStart (timeout 5s, no matcher) handlers
2324
common.sh shared library. Functions:
2425
* load_rules / validate_rules (merge + schema-check)
2526
* pcre_match / match_rule / find_first_match (rule matching)
@@ -51,14 +52,39 @@ scripts/
5152
bootstrap.sh one-time importer from native permissions.allow into passthru.imported.json.
5253
Supported shapes: Bash(prefix:*) | Bash(exact) | mcp__* | WebFetch(domain:X)
5354
| WebSearch | Read/Edit/Write(path[/**]) | Skill(name). Others -> [WARN] skip.
54-
write-rule.sh atomic write wrapper: backup + append + verify + rollback
55+
Stamps every written rule with `_source_hash` (sha256 of the normalized
56+
source entry) so session-start.sh can diff imported vs importable.
57+
write-rule.sh atomic write wrapper: backup + append + verify + rollback. Also the
58+
v1 -> v2 upgrade point: first ask write on a v1 file flips the version.
5559
remove-rule.sh atomic remove wrapper: backup + splice + verify + rollback. Authored-only.
56-
list.sh rule list viewer CLI with scope/list/source/index annotations
57-
verify.sh rule verifier CLI (also invoked by write-rule.sh/remove-rule.sh and /passthru:verify)
58-
log.sh audit-log viewer CLI + sentinel toggle
60+
list.sh rule list viewer CLI with scope/list/source/index annotations. Renders
61+
ALLOW / ASK / DENY groups; --list ask filters.
62+
verify.sh rule verifier CLI (also invoked by write-rule.sh/remove-rule.sh and /passthru:verify).
63+
Accepts schema v1 and v2. Rejects ask+allow and ask+deny conflicts.
64+
log.sh audit-log viewer CLI + sentinel toggle. color_for_event covers
65+
ask (cyan), errored (yellow), and overlay-sourced events.
66+
overlay.sh terminal-multiplexer dispatcher. Detects tmux / kitty / wezterm via
67+
env vars + PATH check, launches overlay-dialog.sh inside the popup.
68+
Writes verdict to $PASSTHRU_OVERLAY_RESULT_FILE.
69+
overlay-dialog.sh pure-bash TUI. Y/A/N/D/Esc keypress menu, optional rule editor on A/D.
70+
Respects PASSTHRU_OVERLAY_TEST_ANSWER for hermetic tests.
71+
PASSTHRU_OVERLAY_TIMEOUT bounds the wait (default 60s).
72+
overlay-propose-rule.sh
73+
regex proposer. Takes tool_name + tool_input JSON, emits a rule JSON
74+
targeting one of four categories (Bash prefix, Read/Edit/Write path,
75+
WebFetch URL host, MCP namespace). Unknown tool -> bare ^<Name>$ rule.
76+
overlay-config.sh overlay sentinel toggle + multiplexer detection reporter. Backs
77+
/passthru:overlay.
5978
tests/
60-
fixtures/ JSON fixture files used by bats tests
61-
*.bats test suites (one per script or component)
79+
fixtures/
80+
overlay/ stub tmux/kitty/wezterm shell scripts used by overlay tests.
81+
*.json JSON fixture files.
82+
overlay.bats overlay.sh + overlay-dialog.sh + overlay-propose-rule.sh coverage.
83+
overlay_config.bats overlay-config.sh + /passthru:overlay frontmatter coverage.
84+
post_tool_use_failure_hook.bats
85+
PostToolUseFailure handler coverage (permission errors, generic
86+
errored events, timeouts, interrupts, missing breadcrumb).
87+
*.bats test suites (one per script or component).
6288
docs/
6389
rule-format.md schema reference
6490
examples.md real-world rule examples
@@ -70,6 +96,19 @@ CLAUDE.md this file
7096

7197
Paths honor `PASSTHRU_USER_HOME` and `PASSTHRU_PROJECT_DIR` so tests never touch the real `~/.claude`.
7298

99+
## Environment variables
100+
101+
Variables the plugin reads at runtime. Most are test-only overrides; a couple (`PASSTHRU_OVERLAY_TIMEOUT`, `PASSTHRU_WRITE_LOCK_TIMEOUT`) have user-facing meaning.
102+
103+
* `PASSTHRU_USER_HOME` - override `~/.claude` as the user scope root. Used by every bats test to redirect reads and writes to a temp dir. Never set in production.
104+
* `PASSTHRU_PROJECT_DIR` - override `$PWD/.claude` as the project scope root. Same use case as above. Tests set both.
105+
* `PASSTHRU_OVERLAY_RESULT_FILE` - path the overlay dispatcher writes the verdict line(s) into. Set by `pre-tool-use.sh` per-invocation via `sanitize_tool_use_id` + `passthru_tmpdir`. The overlay script reads the path from this env var; the hook reads back the contents after the overlay exits.
106+
* `PASSTHRU_OVERLAY_TEST_ANSWER` - short-circuit the interactive keypress loop in `overlay-dialog.sh`. Accepts `yes_once|yes_always|no_once|no_always|cancel`. Used exclusively by `tests/overlay.bats` + `tests/hook_handler.bats` to exercise every branch without pseudo-tty gymnastics. Never set by the hook in production.
107+
* `PASSTHRU_OVERLAY_TOOL_NAME` - tool name passed into the overlay dialog. Hook propagates the inbound `tool_name` field verbatim.
108+
* `PASSTHRU_OVERLAY_TOOL_INPUT_JSON` - tool input JSON (stringified) passed into the overlay dialog. Hook propagates the inbound `tool_input` field verbatim. The dialog and `overlay-propose-rule.sh` parse it for the suggested-rule screen.
109+
* `PASSTHRU_OVERLAY_TIMEOUT` - seconds to wait for a user response inside the overlay. Default 60. If the user does not respond in time, the overlay exits without writing a verdict and the hook treats the prompt as cancelled (falls through to the native dialog). Setting below 60 is fine; setting above requires also raising the PreToolUse hook timeout (currently 75s).
110+
* `PASSTHRU_WRITE_LOCK_TIMEOUT` - seconds `scripts/write-rule.sh` and `scripts/remove-rule.sh` wait for the user-scope mkdir lock. Default 5. See the "Write-wrapper locking" section below.
111+
73112
## How tests run
74113

75114
All shell logic is covered by bats-core. Run the full suite:
@@ -141,11 +180,11 @@ Exit codes:
141180
Checks performed (in order, across the merged set):
142181

143182
1. **parse** - every existing file is valid JSON.
144-
2. **schema** - every rule has at least one of `tool` or `match`, types match spec, version is `1`.
183+
2. **schema** - every rule has at least one of `tool` or `match`, types match spec, version is `1` or `2`. v2 files may declare `ask[]`; rules in `ask[]` are validated with the same rule-shape checks as `allow[]` and `deny[]`.
145184
3. **regex** - every `tool` regex and every `match.*` regex compiles in perl.
146185
4. **duplicates** - exact-duplicate rules (same tool + match) across scopes emit a warning.
147-
5. **conflict** - identical `tool + match` appears in both `allow[]` and `deny[]` (merged) emits an error.
148-
6. **shadowing** - within one merged `allow[]` or `deny[]` array, a later rule duplicates an earlier one. Warning.
186+
5. **conflict** - identical `tool + match` appears in two or more of `allow[]`, `deny[]`, `ask[]` (merged) emits an error.
187+
6. **shadowing** - within one merged `allow[]`, `deny[]`, or `ask[]` array, a later rule duplicates an earlier one. Warning.
149188

150189
## Write-wrapper locking
151190

@@ -168,8 +207,26 @@ concurrent project shells.
168207

169208
## Hook timeout
170209

171-
Both `PreToolUse` and `PostToolUse` are registered with `"timeout": 10` in
172-
`hooks/hooks.json`. The reason for 10 seconds (rather than 2-3):
210+
`PostToolUse`, `PostToolUseFailure`, and `SessionStart` are registered with
211+
short timeouts (10s / 10s / 5s) in `hooks/hooks.json`. `PreToolUse` runs with
212+
a **75s** timeout because Task 8 (v0.5.0) wired the hook to block
213+
synchronously on the interactive terminal-overlay dialog.
214+
215+
The 75s figure breaks down as:
216+
217+
* The overlay dialog (`scripts/overlay-dialog.sh`) enforces its own 60s
218+
budget (`PASSTHRU_OVERLAY_TIMEOUT`, default 60s).
219+
* Add 15s of margin for overlay launch, multiplexer roundtrip, post-dialog
220+
rule write via `write-rule.sh`, and audit line emission.
221+
* CC's hook timeout is wall-clock (confirmed via `time sleep 1`: 1.008s
222+
real). Anything below the overlay's own budget would kill the hook
223+
mid-dialog and lose the user's verdict.
224+
225+
The 10s baseline for non-overlay PreToolUse paths (rule match, mode
226+
auto-allow) still applies in the sense that none of them block on IO; the
227+
75s cap only matters when the overlay is actually invoked.
228+
229+
For post-event handlers, the original 10s baseline continues to hold:
173230

174231
* `load_rules` shells out to `jq` once per rule file (up to 4 files), once for
175232
the parse check, once for normalization, and once for the merge.
@@ -183,8 +240,9 @@ Both `PreToolUse` and `PostToolUse` are registered with `"timeout": 10` in
183240
audit fidelity, never block a tool call. Choosing 10s leaves 5x headroom
184241
over typical worst case.
185242

186-
Lower the timeout only after profiling on the target hardware. Higher is
187-
fine.
243+
Lower the PreToolUse timeout only after also lowering
244+
`PASSTHRU_OVERLAY_TIMEOUT` (and only after profiling on target hardware).
245+
Raising it is always safe since the handler fails open on timeout.
188246

189247
## Releases
190248

@@ -230,3 +288,6 @@ The release flow in one-line form:
230288
* Adding a new hook event: register it in `hooks/hooks.json` and add a handler under `hooks/handlers/`. Reuse `hooks/common.sh` helpers where possible.
231289
* Adding a new verifier check: see `CONTRIBUTING.md` section "Adding a new verifier check".
232290
* Adding a new rule type or schema field: see `CONTRIBUTING.md` section "Rule schema evolution".
291+
* Changing the overlay UI or keyboard flow: `scripts/overlay-dialog.sh` is the TUI, `scripts/overlay.sh` is the multiplexer dispatcher, and `scripts/overlay-propose-rule.sh` proposes the regex on A/D. Test via `PASSTHRU_OVERLAY_TEST_ANSWER`; see `tests/overlay.bats` for the stub-tmux pattern.
292+
* Changing ask-rule semantics: the merged document-order logic sits in `hooks/common.sh` (`find_first_match`) and `hooks/handlers/pre-tool-use.sh`. Ask rule parsing + validation is in `validate_rules` + `load_rules`. The verifier's conflict and shadowing checks in `scripts/verify.sh` must also cover `ask[]`.
293+
* Adding a new overlay multiplexer backend: add detection + launch lines in `scripts/overlay.sh` (search for the tmux / kitty / wezterm branches) and a stub fixture in `tests/fixtures/overlay/`. The shared detector helper lives in `hooks/common.sh` (`detect_overlay_multiplexer`).

0 commit comments

Comments
 (0)