rtk-ai/rtk

Compound commands demoted to ask when read-only pipe target lacks explicit allow rule

Open

#1,347 opened on Apr 16, 2026

View on GitHub
 (1 comment) (0 reactions) (0 assignees)Rust (2,914 forks)batch import
P1-criticalarea:clibugeffort-mediumhelp wantedpriority:high

Repository metrics

Stars
 (48,085 stars)
PR merge metrics
 (Avg merge 11d 1h) (45 merged PRs in 30d)

Description

Summary

When a compound command's primary segment matches a user allow rule but its pipe target doesn't, check_command_with_rules demotes the entire command to Default (exit 3). This causes permission prompts for commands like git show <sha> --stat | head -20 even when the user has Bash(git show:*) in their allow list.

The all_segments_allowed check (added in #1213) correctly prevents escalation — but it also penalizes read-only pipe targets (head, tail, awk, sort, etc.) that don't change the security posture of the primary command.

Reproduction

Given ~/.claude/settings.json:

{
  "permissions": {
    "allow": ["Bash(git show:*)"]
  }
}
$ rtk rewrite "git show abc123 --stat"
rtk git show abc123 --stat
$ echo $?
0    # auto-allowed

$ rtk rewrite "git show abc123 --stat | head -20"
rtk git show abc123 --stat | head -20
$ echo $?
3    # demoted to ask — head has no allow rule

Tested on rtk 0.36.0, Windows 11, Git Bash. The same happens with any read-only pipe target (tail, awk, sort, column, tac, etc.) that lacks an explicit allow rule.

Impact

This disproportionately affects subagents in Claude Code, which commonly pipe to head, tail, or wc to keep output small. In a subagent context, the permission prompt bubbles up to the parent session and blocks the agent.

The workaround is adding explicit allow rules for every pipe target (Bash(head:*), Bash(tail:*), etc.), but that shifts the burden to users who need to anticipate which utilities agents will pipe through.

Root cause

In src/hooks/permissions.rs, check_command_with_rules requires every segment to independently match an allow rule:

// line 69-76
if all_segments_allowed {
    let matched = allow_rules
        .iter()
        .any(|pattern| command_matches_pattern(segment, pattern));
    if !matched {
        all_segments_allowed = false;
    }
}

When the pipe target doesn't match any rule, all_segments_allowed becomes false and the verdict falls to Default.

Related

  • #1213 — compound command escalation fix (the all_segments_allowed check)
  • #1232 — read-only commands returning exit 3 instead of exit 0
  • #1332 — PR implementing #1232 fix

Environment

  • rtk: 0.36.0
  • Claude Code: current
  • Platform: Windows 11 (Git Bash)
  • Hook: rtk-hook-version 3

Contributor guide