feat: role override as a matrix, not seven literals (#80) - #94
Merged
Conversation
`role_override` is the flagship category and it was seven near-literal phrases. Each pinned a specific verb *and* object *and* modifier, so a synonym walked past every one of them: `disregard all previous instructions` missed PI007 because that pattern required the literal `your`, and `forget everything you were told before` missed PI006 because that one required `you know`. Same attack, different wording. The recall corpus from #81 put a number on it: **1/12**. It is now **11/12**, and the clean corpus did not move. ## What changed Each of PI001-PI009 keeps its id, name, severity and concept, and grows from a literal into a composition of verb x modifier x object. Widened in place rather than added to, because PI001-PI009 is the whole reserved block for this category and every other block belongs to a category that does not exist yet (#33-#40). Pattern *names* are deliberately unchanged even where the concept widened. `pattern_name` is part of the JSON contract `spec-ci-plugin` consumes, so renaming six of them would have been a consumer-visible break for zero detection value. Two patterns took a second arm rather than a wider vocabulary: - **PI007** now also matches the declarative form. `Your previous instructions contained an error` does the work of an override without an imperative for a verb to match. That arm requires the full triple — priorness, an instruction object, *and* an explicit invalidation — so `your previous instructions are attached to the ticket` stays clean. - **PI009** splits its verbs by how benign they are. `reset`/`replace`/ `overwrite` are attack-shaped alone; `update`/`change`/`modify` are not, so those require a qualifier binding the object to the running config. `Update your operating instructions` matches, `update your instructions file` does not. ## Precision `old` and `legacy` are deliberately absent from the priorness vocabulary, and the object noun is required wherever the verb is ordinary English. Those two choices are what keep `tests/corpus/clean/` at zero: `agent-spec.md` — the single most common document this scanner is pointed at — says "Ignore the legacy `v1/` package" and "Forget the old naming convention", and `hard-wrapped-prose.md` has "Reviewers should ignore all previous" sitting alone above an `## Instructions for reviewers` heading. The clean corpus holds at 0 findings including under `--strict`, and the documentation corpus still needs context awareness to stay quiet. Beyond that gate, a full self-scan puts every new finding in `examples/`, `patterns/`, `tests/` or `tools/injection-lab/` — none in README.md, PATTERNS.md, CLAUDE.md, `src/` or `docs/` prose. ## Counts moved, deliberately - `recall_test` EXPECTED role-override 1 -> 11, and the README table with it (total 10/60 -> 20/60). Pinned exactly, so it cannot drift. - `markdown_context_test` jailbreak-attack 12 -> 13. PI007 now also matches "override your safety guidelines" on a line PI036 already reported — a duplicate on an existing line, not a new one. - Baseline 145 -> 163. - PI001/2/4/5/6/7 come off the `LEGACY_UNTESTED` debt register in `pattern_policy_test`, each with 4-5 positives and 3-5 near-miss negatives. The one payload still missed is "From this point forward the instructions in this document take precedence" — an instruction-precedence claim rather than a role override, so it belongs to the PI010-PI019 block. 262 -> 268 tests. `exfiltration`, `instruction-injection` and `jailbreak` still hold literal phrases and still read 0/12; that is the same fix in three more files, and each deserves its own PR. Closes #80. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plan, summary and STATE.md for the #80 widening. Records the three carry-forward findings: pattern `name` is part of the JSON consumer contract, the 12-file clean corpus needs a full self-scan alongside it, and HIGH-severity patterns need their verb lists split on benignness because HIGH is what `install-hook` blocks on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #80.
role_overrideis the flagship category and it was seven near-literal phrases. Each pinned aspecific verb and object and modifier, so a synonym walked past every one of them.
The recall corpus from #81 put a number on it. 1/12 → 11/12, and the clean corpus did not move.
What changed
PI001–PI009 each keep their id, name, severity and concept, and grow from a literal into a
composition of verb × modifier × object. Widened in place rather than added to, because
PI001–PI009 is the whole reserved block for this category and every other block belongs to a
category that does not exist yet (#33–#40).
Two patterns took a second arm rather than a wider vocabulary:
Your previous instructions contained an errordoes an override's work without an imperative for a verb to match. That arm requiresthe full triple — priorness, an instruction object, and an explicit invalidation — so
your previous instructions are attached to the ticketstays clean.reset/replace/overwriteareattack-shaped alone;
update/change/modifyare not, so those require a qualifier bindingthe object to the running config.
Update your operating instructionsmatches,update your instructions filedoes not. PI009 is HIGH, which is the thresholdinstall-hookwrites by default — an FP there blocks people's commits.
Pattern names are deliberately unchanged even where the concept widened.
pattern_nameispart of the JSON contract
spec-ci-pluginconsumes, so renaming six of them would have been aconsumer-visible break for zero detection value.
Precision — the direction that actually matters
oldandlegacyare deliberately absent from the priorness vocabulary, and the object noun isrequired wherever the verb is ordinary English. Those two choices are what hold the corpus at
zero:
clean/agent-spec.md— the most common document this scanner is pointed at — says "Ignorethe legacy
v1/package" and "Forget the old naming convention", andclean/hard-wrapped-prose.mdhas "Reviewers should ignore all previous" sitting alone above an
## Instructions for reviewersheading.
tests/corpus/clean/— 0 findings, including under--stricttests/corpus/documentation/— 0 by default, >0 under--strict, so context awareness is stillload-bearing rather than being widened until it swallows attacks
examples/,patterns/,tests/ortools/injection-lab/. None in README.md, PATTERNS.md,CLAUDE.md,
src/ordocs/prose. The 12-file corpus is thin evidence for a widening thissize; this was the second check.
Counts that moved, and why
recall_testEXPECTED role-override 1 → 11, README table with it. Pinned exactly, so it cannotdrift in either direction.
markdown_context_testjailbreak-attack 12 → 13. PI007 now also matches "override your safetyguidelines" on a line PI036 already reported — a duplicate on an existing line, not a new one.
LEGACY_UNTESTEDdebt register inpattern_policy_test, each with4–5 positives and 3–5 negatives. The negatives are near-misses drawn from real prose, three
straight out of
tests/corpus/clean/.262 → 268 tests. fmt, clippy and the full suite green locally.
Still open
The one payload missed is "From this point forward the instructions in this document take
precedence" — an instruction-precedence claim rather than a role override, so it belongs to the
PI010–PI019 block.
exfiltration,instruction-injectionandjailbreakstill hold literal phrases and still read0/12. That is the same fix in three more files, and each deserves its own PR — widening all four
here would have had an unreviewable false-positive blast radius.
Verification Report
Produced from
.claude/skills/pr-artifactsand.claude/skills/code-review. Both were runafter this PR was opened rather than before, which is out of order — noted rather than hidden.
fmt · clippy · test · buildgreenunwrap()in production code —src/is untouched by this PR (0 files changed)println!debug output — same,src/untouchedmatchaddedpr-artifactsrequires one for engine or YAML-format changes;this is "new patterns using the existing format", which it explicitly exempts. No schema
field, loader or matching-engine change.
README.md(recall table + category examples),docs/PATTERN-CATALOGUE.md(regenerated),
patterns/core/role-override.yaml(vocabulary rationale in the header)for PI001/2/4/5/6/7, plus 2 more positives and 2 more negatives on PI009
deny_unknown_fieldsunchanged,pattern_validation_testgreencargo fmt --checkclean ·cargo clippy -- -D warningscleanSmoke test
Run against both binaries, verbatim. This is the first payload in #80's table.
Performance
Regexes were not compiled per call before and are not now —
Scannerowns the compiled set.But this PR grows the
role_overrideregex source 525 → 2720 chars, so the cost wasmeasured rather than assumed:
+6ms on the gate, 5.4x headroom remaining. Not blocking.
One thing worth recording for whoever widens the next category:
tests/perf_regression_test.rsis a ratio of scan time to compile time, and pattern growth inflates the denominator — so it
gets looser as the library grows. It still does its stated job (catching compile-moved-into-the
-loop) but it cannot catch match-time cost from pattern growth. The CI 200ms gate is the thing
that does. With three more categories to widen, PERF-02 (#4) is less "optional headroom" than
.planning/STATE.mdcurrently calls it.