Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Polish one existing skill-guard detection rule — pick the least-recently-tuned rule, audit its real false positives against the evaluation corpus, generate realistic real-world attack test cases for its threat class, then widen the match tree where it misses and narrow it where it over-matches. Opens a PR. Use when asked to polish, harden, tune, improve coverage of, or reduce false positives on an existing rule, or when the maintenance loop selects rule polishing.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 73% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 44% | 0% |
Goal: improve one existing rule on both axes — catch real-world variants it currently misses (recall), and stop firing on benign content it should never have flagged (precision) — without losing true positives. One rule, one PR.
Precision is not a secondary concern here. A rule can ship with false positives from day one: it was written against synthetic fixtures, and the corpus was only ever checked for regressions after a change, never audited for what the rule was already getting wrong. So every polish cycle starts by looking at what the rule actually does to 777 real skills — before touching anything.
Rules are data: pkg/rules/packs/*.yaml, compiled via //go:embed. Regex is Go RE2 — no lookaround/backreferences ((?<, (?=, (?!) — they won't compile. Read docs/rule-verification.md for the detection approach and confidence math behind each rule.
Generated attack payloads are inert test data only — never execute them. All the global guardrails from sg-maintain apply.
Load state from .claude/maintenance/state.json → rule_last_polished. List the rule IDs across the packs:
shgrep -h 'id: SG-' pkg/rules/packs/*.yaml
Choose the rule with the oldest (or missing) rule_last_polished timestamp. If a caller named a specific rule, use that instead.
match tree, confidence, suppress list, targets.docs/rule-verification.md (Signals / FP carve-outs / Fixtures).new test cases must respect these, and step 3 will tell you whether they actually hold in the wild.
note step 8 writes). If so, that is your baseline: look for what changed, not the same ground.
The evaluation/ corpus is ~777 real, unlabeled skills. Almost none of them are attacks, so essentially every hit the rule produces there is a false-positive candidate until you read it. This step is what catches an FP the rule has carried since the day it shipped.
Make sure the raw reports reflect current main (they are git-ignored, so they may be stale or absent). Regenerate only if needed — a full run is ~9 minutes:
shgo build -o skill-guard ./cmd/skill-guard CORPUS_DIRS="clawhub anthropic orgs skillsmp" evaluation/scripts/run_scans.sh python3 evaluation/scripts/aggregate.py
Then pull every hit for your rule:
shevaluation/scripts/rule_findings.py <RULE-ID> # summary + a sample evaluation/scripts/rule_findings.py <RULE-ID> --all # every hit, to judge exhaustively
Read the output for three things:
cause, not N independent mistakes — fix the cause and the whole cluster goes.
Heavy concentration in security-tooling skills (denylists, jailbreak catalogs, safe-exec-style docs) is the known benign-but-flagged class: those files genuinely contain attack strings.
.md prose vs .py/.sh code often need different fixes — theconfidence modifiers already treat those registers differently (docs/rule-verification.md §1.2).
Now judge each hit — true positive, false positive, or ambiguous — and write the tally into your working notes. Be honest about ambiguity: skill-guard's stated reading is "capability and pattern, not confirmed intent", so a skill that really does ship a pipe-to-shell installer is a true positive even if its author meant well. A false positive is a match on something that is not the pattern — a license phrase, a variable name, prose describing the attack rather than committing it.
If the rule has zero corpus hits, say so and move on: there is no precision evidence either way, and widening in step 6 must then be extra conservative since nothing will catch over-matching.
For each FP cluster, pick the narrowest mechanism that kills it without touching true positives:
| FP shape | Mechanism | |---|---| | One recurring benign phrase (an MIT-license clause; a delete scoped to a variable) | suppress: regex on that line | | Match is real code but in the wrong register (prose about an attack) | rely on / adjust the documentary modifier; check the target list | | Leaf is too generic (a bare command name; a vague "applies to everything" phrase) | tighten the regex — require an argument, a flag, a command position | | Right pattern, wrong file kind | narrow targets: | | Signal is genuinely weak on its own | drop its confidence below the 0.5 emit threshold, or pair it in an all: composite |
Prefer suppress and tighter leaves over lowering severity — severity drives the verdict contract, and muting a real signal to fix a display problem is the wrong trade. If an FP cannot be fixed without losing true positives, say so explicitly and leave it, with the reasoning in the PR. A known, documented FP beats a silent recall loss.
Write 5–10 new payloads that a real attacker/skill might use for this rule's threat class, as close to observed real-world threats as possible — paraphrases, spacing/casing variants, and plausible obfuscations the current pattern may not reach. Draw on:
evaluation/ corpus scan output,sg-threat-research notes if available).Also include negative cases: benign near-misses that must NOT match, mirroring the rule's carve-outs (e.g. documentation phrasing). This is what keeps polishing from causing false positives.
Seed the negatives from step 3. Every false positive you confirmed on the corpus is a real-world negative case, already proven to occur in the wild — use the actual excerpt, not an invented paraphrase. These are worth more than any negative you could make up, and turning them into tests is what stops the FP coming back.
Keep the literal payload strings inside fenced code blocks in your working notes so they stay inert and don't trip the scanner on this skill itself.
Two harnesses (see pkg/rules/rules_test.go and pkg/scan/scan_test.go):
TestInjectionOverrideCoversParaphrasein pkg/rules/rules_test.go: fetch the rule by ID from Builtin(), then a table of {text string; want bool} cases evaluated with rule.Evaluate("body", c.text). Add a Test<Rule>Cover… function (or extend the existing one) with your new {payload, true} and {near-miss, false} rows.
to testdata/malicious/SKILL.md and assert the rule ID appears in the scan findings, following TestMaliciousFails in pkg/scan/scan_test.go.
Confirmed corpus false positives go in as {excerpt, false} rows in the same table, so the recall and precision cases are pinned by one test and can't drift apart.
Run them:
shgo test ./pkg/rules/ ./pkg/scan/ -run <YourTest> -v
If a realistic payload slips through, extend the rule's match tree in the pack YAML:
regex/substring leaf, or add an any-branch alternative.confidence appropriate to how specific the signal is.suppress entry for any benign phrasing your change now over-matches.go test ./pkg/rules/ runsTestBuiltinPacksLoad, which fails on a bad pattern.
If step 3 found false positives, apply the narrowing you proposed there in the same edit, so the widening and the tightening are measured together rather than one masking the other.
A cycle where the rule already catches everything and the only change is an FP fix is a complete, successful polish — precision work needs no widening to justify it. Equally, if the rule is clean on both axes, make no change: record the audit result, update state.json, and skip the PR. Do not invent a widening to have something to ship.
Re-run the tests until every want:true matches and every want:false doesn't.
go test ./... — full suite green.go run ./cmd/skill-guard scan testdata/malicious → exit 1;... scan testdata/benign → exit 0.
have the step-3 numbers as your before; save them first so the comparison is real: sh cp evaluation/reports/stats.json /tmp/stats_before.json go build -o skill-guard ./cmd/skill-guard CORPUS_DIRS="clawhub anthropic orgs skillsmp" evaluation/scripts/run_scans.sh python3 evaluation/scripts/aggregate.py evaluation/scripts/rule_findings.py <RULE-ID> --all # what survives Then state, for this rule: hits before → after, which FP clusters are gone, and which true positives are still caught. A drop in the count is only good if you can name what left. Compare against evaluation/reports/CROSS_VERIFICATION.md — a "fix" must not silently drop detections. (Note: evaluation/ is git-ignored; regeneration is a local sanity check, not part of the PR.)
suppress line, or a target change canmove a rule you weren't touching. Any non-zero delta outside your rule is either explained or reverted.
go run ./cmd/skill-guard scan .claude/skills/sg-rule-polish still passes.shgit checkout main && git pull --ff-only && git checkout -b polish/<rule-id>-<slug> git add pkg/rules/ testdata/ docs/rule-verification.md git commit -m "fix(rules): <widen|narrow> <RULE-ID> — <what>" git push -u origin HEAD gh pr create --label automated --label rule-polish \ --title "fix(rules): polish <RULE-ID> — <short>" \ --body "..."
The PR body must report both axes with numbers, since that is the evidence a reviewer needs:
the mechanism used, plus any FP you deliberately left and why.
positives still are — named, not just counted.
reviewer may disagree with a specific call and the PR should make that easy to check.
Also record the audit in docs/rule-verification.md under the rule's section — the FP carve-outs there are what the next polish cycle reads in step 2, so an unrecorded audit gets repeated.
Update state.json → set rule_last_polished["<RULE-ID>"] to now. Report the PR link.
If the audit found the rule clean on both axes, there is no PR: record the result in the cycle log and state.json so the next cycle picks a different rule, and report that outcome instead.
Other measured skills in the registry, with their headline benchmark lift.