|
1 | 1 | #!/bin/bash |
| 2 | +# rtk-hook-version: 3 |
2 | 3 | # RTK auto-rewrite hook for Claude Code PreToolUse:Bash |
3 | 4 | # Transparently rewrites raw commands to their RTK equivalents. |
4 | 5 | # Uses `rtk rewrite` as single source of truth — no duplicate mapping logic here. |
5 | 6 | # |
6 | 7 | # To add support for new commands, update src/discover/registry.rs (PATTERNS + RULES). |
| 8 | +# |
| 9 | +# Exit code protocol for `rtk rewrite`: |
| 10 | +# 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow |
| 11 | +# 1 No RTK equivalent → pass through unchanged |
| 12 | +# 2 Deny rule matched → pass through (Claude Code native deny handles it) |
| 13 | +# 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user |
7 | 14 |
|
8 | 15 | # --- Audit logging (opt-in via RTK_HOOK_AUDIT=1) --- |
9 | 16 | _rtk_audit_log() { |
@@ -37,34 +44,64 @@ case "$CMD" in |
37 | 44 | *'<<'*) _rtk_audit_log "skip:heredoc" "$CMD"; exit 0 ;; |
38 | 45 | esac |
39 | 46 |
|
40 | | -# Rewrite via rtk — single source of truth for all command mappings. |
41 | | -# Exit 1 = no RTK equivalent, pass through unchanged. |
42 | | -# Exit 0 = rewritten command (or already RTK, identical output). |
43 | | -REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || { |
44 | | - _rtk_audit_log "skip:no_match" "$CMD" |
45 | | - exit 0 |
46 | | -} |
| 47 | +# Rewrite via rtk — single source of truth for all command mappings and permission checks. |
| 48 | +# Use "|| EXIT_CODE=$?" to capture non-zero exit codes without triggering set -e. |
| 49 | +EXIT_CODE=0 |
| 50 | +REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || EXIT_CODE=$? |
47 | 51 |
|
48 | | -# If output is identical, command was already using RTK — nothing to do. |
49 | | -if [ "$CMD" = "$REWRITTEN" ]; then |
50 | | - _rtk_audit_log "skip:already_rtk" "$CMD" |
51 | | - exit 0 |
52 | | -fi |
| 52 | +case $EXIT_CODE in |
| 53 | + 0) |
| 54 | + # Rewrite found, no permission rules matched — safe to auto-allow. |
| 55 | + if [ "$CMD" = "$REWRITTEN" ]; then |
| 56 | + _rtk_audit_log "skip:already_rtk" "$CMD" |
| 57 | + exit 0 |
| 58 | + fi |
| 59 | + ;; |
| 60 | + 1) |
| 61 | + # No RTK equivalent — pass through unchanged. |
| 62 | + _rtk_audit_log "skip:no_match" "$CMD" |
| 63 | + exit 0 |
| 64 | + ;; |
| 65 | + 2) |
| 66 | + # Deny rule matched — let Claude Code's native deny rule handle it. |
| 67 | + _rtk_audit_log "skip:deny_rule" "$CMD" |
| 68 | + exit 0 |
| 69 | + ;; |
| 70 | + 3) |
| 71 | + # Ask rule matched — rewrite the command but do NOT auto-allow so that |
| 72 | + # Claude Code prompts the user for confirmation. |
| 73 | + ;; |
| 74 | + *) |
| 75 | + exit 0 |
| 76 | + ;; |
| 77 | +esac |
53 | 78 |
|
54 | 79 | _rtk_audit_log "rewrite" "$CMD" "$REWRITTEN" |
55 | 80 |
|
56 | 81 | # Build the updated tool_input with all original fields preserved, only command changed. |
57 | 82 | ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') |
58 | 83 | UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') |
59 | 84 |
|
60 | | -# Output the rewrite instruction in Claude Code hook format. |
61 | | -jq -n \ |
62 | | - --argjson updated "$UPDATED_INPUT" \ |
63 | | - '{ |
64 | | - "hookSpecificOutput": { |
65 | | - "hookEventName": "PreToolUse", |
66 | | - "permissionDecision": "allow", |
67 | | - "permissionDecisionReason": "RTK auto-rewrite", |
68 | | - "updatedInput": $updated |
69 | | - } |
70 | | - }' |
| 85 | +if [ "$EXIT_CODE" -eq 3 ]; then |
| 86 | + # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. |
| 87 | + jq -n \ |
| 88 | + --argjson updated "$UPDATED_INPUT" \ |
| 89 | + '{ |
| 90 | + "hookSpecificOutput": { |
| 91 | + "hookEventName": "PreToolUse", |
| 92 | + "updatedInput": $updated |
| 93 | + } |
| 94 | + }' |
| 95 | +else |
| 96 | + # Allow: output the rewrite instruction in Claude Code hook format. |
| 97 | + jq -n \ |
| 98 | + --argjson updated "$UPDATED_INPUT" \ |
| 99 | + '{ |
| 100 | + "hookSpecificOutput": { |
| 101 | + "hookEventName": "PreToolUse", |
| 102 | + "permissionDecision": "allow", |
| 103 | + "permissionDecisionReason": "RTK auto-rewrite", |
| 104 | + "updatedInput": $updated |
| 105 | + } |
| 106 | + }' |
| 107 | +fi |
0 commit comments