fix(state-line-guard): Edit path used String.replace, not a function
current.replace(oldStr, newStr) with newStr as a STRING lets JS treat $-sequences inside it ($&, $`, $', $$, $n) as special replacement patterns, even though oldStr (the search side) is a plain string. A new_string documenting old backtick-substitution style ($`cmd`) - the kind of prose a STATE.md shell-conventions section writes routinely - triggers it. Measured against the real bug (.claude/STATE.md, 2026-08-15): a 5-line addition on a 112-line file projected to 219 lines and was wrongly denied. Fix: current.replace(oldStr, () => newStr) - a function replacement is never pattern-substituted, covering every $-sequence at once. The replace_all branch (split/join) was never affected. Direction was always fail-closed (over-blocks, never under-blocks a real oversize), but it made exactly the STATE.md files that document shell conventions hard to edit via Edit. state-line-guard-selftest.sh: 23/23 (+2, section 9: $` as the repro, $& as a second sequence proving the fix is general). Also updates CLAUDE.md's pinned selftest counts (197/178/69/21 were already stale before this session's own additions; now 206/183/69/23).
This commit is contained in:
parent
bd24b8f0e7
commit
b5c860eb03
3 changed files with 104 additions and 6 deletions
|
|
@ -290,6 +290,70 @@ process.stdout.write(JSON.stringify({
|
|||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: shrinking an oversized file allows, even if still over the limit" $?
|
||||
|
||||
# --- 9. Edit: new_string is treated LITERALLY, never as a String.replace ----
|
||||
# special-pattern ($&, $`, $', $$, $n). Line 117 used to call
|
||||
# current.replace(oldStr, newStr) with newStr as a STRING: JavaScript then
|
||||
# interprets $-sequences inside the REPLACEMENT as special patterns even
|
||||
# though the SEARCH side (oldStr) is a plain string, not a RegExp. A
|
||||
# new_string documenting old backtick-substitution style ($`cmd`) is exactly
|
||||
# the kind of prose a STATE.md's shell-conventions section writes routinely.
|
||||
# Measured against the real bug (.claude/STATE.md, 2026-08-15): a 5-line
|
||||
# addition on a 112-line file projected to 219 lines and was wrongly denied.
|
||||
# Fix: current.replace(oldStr, () => newStr) - a function replacement is
|
||||
# never pattern-substituted, so this covers every $-sequence, not just $`.
|
||||
|
||||
FIXTURE5="$TMPDIR/e"
|
||||
mkdir -p "$FIXTURE5"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const content = "p\n".repeat(100) + "TARGET\n" + "q\n".repeat(11);
|
||||
fs.writeFileSync(process.argv[1], content);
|
||||
' "$FIXTURE5/STATE.md"
|
||||
# 112 lines total (100 + 1 + 11), matching the real repro's file size.
|
||||
|
||||
export STATE_GUARD_FIXTURE5="$FIXTURE5/STATE.md"
|
||||
P="$(payload '
|
||||
const path = process.env.STATE_GUARD_FIXTURE5;
|
||||
const oldStr = "TARGET\n";
|
||||
const newStr = "TARGET\n" +
|
||||
"avoid old backtick-substitution style: $`cmd` (use $(cmd) instead)\n" +
|
||||
"line2\n" + "line3\n" + "line4\n" + "line5\n";
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Edit",
|
||||
tool_input: { file_path: path, old_string: oldStr, new_string: newStr }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
# Real net change is +5 lines (112 -> 117): under MAX_LINES, must allow. A
|
||||
# dollar-pattern-vulnerable replace() balloons this past 120 and wrongly denies.
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: new_string containing \$\` is treated literally, not pattern-substituted (allows a real +5-line edit)" $?
|
||||
unset STATE_GUARD_FIXTURE5
|
||||
|
||||
FIXTURE6="$TMPDIR/f"
|
||||
mkdir -p "$FIXTURE6"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const content = "p\n".repeat(100) + "TARGET\n" + "q\n".repeat(11);
|
||||
fs.writeFileSync(process.argv[1], content);
|
||||
' "$FIXTURE6/STATE.md"
|
||||
|
||||
export STATE_GUARD_FIXTURE6="$FIXTURE6/STATE.md"
|
||||
P="$(payload '
|
||||
const path = process.env.STATE_GUARD_FIXTURE6;
|
||||
const oldStr = "TARGET\n";
|
||||
const newStr = "TARGET line, matched text follows: $& -- end\n" +
|
||||
"line2\n" + "line3\n" + "line4\n" + "line5\n";
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Edit",
|
||||
tool_input: { file_path: path, old_string: oldStr, new_string: newStr }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
# Same class, different special sequence ($& = the whole matched substring):
|
||||
# proves the fix is general (a function replacement), not a $`-specific patch.
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: new_string containing \$& is also treated literally (fix is general, not backtick-specific)" $?
|
||||
unset STATE_GUARD_FIXTURE6
|
||||
|
||||
echo ""
|
||||
echo "state-line-guard-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue