Agent guidance tells agents to prefix shell builtins with `rtk`, and promises it is safe — `rtk cd` exits 127 and aborts the compound
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 81.1k
- Forks
- 5.1k
- Avg merge
- 4d 21h
- Merged PRs (30d)
- 35
Description
Summary
Three instruction blocks tell agents to prefix every shell command with rtk and
promise the prefix is always safe. It is not: for a shell builtin, rtk execs a binary
by that name, gets ENOENT, and exits 127 — taking the rest of the compound with it.
$ rtk cd /tmp && echo REACHED
[rtk: No such file or directory (os error 2)] # exit 127, REACHED never prints
Reported twice from real sessions, and each time the agent drew the wrong conclusion:
- #2508 — GLM-4.7-Flash ran
rtk cd /path && rtk npm init -y;npm initran in the
wrong directory, and the model invented a--workdirflag to compensate. - #3331 — Codex ran
rtk cd backend && uv run pytest …, read the ENOENT as "rtk
isn't available in this environment", and fell back to raw commands for the rest of
the session. Token savings off, and the diagnosis was wrong: the error came fromrtk
itself, so the binary was found and invoked.
The three places that say it
| source | wording |
|---|---|
hooks/rtk-awareness-full.md:3-6 |
"Prefix every shell command with rtk… Commands RTK has no filter for run as-is, so the prefix is always safe." |
RTK_INSTRUCTIONS (src/hooks/init.rs:241), live via rtk init --claude-md |
"Always prefix commands with rtk. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use." |
COPILOT_INSTRUCTIONS (src/hooks/init.rs:5925), written to .github/copilot-instructions.md |
"Always prefix shell commands with rtk" |
Also docs/guide/getting-started/configuration.md:69, which describes the full level as
"prefix every command with rtk".
"If not, it passes through unchanged" is the precise falsehood — there is no passthrough
for a builtin, because there is no binary to pass through to.
Hook-based agents at the default awareness.level are not affected: hooks/rtk-awareness.md
carries no prefixing rule at all. The exposure is the full level — which per
hooks/README.md:18 the hookless agents (cline, kilocode, windsurf) get unconditionally,
having no hook to rewrite for them — plus the two legacy blocks above.
Fix: scope the rule to external programs
For hooks/rtk-awareness-full.md:3-6:
-Prefix every shell command with `rtk`: `rtk git status`, `rtk cargo test`,
+Prefix external programs with `rtk`: `rtk git status`, `rtk cargo test`,
`rtk npm run build`, `rtk ls src/`. Keep the prefix inside chains:
-`rtk git add . && rtk git commit -m "msg"`. Commands RTK has no filter for
-run as-is, so the prefix is always safe.
+`rtk git add . && rtk git commit -m "msg"`. Programs RTK has no filter for
+run as-is, so the prefix is safe for anything you would run from `PATH`.
+
+Do not prefix shell builtins or keywords — `test`, `[`, `read`, `cd`, `echo`,
+`export`, `eval`, `source`, `printf`. RTK defines subcommands named for some of
+them that do not mean what the builtin means.
The same wording change applies to RTK_INSTRUCTIONS and COPILOT_INSTRUCTIONS. A fix to
rtk-awareness-full.md alone leaves Copilot and --claude-md installs broken, and
test_awareness_activation_rule_only_in_full pins only the awareness files, so nothing
catches the omission.
Naming the builtins is what makes it work for the cases removal cannot reach: read and
help have no binary to fall through to, so no amount of routing fixes them.
While the file is open: high and full also advertise rtk gain, and
RTK_INSTRUCTIONS:275 advertises rtk test <cmd>. Meta commands are for humans, not
agents — worth dropping against the asserted line budget, but that is a simplification,
not part of this fix.
Why not fix it in the rewrite engine
#2534 proposed stripping the redundant prefix during rewrite (rtk cd /tmp → cd /tmp).
It works for its twelve names, but it opens a permission bypass and was rejected.
Every hook computes the permission verdict on the raw input and emits the rewritten
string — decide_from_verdict(cmd, permissions::check_command_for(cmd, host))
(src/hooks/hook_cmd.rs:274). That is safe only while a rewrite goes foo … → rtk foo …,
so both strings share a prefix. Stripping inverts the direction, and
command_matches_pattern is a word-boundary prefix match, so Bash(source:*) never matches
rtk source x.
With permissions: {"allow": ["Bash(rtk:*)"], "deny": ["Bash(source:*)"]}:
$ echo '{"tool_name":"Bash","tool_input":{"command":"rtk source /tmp/evil.sh"}}' | rtk hook claude
# develop: (no output — Defer; the host applies its own rules)
# with #2534: {"updatedInput":{"command":"source /tmp/evil.sh"},"permissionDecision":"allow"}
An explicitly denied command, emitted with an explicit auto-allow. Bash(rtk:*) is the
obvious workaround for prefixed-command prompt spam, so this is not a contrived config.
It also would not have helped the reporters: hookless agents are exactly the ones getting
rtk-awareness-full.md, and they never call rtk rewrite at all.
For anyone who revives the idea: test, read and env are shell builtins and real
RTK subcommands. Stripping any of them silently changes which program runs.
Optional: a better error than ENOENT
#3331's misdiagnosis came from the bare [rtk: No such file or directory (os error 2)].
When rtk is handed a known builtin name, saying so — "cd is a shell builtin; drop the
rtk prefix" — costs no permission surface and stops the "rtk isn't available" conclusion.
Related
- #2508, #3331 — the two reports, superseded by this issue
- #3932 —
testis the case where RTK has a shadowing subcommand; removal fixes that one,
this wording fixes the rest. Independent changes. - #3186 — the same prompt-shape problem for hook-based agents, fixed on
developby the
awareness.levelsplit - #2534 — the rejected rewrite-engine approach
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Review hooks/rtk-awareness-full.md, the RTK_INSTRUCTIONS and COPILOT_INSTRUCTIONS blocks in src/hooks/init.rs, and the configuration guide entry. Run test_awareness_activation_rule_only_in_full, then verify all instruction blocks scope prefixing to external programs and name the builtin exceptions without changing the rewrite engine.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, shell
- Domain
- cli, documentation
- Issue type
- Documentation
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100