Skip to content

Commit 8d43633

Browse files
committed
feat(overlay): two-field rule editor, per-tool input display, Skill auto-allow
1 parent 33282df commit 8d43633

5 files changed

Lines changed: 191 additions & 87 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.1",
3+
"version": "0.5.2",
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.1",
3+
"version": "0.5.2",
44
"description": "Regex-based permission rules for Claude Code via hooks",
55
"license": "MIT"
66
}

hooks/handlers/pre-tool-use.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ fi
487487
# that should never trigger the overlay. Pass them through unconditionally.
488488
if [ "$MATCHED" != "ask" ]; then
489489
case "$TOOL_NAME" in
490-
ToolSearch|TaskCreate|TaskUpdate|TaskGet|TaskList|TaskOutput|TaskStop|\
490+
ToolSearch|Skill|TaskCreate|TaskUpdate|TaskGet|TaskList|TaskOutput|TaskStop|\
491491
AskUserQuestion|SendMessage|EnterPlanMode|ExitPlanMode|ScheduleWakeup|\
492492
CronCreate|CronDelete|CronList|Monitor|LSP|RemoteTrigger|\
493493
EnterWorktree|ExitWorktree|TeamCreate|TeamDelete)

scripts/overlay-dialog.sh

Lines changed: 168 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -178,48 +178,115 @@ MENU_KEYS=(y a n d esc)
178178
MENU_COUNT=${#MENU_LABELS[@]}
179179
selected=0
180180

181-
# Build a human-readable preview from tool_input. Extract the most relevant
182-
# field per tool type instead of showing raw JSON.
183-
preview=""
181+
# Build a human-readable preview from tool_input. Each tool type gets a
182+
# tailored display. MCP tools show pretty JSON. Edits show a diff preview.
183+
_extract() { jq -r --arg f "$1" '.[$f] // empty' <<<"$TOOL_INPUT_JSON" 2>/dev/null; }
184+
_truncate() {
185+
local s="$1" max="${2:-120}"
186+
if [ "${#s}" -gt "$max" ]; then
187+
printf '%s...' "${s:0:$((max - 3))}"
188+
else
189+
printf '%s' "$s"
190+
fi
191+
}
192+
193+
# preview_lines: array of lines to display. Populated per tool type.
194+
preview_lines=()
195+
extra_height=0 # additional lines beyond standard 1-line preview
196+
184197
if [ -n "$TOOL_INPUT_JSON" ]; then
185-
_extract() { jq -r --arg f "$1" '.[$f] // empty' <<<"$TOOL_INPUT_JSON" 2>/dev/null; }
186198
case "$TOOL_NAME" in
187199
Bash)
188-
preview="$(_extract command)" ;;
189-
WebFetch|WebSearch)
190-
preview="$(_extract url)"
191-
[ -z "$preview" ] && preview="$(_extract query)" ;;
192-
Read|Edit|Write|NotebookEdit|NotebookRead)
193-
preview="$(_extract file_path)" ;;
200+
preview_lines+=("$(_truncate "$(_extract command)" 120)")
201+
;;
202+
WebFetch)
203+
preview_lines+=("$(_extract url)")
204+
;;
205+
WebSearch)
206+
_q="$(_extract query)"
207+
[ -n "$_q" ] && preview_lines+=("search: $_q") || preview_lines+=("$(_extract url)")
208+
;;
209+
Edit|Write)
210+
preview_lines+=("$(_extract file_path)")
211+
;;
212+
Read|NotebookRead)
213+
preview_lines+=("$(_extract file_path)")
214+
;;
215+
NotebookEdit)
216+
_fp="$(_extract file_path)"
217+
_cell="$(_extract cell_id)"
218+
preview_lines+=("$_fp")
219+
[ -n "$_cell" ] && preview_lines+=("cell: $_cell") && extra_height=1
220+
;;
194221
Grep)
195-
preview="$(_extract pattern)"
222+
_pat="$(_extract pattern)"
196223
_path="$(_extract path)"
197-
[ -n "$_path" ] && preview="${preview} (in ${_path})" ;;
224+
preview_lines+=("/$_pat/")
225+
[ -n "$_path" ] && preview_lines+=("in: $_path") && extra_height=1
226+
;;
198227
Glob)
199-
preview="$(_extract pattern)"
228+
_pat="$(_extract pattern)"
200229
_path="$(_extract path)"
201-
[ -n "$_path" ] && preview="${preview} (in ${_path})" ;;
230+
preview_lines+=("$_pat")
231+
[ -n "$_path" ] && preview_lines+=("in: $_path") && extra_height=1
232+
;;
233+
Skill)
234+
_skill="$(_extract skill)"
235+
_args="$(_extract args)"
236+
if [ -n "$_args" ]; then
237+
preview_lines+=("$_skill $_args")
238+
else
239+
preview_lines+=("$_skill")
240+
fi
241+
;;
202242
Agent)
203-
preview="$(_extract description)"
204-
[ -z "$preview" ] && preview="$(_extract prompt | head -c 120)" ;;
243+
_desc="$(_extract description)"
244+
[ -n "$_desc" ] && preview_lines+=("$_desc") || preview_lines+=("$(_truncate "$(_extract prompt)" 120)")
245+
;;
246+
mcp__*)
247+
# MCP tools: pretty-print the JSON args with indentation.
248+
_pretty="$(jq -r '.' <<<"$TOOL_INPUT_JSON" 2>/dev/null || echo "$TOOL_INPUT_JSON")"
249+
_line_count=0
250+
_total_lines=0
251+
while IFS= read -r _line; do
252+
_total_lines=$((_total_lines + 1))
253+
if [ "$_line_count" -lt 10 ]; then
254+
preview_lines+=("$_line")
255+
_line_count=$((_line_count + 1))
256+
fi
257+
done <<<"$_pretty"
258+
if [ "$_total_lines" -gt 10 ]; then
259+
_remaining=$((_total_lines - 10))
260+
preview_lines+=(" ... (${_remaining} more lines)")
261+
_line_count=$((_line_count + 1))
262+
fi
263+
extra_height=$((_line_count - 1))
264+
;;
205265
*)
206-
preview="$TOOL_INPUT_JSON" ;;
266+
preview_lines+=("$(_truncate "$TOOL_INPUT_JSON" 120)")
267+
;;
207268
esac
208-
# Fallback to raw JSON if extraction yielded nothing.
209-
[ -z "$preview" ] && preview="$TOOL_INPUT_JSON"
210-
max_preview=120
211-
truncated_len=$((max_preview - 3))
212-
if [ "${#preview}" -gt "$max_preview" ]; then
213-
preview="${preview:0:$truncated_len}..."
214-
fi
269+
fi
270+
# Fallback if nothing was extracted.
271+
if [ "${#preview_lines[@]}" -eq 0 ]; then
272+
preview_lines+=("$(_truncate "$TOOL_INPUT_JSON" 120)")
215273
fi
216274

217275
render_main_menu() {
218-
# Move cursor to top-left and clear screen.
219276
printf '\033[H\033[2J'
220277
printf "${BOLD}Passthru Permission Prompt${RESET}\n\n"
221278
printf "Tool: ${CYAN}%s${RESET}\n" "${TOOL_NAME:-(unknown)}"
222-
printf "Input: ${DIM}%s${RESET}\n\n" "$preview"
279+
# Render preview lines.
280+
local first=1
281+
for _pline in "${preview_lines[@]}"; do
282+
if [ "$first" -eq 1 ]; then
283+
printf "Input: ${DIM}%b${RESET}\n" "$_pline"
284+
first=0
285+
else
286+
printf " ${DIM}%b${RESET}\n" "$_pline"
287+
fi
288+
done
289+
printf '\n'
223290

224291
local i
225292
for ((i = 0; i < MENU_COUNT; i++)); do
@@ -301,19 +368,40 @@ fi
301368

302369
proposed="$(propose_rule)"
303370

304-
CONFIRM_LABELS=("[Enter] Accept rule" "[E] Edit rule JSON" "[Esc] Back to menu")
305-
CONFIRM_KEYS=(enter e esc)
306-
CONFIRM_COUNT=${#CONFIRM_LABELS[@]}
307-
confirm_sel=0
371+
# Extract tool regex and match fields from the proposed rule for two-field editing.
372+
prop_tool="$(jq -r '.tool // ""' <<<"$proposed" 2>/dev/null)"
373+
prop_match_key="$(jq -r '.match // empty | keys[0] // empty' <<<"$proposed" 2>/dev/null)"
374+
prop_match_val="$(jq -r '.match // empty | to_entries[0].value // empty' <<<"$proposed" 2>/dev/null)"
308375

309-
render_confirm_menu() {
376+
render_rule_editor() {
310377
printf '\033[H\033[2J'
311378
printf "${BOLD}Passthru Permission Prompt${RESET}\n\n"
312379
printf "Tool: ${CYAN}%s${RESET}\n" "${TOOL_NAME:-(unknown)}"
313-
printf "Input: ${DIM}%s${RESET}\n\n" "$preview"
380+
local _first=1
381+
for _pl in "${preview_lines[@]}"; do
382+
if [ "$_first" -eq 1 ]; then
383+
printf "Input: ${DIM}%b${RESET}\n" "$_pl"
384+
_first=0
385+
else
386+
printf " ${DIM}%b${RESET}\n" "$_pl"
387+
fi
388+
done
389+
printf '\n'
314390
printf "Suggested rule:\n"
315-
printf " ${GREEN}%s${RESET}\n\n" "$proposed"
391+
printf " Tool regex: ${GREEN}%s${RESET}\n" "$prop_tool"
392+
if [ -n "$prop_match_key" ]; then
393+
printf " Match %-6s ${GREEN}%s${RESET}\n" "${prop_match_key}:" "$prop_match_val"
394+
fi
395+
printf '\n'
396+
}
316397

398+
CONFIRM_LABELS=("[Enter] Accept rule" "[E] Edit fields" "[Esc] Back to menu")
399+
CONFIRM_KEYS=(enter e esc)
400+
CONFIRM_COUNT=${#CONFIRM_LABELS[@]}
401+
confirm_sel=0
402+
403+
render_confirm_screen() {
404+
render_rule_editor
317405
local i
318406
for ((i = 0; i < CONFIRM_COUNT; i++)); do
319407
if [ "$i" -eq "$confirm_sel" ]; then
@@ -325,75 +413,82 @@ render_confirm_menu() {
325413
printf "\n\033[2mUse arrow keys or press a letter key\033[0m\n"
326414
}
327415

328-
render_confirm_menu
416+
render_confirm_screen
329417

330418
while true; do
331419
read_key
332420

333421
case "$KEY" in
334422
up)
335-
if [ "$confirm_sel" -gt 0 ]; then
336-
confirm_sel=$((confirm_sel - 1))
337-
else
338-
confirm_sel=$((CONFIRM_COUNT - 1))
339-
fi
340-
render_confirm_menu
423+
confirm_sel=$(( (confirm_sel - 1 + CONFIRM_COUNT) % CONFIRM_COUNT ))
424+
render_confirm_screen
341425
;;
342426
down)
343-
if [ "$confirm_sel" -lt $((CONFIRM_COUNT - 1)) ]; then
344-
confirm_sel=$((confirm_sel + 1))
345-
else
346-
confirm_sel=0
347-
fi
348-
render_confirm_menu
427+
confirm_sel=$(( (confirm_sel + 1) % CONFIRM_COUNT ))
428+
render_confirm_screen
349429
;;
350430
enter)
351431
case "${CONFIRM_KEYS[$confirm_sel]}" in
352-
enter)
353-
write_verdict_always "$answer" "$proposed"
354-
exit 0
355-
;;
432+
enter) break ;;
356433
e)
357-
# Edit path below.
358-
break
434+
# Two-field editor below.
435+
printf '\033[H\033[2J'
436+
printf "${BOLD}Edit Rule${RESET}\n\n"
437+
printf "Edit each field. Leave blank to keep the suggested value.\n\n"
438+
printf "Tool regex ${DIM}[%s]${RESET}: " "$prop_tool"
439+
edited_tool=""
440+
IFS= read -r -e -t "$TIMEOUT" edited_tool || true
441+
[ -z "$edited_tool" ] && edited_tool="$prop_tool"
442+
if [ -n "$prop_match_key" ]; then
443+
printf "Match %s ${DIM}[%s]${RESET}: " "$prop_match_key" "$prop_match_val"
444+
edited_match=""
445+
IFS= read -r -e -t "$TIMEOUT" edited_match || true
446+
[ -z "$edited_match" ] && edited_match="$prop_match_val"
447+
prop_match_val="$edited_match"
448+
fi
449+
prop_tool="$edited_tool"
450+
# Rebuild and re-render.
451+
render_confirm_screen
359452
;;
360453
esc)
361-
# Back to main menu. Re-run entire script via exec for simplicity.
362454
exec bash "$0"
363455
;;
364456
esac
365457
;;
366458
e)
367-
break # fall through to edit
459+
# Two-field editor (shortcut).
460+
printf '\033[H\033[2J'
461+
printf "${BOLD}Edit Rule${RESET}\n\n"
462+
printf "Edit each field. Leave blank to keep the suggested value.\n\n"
463+
printf "Tool regex ${DIM}[%s]${RESET}: " "$prop_tool"
464+
edited_tool=""
465+
IFS= read -r -e -t "$TIMEOUT" edited_tool || true
466+
[ -z "$edited_tool" ] && edited_tool="$prop_tool"
467+
if [ -n "$prop_match_key" ]; then
468+
printf "Match %s ${DIM}[%s]${RESET}: " "$prop_match_key" "$prop_match_val"
469+
edited_match=""
470+
IFS= read -r -e -t "$TIMEOUT" edited_match || true
471+
[ -z "$edited_match" ] && edited_match="$prop_match_val"
472+
prop_match_val="$edited_match"
473+
fi
474+
prop_tool="$edited_tool"
475+
render_confirm_screen
368476
;;
369477
esc|timeout)
370-
# Back to main menu.
371478
exec bash "$0"
372479
;;
373480
*)
374-
# Unknown key: ignore.
375481
;;
376482
esac
377483
done
378484

379-
# Edit path: read a full line with readline.
380-
printf '\033[H\033[2J'
381-
printf "${BOLD}Edit Rule JSON${RESET}\n\n"
382-
printf "Current:\n ${GREEN}%s${RESET}\n\n" "$proposed"
383-
printf "Type new JSON (leave blank to accept):\n"
384-
edited=""
385-
if ! IFS= read -r -e -t "$TIMEOUT" edited; then
386-
exit 0
387-
fi
388-
if [ -z "$edited" ]; then
389-
write_verdict_always "$answer" "$proposed"
390-
elif jq -e 'type == "object"' >/dev/null 2>&1 <<<"$edited"; then
391-
write_verdict_always "$answer" "$edited"
485+
# Build the final rule JSON from the (possibly edited) fields.
486+
if [ -n "$prop_match_key" ] && [ -n "$prop_match_val" ]; then
487+
final_rule="$(jq -cn --arg t "$prop_tool" --arg k "$prop_match_key" --arg v "$prop_match_val" \
488+
'{tool: $t, match: {($k): $v}}')"
392489
else
393-
printf '\n${RED}Invalid JSON (must be an object)${RESET}\n'
394-
printf 'Using suggested rule: %s\n' "$proposed"
395-
sleep 2
396-
write_verdict_always "$answer" "$proposed"
490+
final_rule="$(jq -cn --arg t "$prop_tool" '{tool: $t}')"
397491
fi
492+
write_verdict_always "$answer" "$final_rule"
398493

399494
exit 0

scripts/overlay.sh

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,26 @@ _write_env_file() {
137137
}
138138
_write_env_file
139139

140-
# Compute popup height dynamically based on content.
141-
_term_cols="$(tput cols 2>/dev/null || echo 120)"
142-
_popup_cols=$(( _term_cols * 80 / 100 ))
143-
[ "$_popup_cols" -lt 40 ] && _popup_cols=40
144-
_input_str="${PASSTHRU_OVERLAY_TOOL_INPUT_JSON:-}"
145-
_input_len="${#_input_str}"
146-
if [ "$_input_len" -gt 120 ]; then _input_len=120; fi
147-
_input_lines=$(( (_input_len + 7) / (_popup_cols - 8) + 1 ))
148-
[ "$_input_lines" -lt 1 ] && _input_lines=1
149-
_popup_height=$(( 12 + _input_lines ))
150-
[ "$_popup_height" -lt 13 ] && _popup_height=13
140+
# Compute popup height dynamically based on content and tool type.
141+
# Base: title(1) + blank(1) + tool(1) + input(varies) + blank(1) +
142+
# 5 menu(5) + blank(1) + hint(1) + cursor(1) = 12 + input_lines
143+
_tool="${PASSTHRU_OVERLAY_TOOL_NAME:-}"
144+
_input="${PASSTHRU_OVERLAY_TOOL_INPUT_JSON:-}"
145+
_extra=0
146+
case "$_tool" in
147+
NotebookEdit)
148+
# file_path + cell info = 2 lines
149+
_extra=1 ;;
150+
Grep|Glob)
151+
# pattern + path = 2 lines
152+
_extra=1 ;;
153+
mcp__*)
154+
# pretty-printed JSON: estimate lines from key count, cap at 8
155+
_jlines="$(jq 'length' <<<"$_input" 2>/dev/null || echo 1)"
156+
[ "$_jlines" -gt 10 ] && _jlines=10
157+
_extra=$((_jlines)) ;;
158+
esac
159+
_popup_height=$((13 + _extra))
151160
[ "$_popup_height" -gt 30 ] && _popup_height=30
152161

153162
launch_tmux() {

0 commit comments

Comments
 (0)