Problem
The RTK PreToolUse rewrite hook (rtk-rewrite.sh) outputs permissionDecision: "allow" alongside
updatedInput. This completely bypasses Claude Code's permission system, including deny rules configured in
.claude/settings.json.
Reproduction
A project has these deny rules in .claude/settings.json:
{
"permissions": {
"deny": [
"Bash(git push --force)",
"Bash(git push -f)",
"Bash(git branch -D)",
"Bash(gh pr merge)",
"Bash(sudo:*)"
]
}
}
When the hook is active:
- Claude generates
git push --force
- Hook matches
push * → rewrites to rtk git push --force
- Hook returns
permissionDecision: "allow" → deny rule is never evaluated
- Command executes without any user prompt
Affected commands
The hook matches broad git/gh patterns that include dangerous subcommands:
| Command |
Hook pattern |
Deny rule bypassed |
git push --force |
push * |
Bash(git push --force) |
git push -f |
push * |
Bash(git push -f) |
git push --force-with-lease |
push * |
Bash(git push --force-with-lease) |
git branch -D main |
branch * |
Bash(git branch -D) |
gh pr merge 123 |
gh pr * |
Bash(gh pr merge) |
gh pr close 123 |
gh pr * |
Bash(gh pr close) |
gh issue close 42 |
gh issue * |
Bash(gh issue close) |
gh release delete v1 |
gh release * |
Bash(gh release delete) |
Commands not in the hook's match list (e.g. git reset --hard, sudo) are unaffected — they exit 0 and normal
permissions apply.
Proposed fix
Before emitting permissionDecision: "allow", the hook should dynamically check deny patterns from Claude Code
settings files. If the command matches a deny rule, exit 0 (pass-through to normal permission flow) instead of
rewriting.
Implementation
# After extracting FIRST_CMD, before any rewrite logic:
_matches_deny() {
local cmd="$1"
shift
for settings_file in "$@"; do
[ -f "$settings_file" ] || continue
while IFS= read -r raw; do
[ -z "$raw" ] && continue
local inner="${raw#Bash(}"
inner="${inner%)}"
if [[ "$inner" == *":*" ]]; then
# Wildcard: Bash(sudo:*) → prefix match on "sudo"
local prefix="${inner%:*}"
[[ "$cmd" == "$prefix" || "$cmd" == "$prefix "* ]] && return 0
else
# Exact: Bash(git push --force) → prefix match
[[ "$cmd" == "$inner" || "$cmd" == "$inner "* ]] && return 0
fi
done < <(jq -r '.permissions.deny[]? | select(startswith("Bash("))' "$settings_file" 2>/dev/null)
done
return 1
}
# Discover project root (git, then walk-up fallback)
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
if [ -z "$PROJECT_ROOT" ]; then
_dir="$PWD"
while [ "$_dir" != "/" ]; do
[ -f "$_dir/.claude/settings.json" ] && { PROJECT_ROOT="$_dir"; break; }
_dir=$(dirname "$_dir")
done
fi
DENY_SOURCES=()
[ -n "$PROJECT_ROOT" ] && DENY_SOURCES+=("$PROJECT_ROOT/.claude/settings.json"
"$PROJECT_ROOT/.claude/settings.local.json")
DENY_SOURCES+=("$HOME/.claude/settings.json" "$HOME/.claude/settings.local.json")
if _matches_deny "$FIRST_CMD" "${DENY_SOURCES[@]}"; then
exit 0 # Let normal permission flow handle this command
fi
Design choices
- Dynamic: reads deny rules at runtime from all 4 settings files (project + global, shared + local)
- Fallback: walks up
$PWD if not in a git repo (covers non-git projects and worktree edge cases)
- Zero config: users don't need to maintain a separate deny list — RTK respects their existing Claude Code
settings
- Performance: jq on small JSON files adds ~5ms, well within hook timeout
Relationship to #243
#243 proposes exclude_commands config for compatibility reasons (some commands break with RTK). This issue
is complementary — it addresses the security angle: commands that work fine with RTK but should never be
auto-allowed because the user explicitly denied them.
Both could coexist: deny-rule checking as a built-in safety net, exclude_commands as user preference.
Alternative approach
Don't emit permissionDecision: "allow" at all — just return updatedInput and let Claude Code's permission
system evaluate the rewritten command. This would require users to add Bash(rtk:*) to their allow rules and
mirror all deny rules with rtk prefix. Simpler hook, but much worse UX.
Environment
- RTK: 0.22.2
- Claude Code: latest
- OS: Linux
Problem
The RTK
PreToolUserewrite hook (rtk-rewrite.sh) outputspermissionDecision: "allow"alongsideupdatedInput. This completely bypasses Claude Code's permission system, including deny rules configured in.claude/settings.json.Reproduction
A project has these deny rules in
.claude/settings.json:{ "permissions": { "deny": [ "Bash(git push --force)", "Bash(git push -f)", "Bash(git branch -D)", "Bash(gh pr merge)", "Bash(sudo:*)" ] } }When the hook is active:
git push --forcepush *→ rewrites tortk git push --forcepermissionDecision: "allow"→ deny rule is never evaluatedAffected commands
The hook matches broad git/gh patterns that include dangerous subcommands:
git push --forcepush *Bash(git push --force)git push -fpush *Bash(git push -f)git push --force-with-leasepush *Bash(git push --force-with-lease)git branch -D mainbranch *Bash(git branch -D)gh pr merge 123gh pr *Bash(gh pr merge)gh pr close 123gh pr *Bash(gh pr close)gh issue close 42gh issue *Bash(gh issue close)gh release delete v1gh release *Bash(gh release delete)Commands not in the hook's match list (e.g.
git reset --hard,sudo) are unaffected — they exit 0 and normalpermissions apply.
Proposed fix
Before emitting
permissionDecision: "allow", the hook should dynamically check deny patterns from Claude Codesettings files. If the command matches a deny rule,
exit 0(pass-through to normal permission flow) instead ofrewriting.
Implementation
Design choices
$PWDif not in a git repo (covers non-git projects and worktree edge cases)settings
Relationship to #243
#243 proposes
exclude_commandsconfig for compatibility reasons (some commands break with RTK). This issueis complementary — it addresses the security angle: commands that work fine with RTK but should never be
auto-allowed because the user explicitly denied them.
Both could coexist: deny-rule checking as a built-in safety net,
exclude_commandsas user preference.Alternative approach
Don't emit
permissionDecision: "allow"at all — just returnupdatedInputand let Claude Code's permissionsystem evaluate the rewritten command. This would require users to add
Bash(rtk:*)to their allow rules andmirror all deny rules with
rtkprefix. Simpler hook, but much worse UX.Environment