rtk-rewrite.sh 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. #!/usr/bin/env bash
  2. # rtk-hook-version: 3
  3. # RTK Claude Code hook — rewrites commands to use rtk for token savings.
  4. # Requires: rtk >= 0.23.0, jq
  5. #
  6. # This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`,
  7. # which is the single source of truth (src/discover/registry.rs).
  8. # To add or change rewrite rules, edit the Rust registry — not this file.
  9. #
  10. # Exit code protocol for `rtk rewrite`:
  11. # 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow
  12. # 1 No RTK equivalent → pass through unchanged
  13. # 2 Deny rule matched → pass through (Claude Code native deny handles it)
  14. # 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user
  15. if ! command -v jq &>/dev/null; then
  16. echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2
  17. exit 0
  18. fi
  19. if ! command -v rtk &>/dev/null; then
  20. echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2
  21. exit 0
  22. fi
  23. # Version guard: rtk rewrite was added in 0.23.0.
  24. # Older binaries: warn once and exit cleanly (no silent failure).
  25. RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
  26. if [ -n "$RTK_VERSION" ]; then
  27. MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1)
  28. MINOR=$(echo "$RTK_VERSION" | cut -d. -f2)
  29. # Require >= 0.23.0
  30. if [ "$MAJOR" -eq 0 ] && [ "$MINOR" -lt 23 ]; then
  31. echo "[rtk] WARNING: rtk $RTK_VERSION is too old (need >= 0.23.0). Upgrade: cargo install rtk" >&2
  32. exit 0
  33. fi
  34. fi
  35. INPUT=$(cat)
  36. CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
  37. if [ -z "$CMD" ]; then
  38. exit 0
  39. fi
  40. # Delegate all rewrite + permission logic to the Rust binary.
  41. REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null)
  42. EXIT_CODE=$?
  43. case $EXIT_CODE in
  44. 0)
  45. # Rewrite found, no permission rules matched — safe to auto-allow.
  46. # If the output is identical, the command was already using RTK.
  47. [ "$CMD" = "$REWRITTEN" ] && exit 0
  48. ;;
  49. 1)
  50. # No RTK equivalent — pass through unchanged.
  51. exit 0
  52. ;;
  53. 2)
  54. # Deny rule matched — let Claude Code's native deny rule handle it.
  55. exit 0
  56. ;;
  57. 3)
  58. # Ask rule matched — rewrite the command but do NOT auto-allow so that
  59. # Claude Code prompts the user for confirmation.
  60. ;;
  61. *)
  62. exit 0
  63. ;;
  64. esac
  65. ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
  66. UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')
  67. if [ "$EXIT_CODE" -eq 3 ]; then
  68. # Ask: rewrite the command, omit permissionDecision so Claude Code prompts.
  69. jq -n \
  70. --argjson updated "$UPDATED_INPUT" \
  71. '{
  72. "hookSpecificOutput": {
  73. "hookEventName": "PreToolUse",
  74. "updatedInput": $updated
  75. }
  76. }'
  77. else
  78. # Allow: rewrite the command and auto-allow.
  79. jq -n \
  80. --argjson updated "$UPDATED_INPUT" \
  81. '{
  82. "hookSpecificOutput": {
  83. "hookEventName": "PreToolUse",
  84. "permissionDecision": "allow",
  85. "permissionDecisionReason": "RTK auto-rewrite",
  86. "updatedInput": $updated
  87. }
  88. }'
  89. fi