Skip to content

Commit 1d2572d

Browse files
committed
docs(claude.md): cross-session overlay lock, native-dialog cascade, hook ordering
- correct stale 75s hook timeout reference (actual is 300s) - new "Overlay queue lock (cross-session)" section: user-home lock path, macOS TMPDIR caveat, stale-lock recovery - new "Interaction with CC's native permission system" section: decision cascade that explains why "ask" triggers native dialog, and how multi-plugin hook ordering causes passthru to see either original or wrapped (rtk) commands - new "Notifications on overlay prompt" section: /dev/tty requirement and tmux DCS passthrough wrapping - document PASSTHRU_OVERLAY_LOCK_TIMEOUT, PASSTHRU_OVERLAY_LOCK_STALE_AFTER, and PASSTHRU_OVERLAY_UNALLOWED_SEGMENTS env vars
1 parent 1cd98f9 commit 1d2572d

1 file changed

Lines changed: 99 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ commands/
1818
log.md /passthru:log slash command (prompt-based)
1919
overlay.md /passthru:overlay slash command (wraps scripts/overlay-config.sh)
2020
hooks/
21-
hooks.json registers PreToolUse (timeout 75s, matcher "*"), PostToolUse +
21+
hooks.json registers PreToolUse (timeout 300s, matcher "*"), PostToolUse +
2222
PostToolUseFailure (timeout 10s each, matcher "*"), and
2323
SessionStart (timeout 5s, no matcher) handlers
2424
common.sh shared library. Functions:
@@ -118,7 +118,10 @@ Variables the plugin reads at runtime. Most are test-only overrides; a couple (`
118118
* `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.
119119
* `PASSTHRU_OVERLAY_TOOL_NAME` - tool name passed into the overlay dialog. Hook propagates the inbound `tool_name` field verbatim.
120120
* `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.
121-
* `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).
121+
* `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 300s).
122+
* `PASSTHRU_OVERLAY_LOCK_TIMEOUT` - seconds to wait for another CC session's overlay to release the user-scope queue lock. Default 180. On timeout, the hook emits the ask fallback (native dialog). See the "Overlay queue lock" section below.
123+
* `PASSTHRU_OVERLAY_LOCK_STALE_AFTER` - mtime age threshold in seconds after which an existing overlay lock is considered abandoned and auto-cleared. Default 180. Protects against SIGKILLed hooks leaving zombie locks.
124+
* `PASSTHRU_OVERLAY_UNALLOWED_SEGMENTS` - newline-separated list of compound Bash segments that are NOT covered by readonly auto-allow or by any allow/ask rule. Set by `pre-tool-use.sh` before invoking the overlay. Read by `overlay-propose-rule.sh` so that "yes/no always" proposals target only the uncovered portion instead of the full command's first word.
122125
* `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.
123126

124127
## How tests run
@@ -221,22 +224,25 @@ concurrent project shells.
221224

222225
`PostToolUse`, `PostToolUseFailure`, and `SessionStart` are registered with
223226
short timeouts (10s / 10s / 5s) in `hooks/hooks.json`. `PreToolUse` runs with
224-
a **75s** timeout because Task 8 (v0.5.0) wired the hook to block
225-
synchronously on the interactive terminal-overlay dialog.
227+
a **300s** timeout because the hook blocks synchronously on the interactive
228+
terminal-overlay dialog AND may also queue behind an overlay held by another
229+
CC session on the same machine.
226230

227-
The 75s figure breaks down as:
231+
The 300s figure breaks down as:
228232

229233
* The overlay dialog (`scripts/overlay-dialog.sh`) enforces its own 60s
230234
budget (`PASSTHRU_OVERLAY_TIMEOUT`, default 60s).
231-
* Add 15s of margin for overlay launch, multiplexer roundtrip, post-dialog
235+
* The overlay queue lock (`PASSTHRU_OVERLAY_LOCK_TIMEOUT`, default 180s)
236+
waits for other sessions' overlays to complete.
237+
* Add margin for overlay launch, multiplexer roundtrip, post-dialog
232238
rule write via `write-rule.sh`, and audit line emission.
233-
* CC's hook timeout is wall-clock (confirmed via `time sleep 1`: 1.008s
234-
real). Anything below the overlay's own budget would kill the hook
235-
mid-dialog and lose the user's verdict.
239+
* CC's hook timeout is wall-clock. Anything below the overlay's own budget
240+
plus the lock-wait budget would kill the hook mid-wait and lose the
241+
user's verdict.
236242

237243
The 10s baseline for non-overlay PreToolUse paths (rule match, mode
238244
auto-allow) still applies in the sense that none of them block on IO; the
239-
75s cap only matters when the overlay is actually invoked.
245+
300s cap only matters when the overlay is actually invoked.
240246

241247
For post-event handlers, the original 10s baseline continues to hold:
242248

@@ -256,6 +262,89 @@ Lower the PreToolUse timeout only after also lowering
256262
`PASSTHRU_OVERLAY_TIMEOUT` (and only after profiling on target hardware).
257263
Raising it is always safe since the handler fails open on timeout.
258264

265+
## Overlay queue lock (cross-session)
266+
267+
The overlay popup is singleton per machine: tmux/kitty/wezterm can only
268+
show one popup at a time. Two CC sessions racing for the overlay would
269+
otherwise both try to open popups and one would fail, falling through to
270+
CC's native dialog.
271+
272+
`hooks/handlers/pre-tool-use.sh` serializes overlays via a mkdir lock at
273+
`$(passthru_user_home)/passthru-overlay.lock.d`. The lock MUST live under
274+
user home, NOT `$TMPDIR`: on macOS `$TMPDIR` resolves to a per-process
275+
`/var/folders/<session-id>/.../T/` folder that is NOT shared across CC
276+
sessions of the same user. User home is the only guaranteed shared
277+
location.
278+
279+
Stale-lock recovery runs every ~2s during wait. If the existing lock's
280+
mtime is older than `PASSTHRU_OVERLAY_LOCK_STALE_AFTER` (default 180s),
281+
the lock is force-removed. This prevents a hook that was SIGKILLed
282+
(OOM, manual kill) from blocking every subsequent overlay forever.
283+
284+
Env knobs:
285+
286+
* `PASSTHRU_OVERLAY_LOCK_TIMEOUT` (default 180s) - how long to wait for
287+
another session's overlay before falling back to CC's native dialog.
288+
* `PASSTHRU_OVERLAY_LOCK_STALE_AFTER` (default 180s) - mtime age at which
289+
an existing lock is considered abandoned and auto-cleared.
290+
291+
## Interaction with CC's native permission system
292+
293+
Passthru is one of potentially several PreToolUse hooks AND sits alongside
294+
CC's built-in permission evaluation. Understanding which decision wins in
295+
which scenario is essential for debugging "why did the native dialog
296+
appear?" complaints.
297+
298+
**Decision cascade after PreToolUse hooks return:**
299+
300+
1. If any hook emits `permissionDecision: "allow"` - CC proceeds silently.
301+
2. If any hook emits `permissionDecision: "deny"` - CC blocks the tool.
302+
3. If a hook emits `permissionDecision: "ask"` - CC shows its NATIVE
303+
dialog. This is by design: "ask" explicitly defers to CC's UI.
304+
4. If all hooks pass through (`{"continue": true}`) - CC evaluates its own
305+
`permissions.allow` entries from `settings.json`. If none match, CC
306+
shows its native dialog.
307+
308+
Implication: passthru emitting `ask` (either explicitly or via overlay
309+
fall-through / lock timeout) will trigger a native dialog. Only `allow`
310+
fully suppresses it. This is why the compound readonly-filter fix (`go
311+
test | tail` now resolves to allow instead of ask) eliminates the native
312+
dialog cascade.
313+
314+
**Multi-plugin hook ordering:**
315+
316+
CC runs PreToolUse hooks in plugin registration order. Each subsequent
317+
hook sees `tool_input` as MODIFIED by previous hooks. Plugins like `rtk`
318+
(which rewrites `go test` to `rtk go test`) can either run before or
319+
after passthru depending on ordering:
320+
321+
* rtk BEFORE passthru: passthru sees `rtk go test ...`. User rule for
322+
`^go` does not match. Falls through to overlay.
323+
* rtk AFTER passthru: passthru sees `go test ...`. User rule matches,
324+
decision is "allow". CC then runs rtk which rewrites the command, CC
325+
executes the rewritten command.
326+
327+
If the user reports seeing the overlay for BOTH `go ...` and `rtk go ...`
328+
variants intermittently, hook ordering is non-deterministic or multiple
329+
rtk code paths (proxy vs rewrite) are in play. Rule coverage should
330+
anticipate both forms or use a broader pattern.
331+
332+
## Notifications on overlay prompt
333+
334+
`pre-tool-use.sh` sends an OSC 777 desktop notification before invoking
335+
the overlay so the user knows a prompt is waiting. Two gotchas:
336+
337+
* Must write to `/dev/tty`, NOT stdout. Stdout is captured by CC as the
338+
hook's JSON response and the OSC sequence would pollute (or invalidate)
339+
the JSON payload.
340+
* Inside tmux, the OSC must be wrapped in DCS passthrough: `ESC P tmux;
341+
<inner> ESC \` with every inner `ESC` doubled. Additionally tmux needs
342+
`set -g allow-passthrough on` in the user's tmux.conf. Without
343+
passthrough, tmux strips the OSC and Ghostty/iTerm2 never sees it.
344+
345+
OSC 777 format: `ESC ] 777 ; notify ; <title> ; <body> BEL`. Supported
346+
by Ghostty, iTerm2, Konsole, and most modern terminal emulators.
347+
259348
## Compound command splitting
260349

261350
For Bash tool calls, the hook splits compound commands into segments before

0 commit comments

Comments
 (0)