You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(overlay): full command display, smart regex proposal, tmux notification passthrough
- overlay-dialog: wrap long commands at terminal width instead of truncating with "..."
- propose-rule: when compound command has uncovered segments (via new
PASSTHRU_OVERLAY_UNALLOWED_SEGMENTS env var), target the first uncovered
segment's first word instead of the full command's first word
- pre-tool-use: compute uncovered segments via new compute_unallowed_segments
helper in common.sh (filters out readonly auto-allowed + allow/ask matched)
- notification: write OSC 777 to /dev/tty instead of stdout (stdout is the
hook's JSON response); wrap in DCS tmux; ... ST passthrough when inside tmux
so Ghostty can receive the notification through the tmux popup
* fix(compound): readonly segments count as covered in compound allow matching
`go test ./... | tail -50` with user rule `^go` used to fall through to
overlay because `tail -50` had no allow rule. But tail IS readonly.
Fix: before calling match_all_segments on compound Bash commands,
pre-filter out segments that are readonly-auto-allowed (match readonly
regex + paths inside cwd/allowed_dirs). The filter is gated on
has_redirect=false so `cat file > /tmp/out && ls` still falls through
when has_redirect would have blocked the readonly step.
Also adds regression tests:
- readonly segment covers for compound allow
- filter respects has_redirect guard
- 2>&1 fd duplication does not trigger has_redirect
* fix(overlay): cross-session lock at user-scope with stale-lock recovery
The overlay queue lock was scoped to $TMPDIR, which on macOS is often a
per-process folder under /var/folders/... - not shared across CC sessions.
Two simultaneous CC sessions could each open their own tmux popup,
breaking the serialization.
Also: when a hook was SIGKILLed (OOM, timeout) the lock directory
persisted forever, blocking every subsequent overlay until manually
cleared.
Fixes:
- Lock lives at $(passthru_user_home)/passthru-overlay.lock.d, guaranteed
shared across CC sessions of the same user.
- New _OVERLAY_LOCK_STALE_AFTER threshold (default 180s, > overlay
timeout + margin): if the existing lock's mtime is older than that,
clear it and retry. Checked every ~2s during wait.
- Default lock timeout raised from 90s to 180s to match typical user
response time across multiple queued sessions.
* 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
* chore(release): v0.7.0
PostToolUseFailure (timeout 10s each, matcher "*"), and
23
23
SessionStart (timeout 5s, no matcher) handlers
24
24
common.sh shared library. Functions:
@@ -118,7 +118,10 @@ Variables the plugin reads at runtime. Most are test-only overrides; a couple (`
118
118
*`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.
119
119
*`PASSTHRU_OVERLAY_TOOL_NAME` - tool name passed into the overlay dialog. Hook propagates the inbound `tool_name` field verbatim.
120
120
*`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.
122
125
*`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.
123
126
124
127
## How tests run
@@ -221,22 +224,25 @@ concurrent project shells.
221
224
222
225
`PostToolUse`, `PostToolUseFailure`, and `SessionStart` are registered with
223
226
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.
226
230
227
-
The 75s figure breaks down as:
231
+
The 300s figure breaks down as:
228
232
229
233
* The overlay dialog (`scripts/overlay-dialog.sh`) enforces its own 60s
230
234
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
232
238
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.
236
242
237
243
The 10s baseline for non-overlay PreToolUse paths (rule match, mode
238
244
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.
240
246
241
247
For post-event handlers, the original 10s baseline continues to hold:
242
248
@@ -256,6 +262,89 @@ Lower the PreToolUse timeout only after also lowering
256
262
`PASSTHRU_OVERLAY_TIMEOUT` (and only after profiling on target hardware).
257
263
Raising it is always safe since the handler fails open on timeout.
258
264
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.
0 commit comments