From 3b96d37c920d8d7ba4d44e5306a0e10d5068840a Mon Sep 17 00:00:00 2001 From: KilimcininKorOglu <229108989+KilimcininKorOglu@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:13:04 +0300 Subject: [PATCH 1/3] fix(hooks): the command-word lexer cost the cube of the line, 20 s for 4,000 characters rw_is_ripwire_call rebuilt the rest of the line for every character it read, so one long Bash command held the PreToolUse hook for minutes: 2.9 s at 2,000 characters, 20.6 s at 4,000, 155 s at 8,000 under macOS bash 3.2 (bash 5.3: 1.7 s, 12.4 s, 93.6 s). The meter runs it on every Bash call even with the nudge text retired. Two guards now come first, in all three copies of the mirrored block. A line without the word holds no call; that is exact and ends the scan for nearly every command. A line over 1,024 characters is not scanned and reads as no call, a missed call, the same direction as the 2>&1 limit the block already discloses. routehookcheck O10 holds both: two 4,000-character lines, 20 s each and one wrong answer on the old block, 0 s and correct now. Fixes #327 --- hooks/ripwire-claude-route.sh | 9 +++++++++ hooks/ripwire-codex-route.sh | 9 +++++++++ hooks/ripwire-nudge.sh | 9 +++++++++ test/routehookcheck.sh | 25 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index dc8d79191..af3924876 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -138,6 +138,13 @@ resolve_arm() # control-operator branch, so the digit behind it is read as a command word. That can only ever cost a # MISSED call, in a line where `ripwire` sits in exactly that position, and never a false one. # +# COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match +# around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS +# bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the +# word holds no call, which is exact and ends the scan for nearly every command. A line longer than 1,024 +# characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024 +# costs under half a second at worst. test/routehookcheck.sh O10 holds both. +# # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`. rw_cmd_word() { @@ -171,6 +178,8 @@ rw_cmd_word() rw_is_ripwire_call() { + case "$1" in *ripwire*) ;; *) return 1 ;; esac + [ "${#1}" -le 1024 ] || return 1 rw_nl=' ' rw_tab="$( printf '\t' )" diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index 4764f28ad..0c9e34d0b 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -61,6 +61,13 @@ hash_text() # control-operator branch, so the digit behind it is read as a command word. That can only ever cost a # MISSED call, in a line where `ripwire` sits in exactly that position, and never a false one. # +# COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match +# around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS +# bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the +# word holds no call, which is exact and ends the scan for nearly every command. A line longer than 1,024 +# characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024 +# costs under half a second at worst. test/routehookcheck.sh O10 holds both. +# # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`. rw_cmd_word() { @@ -94,6 +101,8 @@ rw_cmd_word() rw_is_ripwire_call() { + case "$1" in *ripwire*) ;; *) return 1 ;; esac + [ "${#1}" -le 1024 ] || return 1 rw_nl=' ' rw_tab="$( printf '\t' )" diff --git a/hooks/ripwire-nudge.sh b/hooks/ripwire-nudge.sh index 49cea7003..c438374d6 100755 --- a/hooks/ripwire-nudge.sh +++ b/hooks/ripwire-nudge.sh @@ -566,6 +566,13 @@ meter_arg1="" # control-operator branch, so the digit behind it is read as a command word. That can only ever cost a # MISSED call, in a line where `ripwire` sits in exactly that position, and never a false one. # +# COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match +# around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS +# bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the +# word holds no call, which is exact and ends the scan for nearly every command. A line longer than 1,024 +# characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024 +# costs under half a second at worst. test/routehookcheck.sh O10 holds both. +# # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`. rw_cmd_word() { @@ -599,6 +606,8 @@ rw_cmd_word() rw_is_ripwire_call() { + case "$1" in *ripwire*) ;; *) return 1 ;; esac + [ "${#1}" -le 1024 ] || return 1 rw_nl=' ' rw_tab="$( printf '\t' )" diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index 18511d9f7..199e71bac 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -682,5 +682,30 @@ o9 0 'echo hi > ripwire' && ok "O9 command-word rule: $o9_n shapes read correctly (19 wrapped/sequenced/operator-attached calls, 9 appearances that run nothing)" \ || no "O9 command-word rule: $o9_bad of $o9_n shapes read WRONG (listed above)" +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# O10 — the rule's cost does not grow with the command line (issue #327) +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# The lexer rebuilds the rest of the line for every character it reads, so one long command cost +# 2.9 s at 2,000 characters, 20.6 s at 4,000 and 155 s at 8,000 under macOS bash 3.2 — inside the +# PreToolUse hook, in front of the Bash call it was only meant to count. RED on the pre-fix block, +# measured while writing this: each line below took about 20 s. The answers are asserted too: a line +# with no `ripwire` in it holds no call, and a line past the cap reads as none — a missed call, +# never a false one, the same direction as the `2>&1` limit the block already discloses. +echo +echo "=== O10: a long command line costs the rule nothing ===" +o10_long="$( head -c 4000 /dev/zero | tr '\0' 'a' | fold -w 60 | tr '\n' ' ' )" +for o10_line in "$o10_long" "echo $o10_long; ripwire ." +do + o10_t0="$( date +%s )" + o10_got="$( sh "$TMP/rule.sh" "$o10_line" 2>/dev/null )" + o10_dt=$(( $( date +%s ) - o10_t0 )) + case "$o10_line" in *ripwire*) o10_what="holding a call past the cap" ;; *) o10_what="without ripwire" ;; esac + if [ "$o10_dt" -le 2 ] && [ "$o10_got" = "0" ]; then + ok "O10 a ${#o10_line}-character line $o10_what answers 0 in ${o10_dt} s" + else + no "O10 a ${#o10_line}-character line $o10_what took ${o10_dt} s and answered [$o10_got] (want 0 within 2 s)" + fi +done + echo if [ "$fail" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "SOME CHECKS FAILED"; exit 1; fi From bb04971e39552257cd16f849234ff25fb8658f82 Mon Sep 17 00:00:00 2001 From: KilimcininKorOglu <229108989+KilimcininKorOglu@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:59:26 +0300 Subject: [PATCH 2/3] fix(hooks): route a prompt only inside a git work tree Outside a git work tree `--help-task` has no file list from git and walks the whole tree under cwd. A session started in $HOME measured over 30 s for one prompt, past the 8 s UserPromptSubmit timeout, so Claude Code discarded the hook and printed a timeout warning on every prompt. Both prompt routers now exit before the classifier when `git rev-parse --is-inside-work-tree` does not answer. routehookcheck.sh O11 puts a stub ripwire on PATH that records each call: a prompt in a non-git cwd must not reach it, and a prompt in the fixture repo must (the positive control). RED on the pre-fix hook. codexpromptroutecheck.sh built its fixture repo as an empty .git directory, which git does not accept; it now runs `git init`. Refs #327 --- hooks/ripwire-claude-route.sh | 5 +++++ hooks/ripwire-codex-route.sh | 5 +++++ test/codexpromptroutecheck.sh | 4 +++- test/routehookcheck.sh | 27 +++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index af3924876..77209192b 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -336,6 +336,11 @@ command -v ripwire >/dev/null 2>&1 || exit 0 prompt="$( printf '%s' "$input" | jq -r '.prompt // .user_prompt // .input // empty' 2>/dev/null )" cwd="$( printf '%s' "$input" | jq -r '.cwd // .workdir // empty' 2>/dev/null )" [ -n "$prompt" ] && [ -n "$cwd" ] && [ -d "$cwd" ] || exit 0 +# Route only inside a git work tree (issue #327). Outside one `--help-task` has no file list from git and +# walks the whole tree under cwd: a session started in $HOME measured over 30 s for one prompt, past the +# 8 s hook timeout the installer registers, on every prompt. The cost is routing in a small non-git +# project too; a missed recommendation is the direction this hook already takes on every doubt. +git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 session="$( printf '%s' "$input" | jq -r '.session_id // .conversation_id // empty' 2>/dev/null )" # A very long prompt is a paste, not a task description, and `--help-task` is not built to read one. diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index 0c9e34d0b..10ae6b4cd 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -254,6 +254,11 @@ command -v ripwire >/dev/null 2>&1 || exit 0 prompt="$( printf '%s' "$input" | jq -r '.prompt // .user_prompt // .input // empty' 2>/dev/null )" cwd="$( printf '%s' "$input" | jq -r '.cwd // .workdir // empty' 2>/dev/null )" [ -n "$prompt" ] && [ -n "$cwd" ] && [ -d "$cwd" ] || exit 0 +# Route only inside a git work tree (issue #327). Outside one `--help-task` has no file list from git and +# walks the whole tree under cwd: a session started in $HOME measured over 30 s for one prompt, on every +# prompt. The cost is routing in a small non-git project too; a missed recommendation is the direction +# this hook already takes on every doubt. +git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 session="$( printf '%s' "$input" | jq -r '.session_id // .conversation_id // empty' 2>/dev/null )" promptBytes="$( printf '%s' "$prompt" | wc -c | tr -d ' ' )" diff --git a/test/codexpromptroutecheck.sh b/test/codexpromptroutecheck.sh index c6f0ec124..d2b8f097c 100755 --- a/test/codexpromptroutecheck.sh +++ b/test/codexpromptroutecheck.sh @@ -18,7 +18,9 @@ command -v jq >/dev/null 2>&1 || { echo "jq required"; exit 2; } "$BIN" --help=all 2>&1 | grep -q -- '--help-task=' \ || { echo "codexpromptroutecheck: supplied binary does not expose --help-task"; exit 1; } -mkdir -p "$TMP/bin" "$TMP/repo/.git" "$TMP/home" +mkdir -p "$TMP/bin" "$TMP/repo" "$TMP/home" +# A real work tree, not an empty .git directory: the router routes only where `git rev-parse` answers. +git -C "$TMP/repo" init -q cat >"$TMP/bin/ripwire" <<'SH' #!/bin/sh case "$*" in diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index 199e71bac..f3ea37af0 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -707,5 +707,32 @@ do fi done +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# O11 — the prompt router does not classify outside a git work tree (issue #327) +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# Outside a git work tree `--help-task` has no file list from git and walks the whole tree under cwd. +# A session started in $HOME measured over 30 s for one prompt, past the 8 s hook timeout, on every +# prompt. A stub ripwire records each call, so the arm measures whether the hook CALLS the classifier, +# not how fast one machine's tree happens to walk. RED on the pre-fix hook: the non-repo prompt +# reached the stub. The repo prompt is the positive control: the stub is reachable, so an empty call +# log for the non-repo prompt is not a stub that never runs. +echo +echo "=== O11: no classifier call outside a git work tree ===" +O11BIN="$TMP/o11bin"; mkdir -p "$O11BIN" +O11LOG="$TMP/o11.calls" +printf '#!/bin/sh\nprintf "%%s\\n" "$*" >>"%s"\n' "$O11LOG" >"$O11BIN/ripwire" +chmod +x "$O11BIN/ripwire" +H11="$TMP/h11"; mkdir -p "$H11" +: >"$O11LOG" +route_run "$H11" "$O11BIN:$PATH" "$( promptjson o11a "$NONREPO" "$RECPROMPT" )" >/dev/null 2>&1 +[ -s "$O11LOG" ] \ + && no "O11 route: a prompt in a non-git cwd called ripwire: [$( tr '\n' ' ' <"$O11LOG" )]" \ + || ok "O11 route: a prompt in a non-git cwd never calls ripwire" +: >"$O11LOG" +route_run "$H11" "$O11BIN:$PATH" "$( promptjson o11b "$REPO" "$RECPROMPT" )" >/dev/null 2>&1 +[ -s "$O11LOG" ] \ + && ok "O11 route: a prompt in a git work tree still calls ripwire (O11's positive control)" \ + || no "O11 route: a prompt in a git work tree never reached the stub, so O11 proved nothing" + echo if [ "$fail" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "SOME CHECKS FAILED"; exit 1; fi From b69ce56309c61ef0314965e6aa09b505728fe51a Mon Sep 17 00:00:00 2001 From: KilimcininKorOglu <229108989+KilimcininKorOglu@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:31:15 +0300 Subject: [PATCH 3/3] docs(hooks): name the assembled-word miss of the substring guard, log both fixes The substring guard reads the raw line before quote removal, so a command word the shell assembles from fragments ('rip''wire' ., rip\wire ., "rip""wire" ., rip"wire" .) reads as no call; the lexer alone read each as one. The block's comment called the check exact. It now names the shape and its direction (a missed call, never a false one) in all three mirrored copies, and O9 pins the four shapes plus two whole quoted words that still read as calls. CHANGELOG gains an Unreleased section with both #327 fixes, and docs/SUBSTITUTION_METER.md names the guard's two skipped shapes under "Known undercount" and the prompt router's git-only population. Refs #327 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ docs/SUBSTITUTION_METER.md | 13 +++++++++++++ hooks/ripwire-claude-route.sh | 6 ++++-- hooks/ripwire-codex-route.sh | 6 ++++-- hooks/ripwire-nudge.sh | 6 ++++-- test/routehookcheck.sh | 12 +++++++++++- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e7bedef..e07949437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,34 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli --- +## [Unreleased] + +### Fixed — the hooks' command-word rule no longer holds a Bash call for minutes on a long line (#327) + +`rw_is_ripwire_call`, the one rule the three hooks share to decide whether a Bash line runs ripwire, rebuilt +the rest of the line for every character it read, so its cost grew with the cube of the line's length: 2.9 s at +2,000 characters, 20.6 s at 4,000 and 155 s at 8,000 under macOS bash 3.2. The PreToolUse meter runs it on every +Bash call, and one command carrying a heredoc held the call for 6 min 49 s. Two guards now run before the scan, +and both can only turn a call into a missed one, never create a false one: + +- A line that does not contain the literal `ripwire` holds no call. The check reads the raw text before quote + removal, so a command word the shell assembles from quoted or escaped fragments (`'rip''wire' .`, + `rip\wire .`) now reads as no call; the scan alone read it as one. +- A line longer than 1,024 characters is not scanned and reads as no call. + +For the substitution meter this is a numerator that can fall short on those two shapes +(`docs/SUBSTITUTION_METER.md`, "Known undercount"). `test/routehookcheck.sh` O10 holds the cost (4,000 +characters: 20 s before, 0 s after) and O9 pins the four assembled-word shapes. + +### Fixed — the prompt routers no longer time out on every prompt outside a git work tree (#327) + +`hooks/ripwire-claude-route.sh` and `hooks/ripwire-codex-route.sh` ran `--help-task` for every prompt. Outside +a git work tree it has no file list from git and walks the whole tree under `cwd`: a session started in `$HOME` +still ran after 30 s, past the 8 s hook timeout, so Claude Code discarded the hook and printed a timeout warning +on every prompt (0.08 s for the same prompt in a git repository). Both routers now exit before the classifier +when git does not place `cwd` inside a work tree. A small non-git project gets no recommendation and +writes no routing row either. `test/routehookcheck.sh` O11 holds it with a stub `ripwire` that records each call. + ## [0.6.2] — 2026-09-21 ### Added — Microsoft's `cl.exe` builds the tree, so both Windows front ends compile and both gate diff --git a/docs/SUBSTITUTION_METER.md b/docs/SUBSTITUTION_METER.md index 5395151e7..e0ad9475e 100644 --- a/docs/SUBSTITUTION_METER.md +++ b/docs/SUBSTITUTION_METER.md @@ -592,6 +592,13 @@ an unstated one makes it untrustworthy: payload, so `detail` can be shorter still — `class` is unaffected, and the row stays valid JSON. - **`nudge=gated`** rows come from a call outside a git repository or with no `ripwire` on `PATH`. They are counted; they were never nudgeable. +- **The command-word rule skips two shapes of Bash line** (issue #327). Its scan cost grows with the + cube of the line's length, so two guards run first. A line longer than 1,024 characters is not + scanned. A line that does not contain the literal `ripwire` is not scanned either, and that check + reads the raw text before quote removal, so a command word the shell assembles from fragments + (`'rip''wire' .`, `rip\wire .`) is not seen. Both shapes read as no ripwire call: the numerator can + only fall short, never gain a false call. `test/routehookcheck.sh` O9 pins the assembled words and + O10 the cost. The meter does not estimate around any of this. An unobserved call is absent, and absent is not zero — the same rule the rest of the tool's output follows. @@ -735,6 +742,12 @@ The **prompt router**'s unit is a prompt; its rows are `UserPromptSubmit` decisi and it is the instrument the band pre-registered in [`EVALS.md` §4](EVALS.md) ("Claude Code prompt router") is measured through. +Its population is prompts whose `cwd` is inside a git work tree (issue #327). Outside one, `--help-task` +has no file list from git and walks the whole tree under `cwd`; a session started in `$HOME` took over +30 s for one prompt, past the 8 s hook timeout, and the killed hook wrote no row. Both prompt routers +now exit before the classifier there. A small non-git tree used to finish in time and write a row; it +writes none now, so a readout that spans the change compares two populations. + Three things it shares with this meter, and one it does not: - **The same arm.** `hooks/ripwire-claude-route.sh` resolves `arm` by the rules `meter_init` applies — diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index 77209192b..5b1e5c625 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -141,9 +141,11 @@ resolve_arm() # COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match # around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS # bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the -# word holds no call, which is exact and ends the scan for nearly every command. A line longer than 1,024 +# word as written holds no call, and that check ends the scan for nearly every command. It reads the raw text, +# before quote removal, so a command word the shell assembles from quoted or escaped fragments (`'rip''wire'`, +# `rip\wire`, `"rip""wire"`) reads as no call: a MISSED call, never a false one. A line longer than 1,024 # characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024 -# costs under half a second at worst. test/routehookcheck.sh O10 holds both. +# costs under half a second at worst. test/routehookcheck.sh O10 holds the cost, O9 pins the assembled words. # # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`. rw_cmd_word() diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index 10ae6b4cd..68e18d803 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -64,9 +64,11 @@ hash_text() # COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match # around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS # bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the -# word holds no call, which is exact and ends the scan for nearly every command. A line longer than 1,024 +# word as written holds no call, and that check ends the scan for nearly every command. It reads the raw text, +# before quote removal, so a command word the shell assembles from quoted or escaped fragments (`'rip''wire'`, +# `rip\wire`, `"rip""wire"`) reads as no call: a MISSED call, never a false one. A line longer than 1,024 # characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024 -# costs under half a second at worst. test/routehookcheck.sh O10 holds both. +# costs under half a second at worst. test/routehookcheck.sh O10 holds the cost, O9 pins the assembled words. # # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`. rw_cmd_word() diff --git a/hooks/ripwire-nudge.sh b/hooks/ripwire-nudge.sh index c438374d6..566b326ca 100755 --- a/hooks/ripwire-nudge.sh +++ b/hooks/ripwire-nudge.sh @@ -569,9 +569,11 @@ meter_arg1="" # COST (issue #327). Each character read rebuilds the rest of the line (`${rw_line#?}`, and the suffix match # around it), so the scan grows with the cube of the line's length: 20 s for a 4,000-character line under macOS # bash 3.2, in front of the tool call it only counts. Two guards come first. A line that does not contain the -# word holds no call, which is exact and ends the scan for nearly every command. A line longer than 1,024 +# word as written holds no call, and that check ends the scan for nearly every command. It reads the raw text, +# before quote removal, so a command word the shell assembles from quoted or escaped fragments (`'rip''wire'`, +# `rip\wire`, `"rip""wire"`) reads as no call: a MISSED call, never a false one. A line longer than 1,024 # characters is not scanned and reads as no call — a MISSED call, the same direction as the limit above; 1,024 -# costs under half a second at worst. test/routehookcheck.sh O10 holds both. +# costs under half a second at worst. test/routehookcheck.sh O10 holds the cost, O9 pins the assembled words. # # POSIX sh only, no bashisms: routehookcheck.sh extracts this block and runs it under `sh`. rw_cmd_word() diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index f3ea37af0..cc32e1671 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -678,8 +678,18 @@ o9 0 "grep -r 'ripwire;' src/" o9 0 'echo "a; ripwire b"' o9 0 '# ripwire .' o9 0 'echo hi > ripwire' +# KNOWN LIMIT of the substring guard (issue #327), pinned so it cannot widen unnoticed. The guard tests the +# raw line for the literal `ripwire`, before quote removal, so a command word the shell ASSEMBLES from quoted +# or escaped fragments reads as no call. The lexer alone read each of these four as a call; they are missed +# calls, never false ones. Quote removal still applies when the word appears whole, which the last two hold. +o9 0 "'rip''wire' ." +o9 0 'rip\wire .' +o9 0 '"rip""wire" .' +o9 0 'rip"wire" .' +o9 1 '"ripwire" .' +o9 1 "'ripwire' ." [ "$o9_bad" -eq 0 ] \ - && ok "O9 command-word rule: $o9_n shapes read correctly (19 wrapped/sequenced/operator-attached calls, 9 appearances that run nothing)" \ + && ok "O9 command-word rule: $o9_n shapes read correctly (21 wrapped/sequenced/operator-attached/quoted calls, 9 appearances that run nothing, 4 assembled words the guard misses by design)" \ || no "O9 command-word rule: $o9_bad of $o9_n shapes read WRONG (listed above)" # ═══════════════════════════════════════════════════════════════════════════════════════════════════