Skip to content

Commit 1c6693e

Browse files
committed
fix(overlay): restore mode auto-allow, write tools fall through to native dialog
1 parent 8d43633 commit 1c6693e

2 files changed

Lines changed: 53 additions & 32 deletions

File tree

hooks/handlers/pre-tool-use.sh

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -497,12 +497,38 @@ if [ "$MATCHED" != "ask" ]; then
497497
esac
498498
fi
499499

500-
# --- 8. Overlay path -------------------------------------------------------
501-
# Passthru handles ALL non-internal tool calls. There is no mode-based
502-
# auto-allow shortcut. Every unmatched call goes to the overlay so the user
503-
# always sees a prompt. CC's native dialog only fires as a fallback when the
504-
# user explicitly cancels the overlay (Esc) or the overlay is unavailable.
505-
#
500+
# --- 8. Mode-based auto-allow -----------------------------------------------
501+
# Replicate CC's per-mode auto-allow logic within passthru. Calls that CC
502+
# would silently approve (e.g. Read inside cwd in default mode, Write inside
503+
# cwd in acceptEdits mode) get an explicit allow from passthru so the overlay
504+
# does not fire for routine operations. Passthru emits allow (not continue),
505+
# keeping the decision on our side rather than falling through to CC.
506+
if [ "$MATCHED" != "ask" ]; then
507+
if permission_mode_auto_allows "$PERMISSION_MODE" "$TOOL_NAME" "$TOOL_INPUT" "$CC_CWD" 2>/dev/null; then
508+
MSG="passthru mode-allow: ${PERMISSION_MODE:-default}"
509+
emit_decision "allow" "$MSG"
510+
audit_write_line "allow" "$TOOL_NAME" "mode:${PERMISSION_MODE:-default}" "" "" "$TOOL_USE_ID" "passthru-mode"
511+
exit 0
512+
fi
513+
fi
514+
515+
# --- 9. Write tools -> native dialog (for diff rendering) ------------------
516+
# Write/Edit/NotebookEdit that weren't mode-auto-allowed (step 8) should
517+
# fall through to CC's native dialog which renders diffs. The overlay can't
518+
# show diffs, so forcing Esc for every edit is bad UX. An explicit ask-rule
519+
# match still routes to the overlay (user opted in).
520+
if [ "$MATCHED" != "ask" ]; then
521+
case "$TOOL_NAME" in
522+
Write|Edit|NotebookEdit|MultiEdit)
523+
emit_decision "ask" "passthru: write tool, deferring to native dialog for diff"
524+
audit_write_line "ask" "$TOOL_NAME" "write-tool native fallback" "" "" "$TOOL_USE_ID"
525+
audit_write_breadcrumb "$TOOL_USE_ID" "$TOOL_NAME" "$TOOL_INPUT"
526+
exit 0
527+
;;
528+
esac
529+
fi
530+
531+
# --- 10. Overlay path ------------------------------------------------------
506532
# Reached when either:
507533
# * an ask[] rule matched, or
508534
# * no rule matched AND mode did NOT auto-allow.

tests/hook_handler.bats

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -939,18 +939,18 @@ run_handler_in_stub_root() {
939939
[ "$decision" = "allow" ]
940940
}
941941

942-
@test "mode: acceptEdits + Write OUTSIDE cwd -> overlay path entered" {
943-
# file_path is /tmp/elsewhere (definitely not under PROJ_ROOT). Mode does
944-
# NOT auto-allow, so we fall through to overlay. Stub emits yes_once.
945-
setup_overlay_stub "yes_once"
942+
@test "mode: acceptEdits + Write OUTSIDE cwd -> native dialog (diff rendering)" {
943+
# Write outside cwd is not mode-auto-allowed. Write tools fall through to
944+
# CC's native dialog (permissionDecision: ask) for diff rendering instead
945+
# of the overlay.
946946
ti='{"file_path":"/tmp/elsewhere/foo.ts","content":"x"}'
947947
payload="$(make_mode_payload 'Write' "$ti" 'acceptEdits' "$PROJ_ROOT")"
948-
run_handler_in_stub_root "$payload"
948+
run_handler "$payload"
949949
[ "$status" -eq 0 ]
950950
json_line="$(printf '%s\n' "$output" | grep -o '{"hookSpecificOutput".*}' | head -n1)"
951951
[ -n "$json_line" ]
952952
decision="$(jq -r '.hookSpecificOutput.permissionDecision' <<<"$json_line")"
953-
[ "$decision" = "allow" ]
953+
[ "$decision" = "ask" ]
954954
}
955955

956956
@test "mode: acceptEdits + Read (non-edit tool) -> overlay path entered" {
@@ -1021,17 +1021,17 @@ run_handler_in_stub_root() {
10211021
[ "$decision" = "allow" ]
10221022
}
10231023

1024-
@test "mode: plan + Write -> overlay path entered" {
1025-
# plan mode restricts writes; the overlay (or native fallback) gates them.
1026-
setup_overlay_stub "no_once"
1024+
@test "mode: plan + Write -> native dialog (diff rendering)" {
1025+
# plan mode restricts writes. Write tools fall through to native dialog
1026+
# for diff rendering rather than the overlay.
10271027
ti="$(jq -cn --arg fp "$PROJ_ROOT/src/foo.ts" '{file_path:$fp,content:"x"}')"
10281028
payload="$(make_mode_payload 'Write' "$ti" 'plan' "$PROJ_ROOT")"
1029-
run_handler_in_stub_root "$payload"
1029+
run_handler "$payload"
10301030
[ "$status" -eq 0 ]
10311031
json_line="$(printf '%s\n' "$output" | grep -o '{"hookSpecificOutput".*}' | head -n1)"
10321032
[ -n "$json_line" ]
10331033
decision="$(jq -r '.hookSpecificOutput.permissionDecision' <<<"$json_line")"
1034-
[ "$decision" = "deny" ]
1034+
[ "$decision" = "ask" ]
10351035
}
10361036

10371037
# WebFetch and WebSearch go through the overlay --------------------------------
@@ -1063,21 +1063,17 @@ run_handler_in_stub_root() {
10631063

10641064
# Path-traversal safety ------------------------------------------------------
10651065

1066-
@test "mode: acceptEdits + file_path with ../ traversal is NOT auto-allowed" {
1067-
# $PROJ_ROOT/../outside literally starts with $PROJ_ROOT/ but resolves
1068-
# OUTSIDE cwd. permission_mode_auto_allows must reject these so crafted
1069-
# tool_inputs cannot sneak past the prefix check.
1070-
setup_overlay_stub "no_once"
1066+
@test "mode: acceptEdits + file_path with ../ traversal -> native dialog (not auto-allowed)" {
1067+
# Write with ../ traversal is not mode-auto-allowed. Write tools fall
1068+
# through to native dialog for diff rendering.
10711069
ti="$(jq -cn --arg fp "$PROJ_ROOT/../outside/secret.txt" '{file_path:$fp,content:"x"}')"
10721070
payload="$(make_mode_payload 'Write' "$ti" 'acceptEdits' "$PROJ_ROOT")"
1073-
run_handler_in_stub_root "$payload"
1071+
run_handler "$payload"
10741072
[ "$status" -eq 0 ]
10751073
json_line="$(printf '%s\n' "$output" | grep -o '{"hookSpecificOutput".*}' | head -n1)"
10761074
[ -n "$json_line" ]
1077-
# Decision came from overlay (stub returned no_once). Auto-allow was
1078-
# rejected -> overlay was consulted -> deny.
10791075
decision="$(jq -r '.hookSpecificOutput.permissionDecision' <<<"$json_line")"
1080-
[ "$decision" = "deny" ]
1076+
[ "$decision" = "ask" ]
10811077
}
10821078

10831079
@test "mode: symlink inside cwd -> overlay path entered (no auto-allow shortcut)" {
@@ -1405,20 +1401,19 @@ EOF
14051401
[ "$output" = "overlay" ]
14061402
}
14071403

1408-
@test "audit: bypassPermissions mode logs source=overlay (no mode auto-allow)" {
1409-
# Mode-based auto-allow is removed. bypassPermissions goes through the
1410-
# overlay like every other mode and is logged with source=overlay.
1404+
@test "audit: bypassPermissions mode logs source=passthru-mode (mode auto-allow)" {
1405+
# bypassPermissions auto-allows everything. Passthru emits allow with
1406+
# source=passthru-mode, keeping the decision on our side.
14111407
enable_audit
1412-
setup_overlay_stub "yes_once"
14131408
ti='{"command":"ls"}'
14141409
payload="$(jq -cn --arg t 'Bash' --argjson ti "$ti" --arg m 'bypassPermissions' --arg c "$PROJ_ROOT" \
14151410
'{tool_name:$t,tool_input:$ti,permission_mode:$m,cwd:$c,tool_use_id:"tMODE"}')"
1416-
run_handler_in_stub_root "$payload"
1411+
run_handler "$payload"
14171412
[ "$status" -eq 0 ]
14181413
[ -f "$(audit_log)" ]
14191414
line="$(head -n1 "$(audit_log)")"
14201415
run jq -r '.source' <<<"$line"
1421-
[ "$output" = "overlay" ]
1416+
[ "$output" = "passthru-mode" ]
14221417
run jq -r '.event' <<<"$line"
14231418
[ "$output" = "allow" ]
14241419
}

0 commit comments

Comments
 (0)