Skip to content

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

@joshua-vesselbags

Description

@joshua-vesselbags

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions