Skip to content

Commit a051a6f

Browse files
authored
Merge pull request #576 from rtk-ai/fix/hook-bypass-permission
fix(hook): respect Claude Code deny/ask permission rules on rewrite
2 parents 976077c + 466aa78 commit a051a6f

8 files changed

Lines changed: 614 additions & 54 deletions

File tree

.claude/hooks/rtk-rewrite.sh

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
#!/bin/bash
2+
# rtk-hook-version: 3
23
# RTK auto-rewrite hook for Claude Code PreToolUse:Bash
34
# Transparently rewrites raw commands to their RTK equivalents.
45
# Uses `rtk rewrite` as single source of truth — no duplicate mapping logic here.
56
#
67
# 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
714

815
# --- Audit logging (opt-in via RTK_HOOK_AUDIT=1) ---
916
_rtk_audit_log() {
@@ -37,34 +44,64 @@ case "$CMD" in
3744
*'<<'*) _rtk_audit_log "skip:heredoc" "$CMD"; exit 0 ;;
3845
esac
3946

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=$?
4751

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
5378

5479
_rtk_audit_log "rewrite" "$CMD" "$REWRITTEN"
5580

5681
# Build the updated tool_input with all original fields preserved, only command changed.
5782
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
5883
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')
5984

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

ARCHITECTURE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,15 @@ SYSTEM init.rs init N/A ✓
290290
gain.rs gain N/A ✓
291291
config.rs (internal) N/A ✓
292292
rewrite_cmd.rs rewrite N/A ✓
293+
permissions.rs CC permission checks N/A ✓
293294
294295
SHARED utils.rs Helpers N/A ✓
295296
filter.rs Language filters N/A ✓
296297
tracking.rs Token tracking N/A ✓
297298
tee.rs Full output recovery N/A ✓
298299
```
299300

300-
**Total: 67 modules** (45 command modules + 22 infrastructure modules)
301+
**Total: 71 modules** (49 command modules + 22 infrastructure modules)
301302

302303
### Module Count Breakdown
303304

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Bug Fixes
1111

12+
* **hook:** respect Claude Code deny/ask permission rules on rewrite — hook now checks settings.json before rewriting commands, preventing bypass of user-configured deny/ask permissions
13+
* **git:** replace symbol prefixes (`* branch`, `+ Staged:`, `~ Modified:`, `? Untracked:`) with plain lowercase labels (`branch:`, `staged:`, `modified:`, `untracked:`) in git status output
1214
* **ruby:** use `rails test` instead of `rake test` when positional file args are passed — `rake test` ignores positional files and only supports `TEST=path`
1315

1416
### Features

hooks/rtk-rewrite.sh

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
#!/usr/bin/env bash
2-
# rtk-hook-version: 2
2+
# rtk-hook-version: 3
33
# RTK Claude Code hook — rewrites commands to use rtk for token savings.
44
# Requires: rtk >= 0.23.0, jq
55
#
66
# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`,
77
# which is the single source of truth (src/discover/registry.rs).
88
# 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
915

1016
if ! command -v jq &>/dev/null; then
1117
echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2
@@ -37,25 +43,56 @@ if [ -z "$CMD" ]; then
3743
exit 0
3844
fi
3945

40-
# Delegate all rewrite logic to the Rust binary.
41-
# rtk rewrite exits 1 when there's no rewrite — hook passes through silently.
42-
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || exit 0
46+
# Delegate all rewrite + permission logic to the Rust binary.
47+
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null)
48+
EXIT_CODE=$?
4349

44-
# No change — nothing to do.
45-
if [ "$CMD" = "$REWRITTEN" ]; then
46-
exit 0
47-
fi
50+
case $EXIT_CODE in
51+
0)
52+
# Rewrite found, no permission rules matched — safe to auto-allow.
53+
# If the output is identical, the command was already using RTK.
54+
[ "$CMD" = "$REWRITTEN" ] && exit 0
55+
;;
56+
1)
57+
# No RTK equivalent — pass through unchanged.
58+
exit 0
59+
;;
60+
2)
61+
# Deny rule matched — let Claude Code's native deny rule handle it.
62+
exit 0
63+
;;
64+
3)
65+
# Ask rule matched — rewrite the command but do NOT auto-allow so that
66+
# Claude Code prompts the user for confirmation.
67+
;;
68+
*)
69+
exit 0
70+
;;
71+
esac
4872

4973
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
5074
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')
5175

52-
jq -n \
53-
--argjson updated "$UPDATED_INPUT" \
54-
'{
55-
"hookSpecificOutput": {
56-
"hookEventName": "PreToolUse",
57-
"permissionDecision": "allow",
58-
"permissionDecisionReason": "RTK auto-rewrite",
59-
"updatedInput": $updated
60-
}
61-
}'
76+
if [ "$EXIT_CODE" -eq 3 ]; then
77+
# Ask: rewrite the command, omit permissionDecision so Claude Code prompts.
78+
jq -n \
79+
--argjson updated "$UPDATED_INPUT" \
80+
'{
81+
"hookSpecificOutput": {
82+
"hookEventName": "PreToolUse",
83+
"updatedInput": $updated
84+
}
85+
}'
86+
else
87+
# Allow: rewrite the command and auto-allow.
88+
jq -n \
89+
--argjson updated "$UPDATED_INPUT" \
90+
'{
91+
"hookSpecificOutput": {
92+
"hookEventName": "PreToolUse",
93+
"permissionDecision": "allow",
94+
"permissionDecisionReason": "RTK auto-rewrite",
95+
"updatedInput": $updated
96+
}
97+
}'
98+
fi

src/hook_check.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::PathBuf;
22

3-
const CURRENT_HOOK_VERSION: u8 = 2;
3+
const CURRENT_HOOK_VERSION: u8 = 3;
44
const WARN_INTERVAL_SECS: u64 = 24 * 3600;
55

66
/// Hook status for diagnostics and `rtk gain`.

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ mod mypy_cmd;
3939
mod next_cmd;
4040
mod npm_cmd;
4141
mod parser;
42+
mod permissions;
4243
mod pip_cmd;
4344
mod playwright_cmd;
4445
mod pnpm_cmd;

0 commit comments

Comments
 (0)