Skip to content

[hooks] exclude_commands is bypassed for head/tail (line-range fast path skips the exclusion check) #2823

Closed

Description

@jinn001-001

Environment: rtk 0.43.0 · Ubuntu 24.04 (WSL2) · Claude Code hook (rtk hook claude)

[hooks] exclude_commands is bypassed for head/tail by the line-range fast path

Repro (config: ~/.config/rtk/config.toml → [hooks] exclude_commands = ["tail", "head", "rg"]):

$ rtk rewrite "rg -n foo src/"
$ echo $?        # excluded — no rewrite, as configured ✓
1

$ rtk rewrite "tail -3 sample.txt"
rtk read sample.txt --tail-lines 3     # rewritten despite the exclusion ✗

$ rtk rewrite "head -5 sample.txt"
rtk read sample.txt --max-lines 5      # rewritten despite the exclusion ✗

Same through the hook path: echo '{"tool_input":{"command":"tail -5 x.log"}}' | rtk hook claude returns an updatedInput rewrite even with tail excluded.

Cause (master at the time of writing): the head/tail line-range fast path in rewrite_segment_inner returns before the exclusion check —

// src/discover/registry.rs (~L867)
if cmd_part.starts_with("head -") || cmd_part.starts_with("tail ") {
    return rewrite_line_range(cmd_part).map(|r| format!("{}{}", r, redirect_suffix));
}

is_excluded is only consulted in the Classification::Supported arm (~L886) and the TOML/Unsupported arm (~L897), both downstream of this early return. Commands routed through classification (e.g. rg) respect exclusions; head/tail never reach it.

Since exclusions exist precisely to encode "the user said hands off this command", the fast path should check is_excluded(cmd_part, excluded) before rewriting. PR with the one-line guard + tests incoming.

Activity

  1. pszymkowiak commented on Jul 4, 2026

    @pszymkowiak
    Collaborator

    wshm · Automated triage by AI

    🔍 Automated Triage

    🐛 Category bug
    🟠 Priority high
    🎯 Confidence 95%

    Summary

    The head/tail line-range fast path in rewrite_segment_inner (src/discover/registry.rs ~L867) returns before checking is_excluded, silently bypassing configured exclude_commands for head/tail; add the exclusion check before the early return.

    This looks like a simple fix that could be auto-resolved. Repo maintainers can use /wshm fix to attempt it.

    📁 Relevant files
    • src/discover/registry.rs

    Triaged automatically by wshm · This is an automated analysis, not a human review.

  2. bluedepin commented on Aug 22, 2026

    @bluedepin

    Still present in v0.45.0 (Homebrew, macOS arm64). Confirming the root cause above, and adding a consequence that I think raises the severity beyond "config ignored".

    Under an agent harness, this fast path can defeat the harness permission system.

    Claude Code matches its permissions.deny rules against the command after PreToolUse hooks apply updatedInput. So a rewrite that renames a file read also renames it out of the deny rules. Three-cell probe, same file, same tool, same session, with exclude_commands = ["^cat", "^head", "^tail"] configured:

    $ git status                              # compact output -> hook is live
    $ head -n 50 /tmp/denyverify/id_rsa       # not rewritten (exclusion honored)
       -> DENIED by rule Read(**/id_rsa*)
    $ head -50  /tmp/denyverify/id_rsa        # rewritten -> `rtk read ... --max-lines 50`
       -> returned the file contents

    The only difference is the -N shorthand that takes the line-range fast path. rtk read is not a command the harness recognises as a file read, so the deny rule stops applying. On a setup where defaultMode is bypassPermissions, that deny list is the only enforcement there is.

    Two notes for whoever picks up the fix:

    1. No exclude_commands value can work around it. I tested "head", "^head", "^(head|tail)", and even the literal "head -" that provably matches the string — all still rewrite head -50 f, while head -n 50 f and head f are excluded correctly. That matches the early-return analysis in the issue body exactly.
    2. The hook itself behaves well otherwise — v0.45.0 emits updatedInput with no permissionDecision, so it is not force-allowing anything, and rm -rf, git push --force, git push -f and sed are all correctly left alone. This is specifically the head/tail fast path.

    Happy to re-test against the PR when it lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions