hook prefixes `rtk` to a bare variable-assignment line in a multi-line command, breaking the assignment (exec fails, variable left unset)
Summary
In a multi-line Bash command, the hook rewrites a plain shell variable
assignment line into an rtk invocation when the assigned value is a path whose
last segment matches a known handler name (observed with jq):
J=/tmp/jq -> rtk J=/tmp/jq
At runtime rtk then tries to exec J=/tmp/jq as a program, prints
[rtk: No such file or directory (os error 2)], exits 127, and -- because the
line was consumed by rtk instead of the shell -- the variable is never set.
Every later $J in the script expands empty, so a path like $J/file silently
becomes /file.
Version
rtk 0.46.0
Repro
$ rtk hook check "$(printf 'J=/tmp/jq\nls -l $J/f')"
rtk J=/tmp/jq
rtk ls -l $J/f
Same result through the Claude Code hook path:
$ printf '%s' '{"session_id":"t","tool_name":"Bash","tool_input":{"command":"J=/tmp/jq\nls -l $J/f"},"permission_mode":"default","cwd":"/tmp"}' \
| rtk hook claude | jq -r '.hookSpecificOutput.updatedInput.command'
rtk J=/tmp/jq
rtk ls -l $J/f
Expected: the assignment line is left untouched (only ls should gain the
rtk prefix).
Trigger bisect -- the value must be a multi-segment path whose final segment is a handler name; the variable name does not matter:
| assignment line | result |
|---|---|
J=/tmp/jq |
mangled |
K=/tmp/jq |
mangled |
J=~/x/jq |
mangled |
J=jq (no slash) |
ok |
J=/tmp/jq/x |
ok |
J=/tmp/git |
ok |
J=/tmp/grep |
ok |
Not every handler name triggers (git, grep, docker, kubectl, cargo,
npm are safe; jq and jira are not), so it looks specific to how certain
handlers match the command word.
Single-line forms are all handled correctly (J=/tmp/jq; ls $J/f,
FOO=1 git status) -- only the line-by-line multi-line path is affected.
Impact
This is a data-loss class bug, similar in spirit to #3262. Real-world shape: a copy-then-cleanup script
DEST=<path ending in a trigger segment>
cp draft.md $DEST/final.md
...
rm -rf <scratch copies>
has its assignment eaten, every cp writes to / (Permission denied), and
the trailing rm -rf still runs, deleting the originals before any copy
landed. Claude Code executes hook-rewritten commands in an errexit-suppressed
shell, so even a set -e at the top of the script cannot stop it.
Suggested fix
Never rewrite a line that parses as a shell variable assignment
(NAME=value ... with no command word before it); the existing single-line
code path already gets this right.
Source: rtk-ai/rtk