Skip to content

Commit 3eb9516

Browse files
kyle-sextonclaude
andcommitted
fix(guardrails): count only nested segments against the reading budget
The 900-sibling substitution pin tripped the 2,048 budget: the tokenizer splits on parens, so that legitimate command holds about 2,700 top-level and body segments. Top-level segments are bounded by the command length, so only nested segments (a launcher, child shell, eval or resolved walk re-entering) are counted now, against 1,024; runuser -u x -s env -- nested 25 deep refuses in about 2.5 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c59300b commit 3eb9516

3 files changed

Lines changed: 14 additions & 9 deletions

File tree

‎plugins/guardrails/CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ All notable changes to the `guardrails` plugin are documented here. Format follo
1212
- **su parses every `-c`-like operand, not only the first.** `su bob -c true -c 'rm -rf /'` returned 0, because su runs the last `-c` and the guard read the first. su's `--command` and `--session-command` also match on any unambiguous prefix now, and an operand attached to `-c` (`su -c'rm -rf /'`, one word `-crm -rf /`) is parsed too.
1313
- **`--` no longer hides a launcher's positional.** For every launcher that takes a positional ahead of the command (`taskset`, `flock`, `chroot`, `chrt`, `timeout`), the word after `--` is still that positional, so `timeout -- 5 rm -rf /` is refused; chrt's priority must be all digits and timeout's duration must start like a number, so `timeout -- rm -rf /` stays refused.
1414
- **Launcher long options match on any unambiguous prefix** (`flock --wa 5`, `nsenter --ta 1`, `timeout --k 1`), except sudo's, and a short cluster ending in an operand-taking letter takes the next word as getopt reads it (`flock -nw 1`, `chrt -dT 1000`); that reading is judged beside the plain one in every segment, so `nice --adj rm -rf /` stays refused, and past 256 such readings in one command the guard refuses rather than judging one reading only. A `-s` / `--shell` naming a program that is not a shell is judged as the command (`su root -s /bin/rm -- -rf /`), `-c -- '…'` is read as `-c '…'`, a `-c` operand of exactly `--` hands the next word to the shell (`su root --com=-- 'rm -rf /'`), a word runuser rejects is never its command word, flock's `-c` after `--` and its lock file is parsed, and timeout's duration after `--` is read in strtod's shape (`+5`, `' 5'`, `inf`).
15-
- **Launcher, child-shell and eval nesting is capped at 24 levels, and past it the guard refuses.** Each level re-enters the parser, and 120 nested `runuser -u bob --` exhausted bash's stack inside the block's telemetry, so the BLOCKED message printed and the process exited 0; deeper nesting died on SIGSEGV. Nested evals are also charged to the tokenizing budget, so `eval` repeated hundreds of times refuses in seconds instead of outrunning the hook timeout, and every judged segment is counted for the whole command, so past 2,048 the guard refuses rather than letting readings that double per level (`runuser -u x -s env --` nested 25 deep) outrun it.
15+
- **Launcher, child-shell and eval nesting is capped at 24 levels, and past it the guard refuses.** Each level re-enters the parser, and 120 nested `runuser -u bob --` exhausted bash's stack inside the block's telemetry, so the BLOCKED message printed and the process exited 0; deeper nesting died on SIGSEGV. Nested evals are also charged to the tokenizing budget, so `eval` repeated hundreds of times refuses in seconds instead of outrunning the hook timeout, and every nested segment a launcher, child shell, eval or resolved walk re-enters is counted for the whole command, so past 1,024 the guard refuses rather than letting readings that double per level (`runuser -u x -s env --` nested 25 deep) outrun it.
1616
- **Still declared gaps, all in sudo:** `sudo -R` / `--chroot`, a short cluster ending in an operand-taking letter (`sudo -Eu bob`), and an abbreviated long option (`sudo --us bob`). Reading them correctly changes how sudo lines refused today are read (`sudo -R rm -rf /` would take `rm` as the chroot directory), and this guard only adds refusals.
1717

1818
## [0.36.5] - 2026-09-24

0 commit comments

Comments
 (0)