Skip to content

fix(hooks): require all segments to match allow rules (#1213)#1214

Merged
FlorianBruniaux merged 1 commit intodevelopfrom
fix/permission-compound-allow-escalation
Apr 13, 2026
Merged

fix(hooks): require all segments to match allow rules (#1213)#1214
FlorianBruniaux merged 1 commit intodevelopfrom
fix/permission-compound-allow-escalation

Conversation

@pszymkowiak
Copy link
Copy Markdown
Collaborator

Summary

Fixes #1213 — a compound command permission escalation where a single allowed segment granted auto-allow to the entire chain.

Before (broken):

rtk rewrite "git status"                    # exit 3 (ask) ✓
rtk rewrite "git add ."                     # exit 3 (ask) ✓
rtk rewrite "git status && git add ."       # exit 0 (allow) ❌ BUG

After (fixed):

rtk rewrite "git status && git add ."       # exit 3 (ask) ✓
rtk rewrite "cargo test && git add . && git commit -m foo"  # exit 3 (ask) ✓

Root cause

check_command_with_rules used an any_allow flag that was set by the first matching segment and never unset. Once set, subsequent segments skipped the allow check entirely, escalating the whole chain.

Fix

Replace any_allow with all_segments_allowed logic. Every non-empty segment must independently match an allow rule — otherwise the chain demotes to Default (ask). Also add:

  • saw_segment guard to prevent vacuous Allow on empty commands
  • !allow_rules.is_empty() check in the final verdict

Precedence is unchanged: Deny > Ask > Allow > Default.

Impact

Closes a permission escalation vector: an LLM agent could construct <allowed_cmd> && <unapproved_cmd> to bypass user confirmation on the second command.

Test plan

Related

Compound commands (`&&`, `||`, `|`, `;`) previously received
PermissionVerdict::Allow when *any single segment* matched an allow
rule. This allowed a permission escalation where an LLM agent could
chain an allowed command with an unapproved one (e.g.
`git status && git add .`) to bypass user confirmation.

Replace the `any_allow` flag with `all_segments_allowed` logic: every
non-empty segment must independently match an allow rule for the chain
to receive Allow. If any segment fails to match, the verdict demotes to
Default (ask). Deny still short-circuits on any match, and Ask still
wins over a partial Allow.

Also add a `saw_segment` guard and a `!allow_rules.is_empty()` check to
prevent a vacuous Allow on empty commands or empty rule sets.

Add 6 regression tests covering the reproduction case from #1213, all
four compound separators, and ask-wins-over-partial-allow precedence.

Fixes #1213

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>
Copy link
Copy Markdown
Collaborator

@FlorianBruniaux FlorianBruniaux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — security fix, reviewed diff, tests pass.

@FlorianBruniaux FlorianBruniaux merged commit 60c8416 into develop Apr 13, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants