Skip to content

Commit 2bc23df

Browse files
committed
feat(overlay): WebSearch auto-allow + overlay queue lock for concurrent prompts
1 parent 5fe323a commit 2bc23df

2 files changed

Lines changed: 44 additions & 6 deletions

File tree

hooks/handlers/pre-tool-use.sh

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ fi
345345
# cannot be affected by broken rule files. ToolSearch and other CC-internal
346346
# tools stay in the step 7 passthrough list (they use {"continue": true}).
347347
case "$TOOL_NAME" in
348-
Agent|Skill|Glob)
348+
Agent|Skill|Glob|WebSearch)
349349
emit_decision "allow" "passthru internal: ${TOOL_NAME}"
350350
audit_write_line "allow" "$TOOL_NAME" "passthru internal: ${TOOL_NAME}" "" "" "$TOOL_USE_ID" "passthru-internal"
351351
exit 0
@@ -751,6 +751,40 @@ export PASSTHRU_OVERLAY_TOOL_NAME="$TOOL_NAME"
751751
export PASSTHRU_OVERLAY_TOOL_INPUT_JSON="$TOOL_INPUT"
752752
export PASSTHRU_OVERLAY_CWD="$CC_CWD"
753753

754+
# --- Overlay queue lock -------------------------------------------------------
755+
# CC can fire multiple PreToolUse hooks concurrently (parallel tool calls).
756+
# Only one overlay popup can be visible at a time in a given multiplexer.
757+
# Without serialization, the second+ hook falls through to CC's native dialog.
758+
# We use a mkdir-based lock to queue concurrent overlay invocations.
759+
_OVERLAY_LOCK="${_tmpdir}/passthru-overlay.lock.d"
760+
_OVERLAY_LOCK_TIMEOUT="${PASSTHRU_OVERLAY_LOCK_TIMEOUT:-90}"
761+
_overlay_lock_acquired=0
762+
763+
_release_overlay_lock() {
764+
if [ "$_overlay_lock_acquired" -eq 1 ]; then
765+
rm -rf "$_OVERLAY_LOCK" 2>/dev/null || true
766+
_overlay_lock_acquired=0
767+
fi
768+
}
769+
770+
# Acquire the lock. Poll at 200ms intervals up to the timeout.
771+
_lock_start="$(date +%s)"
772+
while true; do
773+
if mkdir "$_OVERLAY_LOCK" 2>/dev/null; then
774+
_overlay_lock_acquired=1
775+
# Ensure lock is released even on unexpected exits (ERR trap, signals).
776+
trap '_release_overlay_lock; printf "[passthru] unexpected error in pre-tool-use.sh\n" >&2; emit_passthrough; exit 0' ERR
777+
trap '_release_overlay_lock' EXIT
778+
break
779+
fi
780+
_now="$(date +%s)"
781+
if [ $((_now - _lock_start)) -ge "$_OVERLAY_LOCK_TIMEOUT" ]; then
782+
printf '[passthru] overlay lock timeout after %ds; falling back to native dialog\n' "$_OVERLAY_LOCK_TIMEOUT" >&2
783+
emit_ask_fallback "overlay lock timeout"
784+
fi
785+
sleep 0.2
786+
done
787+
754788
# Send a desktop notification so the user knows a permission prompt is waiting.
755789
# OSC 777 is supported by Ghostty, iTerm2, and other modern terminals.
756790
printf '\033]777;notify;passthru;permission prompt: %s\a' "$TOOL_NAME" 2>/dev/null || true
@@ -764,7 +798,10 @@ set +e
764798
bash "$OVERLAY_SH"
765799
OVERLAY_RC=$?
766800
set -e
767-
trap 'printf "[passthru] unexpected error in pre-tool-use.sh\n" >&2; emit_passthrough; exit 0' ERR
801+
trap '_release_overlay_lock; printf "[passthru] unexpected error in pre-tool-use.sh\n" >&2; emit_passthrough; exit 0' ERR
802+
803+
# Release the lock so the next queued overlay can proceed.
804+
_release_overlay_lock
768805

769806
if [ "$OVERLAY_RC" -ne 0 ]; then
770807
# Launch failure (rc 1 = no multiplexer detected at launch time, rc 2 =

tests/hook_handler.bats

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,16 +1083,17 @@ run_handler_in_stub_root() {
10831083
[ "$decision" = "deny" ]
10841084
}
10851085

1086-
@test "mode: default + WebSearch -> overlay path entered" {
1087-
setup_overlay_stub "no_once"
1086+
@test "mode: default + WebSearch -> explicit allow (internal tool)" {
10881087
ti='{"query":"what is claude code"}'
10891088
payload="$(make_mode_payload 'WebSearch' "$ti" 'default' "$PROJ_ROOT")"
1090-
run_handler_in_stub_root "$payload"
1089+
run_handler "$payload"
10911090
[ "$status" -eq 0 ]
10921091
json_line="$(printf '%s\n' "$output" | grep -o '{"hookSpecificOutput".*}' | head -n1)"
10931092
[ -n "$json_line" ]
10941093
decision="$(jq -r '.hookSpecificOutput.permissionDecision' <<<"$json_line")"
1095-
[ "$decision" = "deny" ]
1094+
[ "$decision" = "allow" ]
1095+
reason="$(jq -r '.hookSpecificOutput.permissionDecisionReason' <<<"$json_line")"
1096+
[[ "$reason" == *"passthru internal"* ]]
10961097
}
10971098

10981099
# Path-traversal safety ------------------------------------------------------

0 commit comments

Comments
 (0)