ruleprobe by Jake Selby v0.2.0
GitHub

How good are the detectors?

A hit rate is a rate of the detector until somebody says what the detector should have found. So a labelled corpus ships inside the package, at ruleprobe/corpus/: ten synthetic sessions in all three transcript shapes, every interesting event labelled by hand with the detectors that ought to fire on it, and a deliberate near-miss beside each one - a cat of a line range, a find narrowed by -name, a git push after the gate ran, a heredoc with rm -rf in its body as text rather than as a command.

ruleprobe corpus
detector                                pos  neg   tp   fp   fn   prec  recall     f1  note
-------------------------------------------------------------------------------------------
cache-hygiene/compact                     5    6    5    0    0   1.00    1.00   1.00
cache-hygiene/model-switch                5   15    5    0    0   1.00    1.00   1.00
commits/non-conventional-subject          5    9    5    0    0   1.00    1.00   1.00
git-safety/force-push-default             5    7    5    0    0   1.00    1.00   1.00
package-manager/pip-install               5    7    5    0    0   1.00    1.00   1.00
secrets/secret-file-add                   5    6    5    0    0   1.00    1.00   1.00
secrets/secret-in-write                  15   13   15    0    0   1.00    1.00   1.00
testing/test-after-change                11    6   11    0    0   1.00    1.00   1.00
transcript-hygiene/unfiltered-find       10   15   10    0    0   1.00    1.00   1.00
transcript-hygiene/whole-file-cat        10   14   10    0    0   1.00    1.00   1.00
verification/no-verify                   11   13   11    0    0   1.00    1.00   1.00
-------------------------------------------------------------------------------------------
total                                    87  111   87    0    0   1.00    1.00   1.00  floor 0.90

The six shipped detectors and every catalog entry are scored over the corpus. A detector the corpus labels is scored by those labels alone, so a catalog entry’s own examples: score its row only when the corpus labels none of it, and the row’s source in --json then says examples. The test suite runs every restating entry’s examples against the shipped detector, so they stay true of it.

pos and neg are what the labels asked for; tp, fp and fn are what happened. ruleprobe corpus --floor 0.9 exits non-zero when a scored detector falls under the floor, and CI in this repository runs exactly that. It is a gate on the repository, not on a run: nothing in ruleprobe report reads the floor, and no report of yours will ever fail because a detector scored badly. --json prints the same numbers as data.

ruleprobe report --validity puts each detector’s p= and r= beside its row. It is off by default because the report is meant to be read in a minute and an eight-column table is not, and because the same two numbers apply to every run - they belong to the detector, not to your transcripts.

Read the number for what it is. The corpus is synthetic and hand-labelled: no real transcript content, no home paths, no personal names. A 1.00 says the detector is right about the shapes somebody thought to write down, which is a weaker claim than it looks - the false positives a detector meets in the wild are the ones nobody anticipated. It is a floor under an obvious mistake and a place to put the next surprising transcript, not a measured field accuracy. Growing it is the cheapest contribution this repository takes: add a session under ruleprobe/corpus/sessions/, label it in ruleprobe/corpus/labels.yaml, and the table above moves.

A wrong hit of yours becomes a labelled negative. When ruleprobe explain shows a hit that should not have fired, ruleprobe label records it in a corpus directory you name:

mkdir -p ~/ruleprobe-corpus
ruleprobe label --session claude-code:sess-1 --detector verification/no-verify \
                --key 1:toolu_2 --corpus ~/ruleprobe-corpus --name no-verify-in-a-message
ruleprobe corpus --corpus ~/ruleprobe-corpus

It reruns the detector over your transcripts, takes the one event behind the hit, and writes it, redacted, as sessions/<name>.events.jsonl - one event per line, read back without a runtime reader - with one near label for it in labels.yaml, which it creates or appends to so a hand-written file keeps its comments. It is the only command that writes, and it writes nothing outside that directory. It refuses, says why and writes nothing when the hit is a session hit (<turn>:-), which one event cannot reproduce; when the written event no longer produces the hit, because redaction changed what the detector matched or the hit needs the events around it, since that negative would pass trivially; when a secret shape would survive into the written bytes; and when the session file exists already or labels.yaml would not read back as before plus one entry. It takes report’s --root, --runtime and --since, and --rules, --detectors and --no-config; --name is a plain file stem. It takes no --stance, because a detector gated on one is refused, since the corpus runs with none, and no --plugins, because ruleprobe corpus loads none to score the negative with. A Claude Code subagent’s own transcript is its own session, keyed as <parent session id>/<agent id>, and a transcript that yields no event is no session at all. Transcripts that share a session id, as copies do, are one session in report, explain and label: the one with the most events is read, the first in path order on a tie, and the rest are reported as copies set aside. When the session read has no hit at the key and copies of it were set aside, the refusal names them with their folders, so --root can point at the one that holds the hit. ruleprobe corpus is the exception: a label names its session by file, so it reads each labelled file as its own session, copies included. It prints the ruleprobe corpus command that scores the new negative, with the same --rules, --detectors and --no-config it was given. The new negative scores as a false positive until the detector stops firing on it, which is the point.

In a corpus’s sessions/, the suffix .events.jsonl is reserved: a file ending in it is read as one event per line, never through a runtime reader, so a native transcript must not be named that way.

A detector of your own scores itself. Rather than a corpus, a declarative detector may carry an examples: block of minimal cases, and ruleprobe corpus --rules ./docs/rules scores those:

- id: house-style/sudo-install
  rule: house-style
  event: tool_use
  when:
    command: {starts_with: [sudo, pip]}
  examples:
    fire:
      - bash: sudo pip install ruff
    skip:
      - bash: uv pip install ruff
        note: the tool the rule asks for

fire is a list of cases the detector should fire on and skip a list it should not. A case is bash: <command>, or event: <one event>, or events: [...] for a session detector, with an optional note. Nothing runs them at report time. A detector with no examples and no corpus label prints no examples rather than a number, and the floor steps over it: an unmeasured detector is a gap to see, not a failure to fix.

↑↓ move↵ openPagefind · indexed at build