rtk-ai/rtk

Hook bypass: `$(...)` subshell, backticks, and `xargs <cmd>` skip auto-rewrite

Open

#2.145 aberto em 28 de mai. de 2026

Ver no GitHub
 (1 comment) (0 reactions) (0 assignees)Rust (2.914 forks)batch import
area:clibughelp wantedpriority:medium

Métricas do repositório

Stars
 (48.085 stars)
Métricas de merge de PR
 (Mesclagem média 8d 17h) (49 fundiu PRs em 30d)

Description

Summary

The rtk hook claude PreToolUse rewriter correctly handles top-level commands and common compound forms (|, ;, &&) but bypasses three runtime-resolved patterns. Result: commands that should be rewritten ship as bare invocations, leaking token savings.

Reproduction

Each test uses echo '<json>' | rtk hook claude to simulate the Claude Code PreToolUse hook input.

Works: top-level + | + ; + &&

$ echo '{"tool_input":{"command":"grep -nE pat file"}}' | rtk hook claude
{"hookSpecificOutput":{...,"updatedInput":{"command":"rtk grep -nE pat file"}}}

$ echo '{"tool_input":{"command":"grep -nE pat file | head -3"}}' | rtk hook claude
{"hookSpecificOutput":{...,"updatedInput":{"command":"rtk grep -nE pat file | head -3"}}}

$ echo '{"tool_input":{"command":"grep -nE pat1 f1; rtk grep -nE pat2 f2"}}' | rtk hook claude
{"hookSpecificOutput":{...,"updatedInput":{"command":"rtk grep -nE pat1 f1; rtk grep -nE pat2 f2"}}}

$ echo '{"tool_input":{"command":"grep -nE pat file && echo ok"}}' | rtk hook claude
{"hookSpecificOutput":{...,"updatedInput":{"command":"rtk grep -nE pat file && echo ok"}}}

Bypass 1: `$(...)` command substitution

$ echo '{"tool_input":{"command":"echo \$(grep -nE pat file)"}}' | rtk hook claude
(empty - no rewrite emitted)

Bypass 2: backticks

$ echo '{"tool_input":{"command":"echo \`grep -nE pat file\`"}}' | rtk hook claude
(empty - no rewrite emitted)

This bites real workflows like `git commit -m "$(cat <<EOF ... EOF)"` if anything inside the heredoc shells out, and `var=$(grep ...)` assignments.

Bypass 3: `xargs ` indirection

$ echo '{"tool_input":{"command":"ls *.js | xargs grep -nE pat"}}' | rtk hook claude
{"hookSpecificOutput":{...,"updatedInput":{"command":"rtk ls *.js | xargs grep -nE pat"}}}

The outer `ls` rewrites; the `grep` invoked via `xargs` stays bare because xargs resolves the command at runtime, not parse time. Same surface applies to `find ... -exec `, `bash -c ""`, `eval`, and `parallel`.

Impact

`rtk discover` over 9 sessions / 319 commands surfaced 75 missed `grep -nE` invocations totaling ~10.9K tokens. A meaningful fraction trace to these three bypass patterns (the rest are pre-hook-install historical sessions). For `git commit -m "$(...)"` users the `$()` leak alone is constant.

Suggested fixes (pick whichever the parser comfortably supports)

  1. Recursive parser: walk into `$(...)` and backticks and rewrite the inner command tree. The shell grammar makes this tractable - both forms have unambiguous delimiters.
  2. Indirect-exec detection: when the parser sees `xargs`, `find -exec`, `bash -c`, `eval`, or `parallel`, peek at the next argv token and rewrite it if it's a known RTK-handled cmd.
  3. Shell-side aliases: ship an opt-in `rtk shim install` that drops PATH-shadowing wrappers for the rewritable cmd set into `~/.rtk/bin`. Narrows the leak surface to `command grep` / absolute-path invocations. Cheap to add as a complement to the parser fixes.

Environment

  • rtk 0.42.0 (homebrew)
  • macOS Darwin 25.2.0
  • Claude Code (latest)
  • Hook: PreToolUse / matcher `Bash` / command `rtk hook claude`

Happy to test a fix against my workflow if a branch is published.

Guia do colaborador