Skip to content

Commit 27c6af2

Browse files
authored
feat(overlay): project-scope default + Tab toggle for rule scope (#20)
* feat(overlay): project-scope default + Tab toggle for rule scope * chore(release): v0.5.3
1 parent 6b1317a commit 27c6af2

5 files changed

Lines changed: 45 additions & 21 deletions

File tree

.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.5.2",
3+
"version": "0.5.3",
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.5.2",
3+
"version": "0.5.3",
44
"description": "Regex-based permission rules for Claude Code via hooks",
55
"license": "MIT"
66
}

hooks/handlers/pre-tool-use.sh

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ RULE_JSON_LINE=""
635635
if [ -s "$OVERLAY_RESULT" ]; then
636636
VERDICT="$(head -n 1 "$OVERLAY_RESULT" 2>/dev/null || true)"
637637
RULE_JSON_LINE="$(sed -n '2p' "$OVERLAY_RESULT" 2>/dev/null || true)"
638+
RULE_SCOPE_LINE="$(sed -n '3p' "$OVERLAY_RESULT" 2>/dev/null || true)"
638639
fi
639640
# Best-effort cleanup of the result file; harmless if the file is already
640641
# missing.
@@ -654,13 +655,15 @@ case "$VERDICT" in
654655
exit 0
655656
;;
656657
yes_always|no_always)
657-
# Persist the proposed rule via write-rule.sh. Scope is always user:
658-
# overlay-dialog.sh does not expose a scope picker today, and the
659-
# proposer never writes one. If a future overlay UX adds per-rule scope
660-
# selection, add scope extraction here then.
658+
# Persist the proposed rule via write-rule.sh. Scope comes from overlay
659+
# dialog line 3 (project or user, defaults to project).
661660
target_list="allow"
662661
[ "$VERDICT" = "no_always" ] && target_list="deny"
663-
target_scope="user"
662+
target_scope="project"
663+
case "$RULE_SCOPE_LINE" in
664+
user) target_scope="user" ;;
665+
project) target_scope="project" ;;
666+
esac
664667
if [ -n "$RULE_JSON_LINE" ] && jq -e '.' >/dev/null 2>&1 <<<"$RULE_JSON_LINE"; then
665668
_rule_to_write="$RULE_JSON_LINE"
666669

scripts/overlay-dialog.sh

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ write_verdict_once() {
7474

7575
write_verdict_always() {
7676
# $1 = yes_always | no_always
77-
# $2 = proposed rule JSON (one line, compact)
77+
# $2 = rule JSON (one line, compact)
78+
# $3 = scope (project|user, default project)
7879
{
7980
printf '%s\n' "$1"
8081
printf '%s\n' "$2"
82+
printf '%s\n' "${3:-project}"
8183
} > "$RESULT_FILE" 2>/dev/null || true
8284
}
8385

@@ -154,11 +156,11 @@ if [ -n "$TEST_ANSWER" ]; then
154156
;;
155157
yes_always)
156158
proposed="$(propose_rule)"
157-
write_verdict_always "yes_always" "$proposed"
159+
write_verdict_always "yes_always" "$proposed" "project"
158160
;;
159161
no_always)
160162
proposed="$(propose_rule)"
161-
write_verdict_always "no_always" "$proposed"
163+
write_verdict_always "no_always" "$proposed" "project"
162164
;;
163165
cancel)
164166
:
@@ -413,6 +415,9 @@ prop_tool="$(jq -r '.tool // ""' <<<"$proposed" 2>/dev/null)"
413415
prop_match_key="$(jq -r '.match // empty | keys[0] // empty' <<<"$proposed" 2>/dev/null)"
414416
prop_match_val="$(jq -r '.match // empty | to_entries[0].value // empty' <<<"$proposed" 2>/dev/null)"
415417

418+
# Scope defaults to project. Tab toggles between project/user.
419+
rule_scope="project"
420+
416421
render_rule_editor() {
417422
printf '\033[H\033[2J'
418423
_render_header "$_display_cwd"
@@ -432,6 +437,12 @@ render_rule_editor() {
432437
if [ -n "$prop_match_key" ]; then
433438
printf " Match %-6s ${GREEN}%s${RESET}\n" "${prop_match_key}:" "$prop_match_val"
434439
fi
440+
# Scope toggle: highlight the active scope.
441+
if [ "$rule_scope" = "project" ]; then
442+
printf " Scope: ${REVERSE} project ${RESET} \033[2muser\033[0m \033[2m(Tab to toggle)\033[0m\n"
443+
else
444+
printf " Scope: \033[2mproject\033[0m ${REVERSE} user ${RESET} \033[2m(Tab to toggle)\033[0m\n"
445+
fi
435446
printf '\n'
436447
}
437448

@@ -514,6 +525,15 @@ while true; do
514525
prop_tool="$edited_tool"
515526
render_confirm_screen
516527
;;
528+
$'\t')
529+
# Tab toggles scope.
530+
if [ "$rule_scope" = "project" ]; then
531+
rule_scope="user"
532+
else
533+
rule_scope="project"
534+
fi
535+
render_confirm_screen
536+
;;
517537
esc|timeout)
518538
exec bash "$0"
519539
;;
@@ -529,6 +549,6 @@ if [ -n "$prop_match_key" ] && [ -n "$prop_match_val" ]; then
529549
else
530550
final_rule="$(jq -cn --arg t "$prop_tool" '{tool: $t}')"
531551
fi
532-
write_verdict_always "$answer" "$final_rule"
552+
write_verdict_always "$answer" "$final_rule" "$rule_scope"
533553

534554
exit 0

tests/hook_handler.bats

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -847,17 +847,18 @@ STUB
847847
# Write verdict + optional rule JSON, then exit 0. The hook reads
848848
# \$PASSTHRU_OVERLAY_RESULT_FILE afterwards.
849849
local rule_literal=""
850+
local scope_literal=""
850851
if [ -n "$rule_json" ]; then
851-
# Pass the rule JSON through printf as a literal string (escape \$ so
852-
# the stub itself does not try to expand it).
853852
rule_literal="printf '%s\\n' '$rule_json' >> \"\$PASSTHRU_OVERLAY_RESULT_FILE\""
853+
scope_literal="printf '%s\\n' 'project' >> \"\$PASSTHRU_OVERLAY_RESULT_FILE\""
854854
fi
855855
cat > "$stub_overlay" <<STUB
856856
#!/usr/bin/env bash
857857
: "\${PASSTHRU_OVERLAY_RESULT_FILE:?}"
858858
mkdir -p "\$(dirname "\$PASSTHRU_OVERLAY_RESULT_FILE")" 2>/dev/null || true
859859
printf '%s\\n' '${verdict}' > "\$PASSTHRU_OVERLAY_RESULT_FILE"
860860
${rule_literal}
861+
${scope_literal}
861862
# Touch a log file so tests can assert the stub ran.
862863
if [ -n "\${PASSTHRU_OVERLAY_STUB_LOG:-}" ]; then
863864
{
@@ -1183,7 +1184,7 @@ EOF
11831184
[[ "$reason" == *"overlay"* ]]
11841185
}
11851186

1186-
@test "overlay: yes_always verdict writes rule to user/allow AND emits allow" {
1187+
@test "overlay: yes_always verdict writes rule to project/allow AND emits allow" {
11871188
rule_json='{"tool":"Bash","match":{"command":"^gh "},"reason":"overlay yes_always"}'
11881189
setup_overlay_stub "yes_always" 0 "$rule_json"
11891190
export TMUX="mock/0"
@@ -1204,15 +1205,15 @@ EOF
12041205
[ -n "$json_line" ]
12051206
decision="$(jq -r '.hookSpecificOutput.permissionDecision' <<<"$json_line")"
12061207
[ "$decision" = "allow" ]
1207-
# Rule landed in user-scope passthru.json under allow[].
1208-
[ -f "$USER_ROOT/.claude/passthru.json" ]
1209-
match_cmd="$(jq -r '.allow[-1].match.command' "$USER_ROOT/.claude/passthru.json")"
1208+
# Rule landed in project-scope passthru.json under allow[] (default scope).
1209+
[ -f "$PROJ_ROOT/.claude/passthru.json" ]
1210+
match_cmd="$(jq -r '.allow[-1].match.command' "$PROJ_ROOT/.claude/passthru.json")"
12101211
[ "$match_cmd" = "^gh " ]
1211-
tool_val="$(jq -r '.allow[-1].tool' "$USER_ROOT/.claude/passthru.json")"
1212+
tool_val="$(jq -r '.allow[-1].tool' "$PROJ_ROOT/.claude/passthru.json")"
12121213
[ "$tool_val" = "Bash" ]
12131214
}
12141215

1215-
@test "overlay: no_always verdict writes rule to user/deny AND emits deny" {
1216+
@test "overlay: no_always verdict writes rule to project/deny AND emits deny" {
12161217
rule_json='{"tool":"Bash","match":{"command":"^curl "},"reason":"overlay no_always"}'
12171218
setup_overlay_stub "no_always" 0 "$rule_json"
12181219
export TMUX="mock/0"
@@ -1233,8 +1234,8 @@ EOF
12331234
[ -n "$json_line" ]
12341235
decision="$(jq -r '.hookSpecificOutput.permissionDecision' <<<"$json_line")"
12351236
[ "$decision" = "deny" ]
1236-
[ -f "$USER_ROOT/.claude/passthru.json" ]
1237-
match_cmd="$(jq -r '.deny[-1].match.command' "$USER_ROOT/.claude/passthru.json")"
1237+
[ -f "$PROJ_ROOT/.claude/passthru.json" ]
1238+
match_cmd="$(jq -r '.deny[-1].match.command' "$PROJ_ROOT/.claude/passthru.json")"
12381239
[ "$match_cmd" = "^curl " ]
12391240
}
12401241

0 commit comments

Comments
 (0)