Skip to content

Read-only commands (ls/grep/find/wc/cat/tail) should return exit 0 from rtk rewrite, not exit 3 #1232

Open

Description

@FolahanWilliams

Summary

`rtk rewrite` returns exit code 3 ("ask rule matched") for read-only file-inspection commands like `ls`, `grep`, `find`, `wc`. These are non-destructive and should return exit 0 ("auto-allow") so the Claude Code hook can rewrite and auto-execute them without requiring user approval on every call.

Reproduction

Running against the standard `rtk-rewrite.sh` hook (`rtk-hook-version: 3`):

```bash
$ rtk rewrite 'ls -la /tmp'
rtk ls -la /tmp
$ echo $?
3

$ rtk rewrite 'grep -n foo /tmp/x'
rtk grep -n foo /tmp/x
$ echo $?
3

$ rtk rewrite 'ls -la /tmp; echo done'
rtk ls -la /tmp; echo done
$ echo $?
3

$ rtk rewrite 'ls /tmp && echo ok'
rtk ls /tmp && echo ok
$ echo $?
3
```

All four return exit 3. Per the hook protocol documented in `rtk-rewrite.sh`:

```

Exit code protocol for `rtk rewrite`:

0 + stdout Rewrite found, no deny/ask rule matched → auto-allow

1 No RTK equivalent → pass through unchanged

2 Deny rule matched → pass through

3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user

```

Why this matters

Read-only commands don't warrant a user prompt. But more importantly, exit 3 has a silent-failure mode in permissive Claude Code Bash configurations (filed separately): when exit 3 is returned, the hook outputs `updatedInput` without `permissionDecision`, expecting Claude Code to prompt. In a session where Bash is auto-approved, no prompt is shown and the mutation is silently dropped — the original unrewritten command runs.

Because of this interaction, classifying `ls`/`grep`/`find`/`wc` as exit 3 means in many real-world Claude Code sessions, RTK provides ~0% savings on the very commands that should be its hot path.

Empirically on my setup, `rtk discover` reports across 30 days of Claude Code usage:

Command Count
`grep -n` 60
`ls -la` 27
`find` 24
`tail -20` 11
`wc -l` 5

All listed as "already handled by RTK" — yet the corresponding session JSONLs show raw command output, not rewritten format, proving the hook never successfully mutated them.

Suggested fix

In `src/discover/registry.rs`, classify these as auto-allow (exit 0):

  • `ls`, `ll`
  • `grep`, `rg`
  • `find`
  • `wc`
  • `cat`, `head`, `tail`
  • `stat`, `file`

Keep exit 3 for commands that modify state: `rm`, `mv`, destructive `git`/`npm` operations, etc.

Environment

  • rtk: 0.23.0+ (verified via `rtk verify`: 141/141 tests pass)
  • Claude Code: current
  • Platform: macOS Darwin 25.5.0
  • Hook: `rtk-hook-version: 3`

Activity

  1. pszymkowiak commented on Apr 12, 2026

    @pszymkowiak
    Collaborator

    wshm · Automated triage by AI

    🔍 Automated Triage

    🐛 Category bug
    🟡 Priority medium
    🎯 Confidence 91%

    Summary

    Read-only commands (ls/grep/find/wc/cat/tail) incorrectly return exit 3 from rtk rewrite instead of exit 0, causing silent hook failures in auto-approved Bash sessions; PR #1332 is in progress.

    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. TechWizard9999 commented on Apr 15, 2026

    @TechWizard9999
    Contributor

    Taking this now. I will adjust rewrite permission behavior for read-only commands and open a focused PR shortly. @pszymkowiak

  3. TechWizard9999 commented on Apr 15, 2026

    @TechWizard9999
    Contributor

    Implemented in PR #1332.\n\nMain change: read-only command rewrites now resolve to allow verdict (exit 0) while deny/ask precedence and default safety remain intact.\n\n@pszymkowiak please review when convenient.

  4. migelizaga-rgb commented on Apr 16, 2026

    @migelizaga-rgb

    Update: Most commands fixed, head and tail still exit 3

    Tested on rtk 0.36.0 — most read-only commands from this issue now correctly return exit 0:

    exit=0  ls -la /tmp
    exit=0  grep -n foo /tmp/x
    exit=0  find . -name "*.js"
    exit=0  wc -l file
    exit=0  cat file.txt
    exit=3  head -5 file       ← still ask
    exit=3  tail -20 file      ← still ask
    exit=1  stat file           ← no RTK equivalent
    

    head is the biggest remaining gap. rtk discover on my Claude Code setup shows 401 head -5 commands across 30 days — the single most common rewritable command that's still blocked by exit 3.

    head and tail are pure read-only. They should be exit 0 (auto-allow) like cat, ls, grep, find, and wc now are.

    Environment

    • rtk: 0.36.0
    • Claude Code: 2.1.110
    • macOS Darwin 25.3.0
    • Hook: rtk-hook-version: 3
  5. acpatelmd commented on May 11, 2026

    @acpatelmd

    Confirming on rtk 0.39.0 (28 days after the April 16 update above): PR #1332 holds — ls, grep, find, wc, cat, gh api, git log, curl standalone all return exit 0. The remaining gap is head / tail AND any pipe terminating in them.

    Probe on 0.39.0

    $ rtk --version
    rtk 0.39.0
    
    $ rtk rewrite 'grep -n foo /tmp/x' && echo exit=$?
    rtk grep -n foo /tmp/x
    exit=0
    
    $ rtk rewrite 'grep -n "foo" file.py'         # quoted pattern, relative path
    exit=0
    
    $ rtk rewrite 'cat /tmp/f1 /tmp/f2 | grep foo'
    exit=0
    
    $ rtk rewrite 'grep -n "foo" *.py | head -20'  # ← the residual leak
    exit=3
    
    $ rtk rewrite 'head -5 /tmp/file' && echo exit=$?
    rtk read /tmp/file --head 5
    exit=3
    

    My 30-day data point

    rtk discover --all on this Claude Code account (macOS arm64, rtk 0.39.0, hook v3, 1,282 sessions, 30,435 Bash commands over 30 days):

    Pattern Count rtk equivalent Status on 0.39.0
    grep -n ... (standalone) 1,483 rtk grep -n ... ✅ exit 0 — but historically exit 3 before PR #1332
    cat <path> (standalone) 1,444 rtk read <path> ✅ exit 0
    `... head -N/... tail -N` (subset of above)
    head -N <file> / tail -N <file> counted under "TOP UNHANDLED" rtk read --head/--tail ❌ exit 3

    The pipe-to-head/tail subset is the residual leak in real-world agentic sessions where many invocations chain through pagination (grep ... | head -20 to get the first match cluster, ls -la | head -10 for top-10 newest, find ... | head -50 for first 50 hits). On this account it's the single largest unfixed pattern.

    Interaction with #1233 silent-mutation-drop

    Worth flagging that the head/tail gap is amplified by #1233 for any account running in permissive Bash mode. My account has Bash(rtk:*) plus per-command allowlists, so exit-3 mutations from ... | head aren't prompted — they're silently dropped and the bare form executes. That's about ~750K input tokens / 30 days that the diagnosis-half of the fix recovers, with the execution-half pending #1233.

    Suggested patch shape (head/tail)

    Adding head/tail to the read-only allow list in src/discover/registry.rs should be a one-line extension of the PR #1332 change. The pipe-chain case (<anything> | head -N) is harder — needs the analyzer to walk the AST and verify the terminal command is read-only. Could ship in two passes: (a) standalone head/tail first, (b) pipe-terminal head/tail recognition second.

    Happy to send a PR if helpful — wanted to confirm the diagnosis is still current before duplicating someone else's in-flight work.

  6. KuSh commented on Sep 27, 2026

    @KuSh
    Collaborator

    The request is valid, but the solution as envisioned is too simple to ship. A command that only reads can become a write through what surrounds it: ls > file, find . | xargs rm, cat f | tee g, grep x $(...), process substitution, and more shell constructs than a list of read-only names can cover, so an intrinsic allow verdict would have to understand the whole command line, not the first word. That needs real design work. It is also less pressing than it was: agents now validate commands themselves and offer their own auto-approve modes, so the hook deliberately asserts allow only when one of the user's explicit allow rules matches and never grants it on its own. Leaving this open as the design request.

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