rtk-ai/rtk

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

Open

#2 145 ouverte le 28 mai 2026

Voir sur GitHub
 (1 commentaire) (0 réactions) (0 assignés)Rust (2 914 forks)batch import
area:clibughelp wantedpriority:medium

Métriques du dépôt

Stars
 (48 085 stars)
Métriques de merge PR
 (Merge moyen 8j 17h) (49 PRs mergées en 30 j)

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.

Guide contributeur