ruleprobe by Jake Selby v0.1.0
GitHub
Open sourceMITPython 3.9+ Standard library only

Find out which of your agent rules actually fire.

You have written rules for your coding agent. A CLAUDE.md, an AGENTS.md, a house style your team argued over. You do not know whether any of them changed what the agent did. ruleprobe reads the transcripts your agent already wrote and counts the observable things that happened in them.

Sixty seconds

uvx ruleprobe report

That reads ~/.claude/projects/**/*.jsonl (and ~/.codex/sessions/**/*.jsonl, if Codex wrote any), runs the detectors over them, and prints:

detector                                 hits  sessions    of   share  note
---------------------------------------------------------------------------
cache-hygiene/compact                       0         0   339       0%  unobserved
cache-hygiene/model-switch                 11        11   339       3%
secrets/secret-in-write                     3         2   339       1%
transcript-hygiene/unfiltered-find          0         0   339       0%  unobserved
transcript-hygiene/whole-file-cat          89        22   339       6%
verification/no-verify                      0         0   339       0%  unobserved

That is a real run, over one week of one person’s transcripts. Two detectors have never fired on this machine, which is itself the finding: unfiltered-find and no-verify are guarding against something that is not happening, and the rules behind them are paying rent in the context window for nothing.

share is the fraction of sessions the detector fired in at least once. unobserved means it has never fired in the window. promote?, above a 30 percent share, means it is common enough that either the rule is worth stating more loudly or the rule is wrong. Both notes stay blank until there are twenty measured sessions, because a share over five sessions is noise.

Nothing is sent anywhere, no model is asked anything, nothing is written to disk, and the same transcript gives the same answer every time. Python 3.9 or newer, standard library only.

Other groupings, and a window:

uvx ruleprobe report --by repo --since 30      # last 30 days, one line per repository
uvx ruleprobe report --by stance --stance commits=conventional   # grouped by configuration
uvx ruleprobe report --root ./transcripts      # a directory of your own
uvx ruleprobe report --rules ./docs/rules      # bind detectors to rule files, and name the gaps
uvx ruleprobe detectors                        # what would run
uvx ruleprobe corpus                           # how good each detector is, over the labelled corpus
uvx ruleprobe report --json                    # the same numbers as data, rows included

A transcript does not record the configuration it ran under, so --stance dimension=variant is how you say what it was. It is repeatable, it is what --by stance groups on, and it is what a detector’s gate: block reads: a gated detector with no stance passed never fires, and ruleprobe detectors names the stance each one is waiting for.

What it actually covers

Be clear-eyed about the scope, because the name promises more than version 0.1 delivers.

Six detectors ship with the package, and they are the generic ones: reading a whole file into the context window, an unfiltered find, a commit or push that walks past the repository’s hooks, a secret-shaped string written to a file, a context compaction, and a model change mid-session. They are in the package because they mean the same thing in every repository, and none of them needs to know what your rules say.

Your own rules take a detector you write, as data in the declarative format above or, when the shape is past what a matcher can say, as Python:

from ruleprobe import DEFAULT, Detector, iter_sessions, measure, report

def sudo_install(events, ctx):
    return [(p.turn, p.id) for p in ctx.bash
            if any(seg[:2] == ["sudo", "pip"] for pipe in p.pipelines for seg in pipe)]

DEFAULT.add(Detector("house-style/sudo-install", "house-style", "bash", sudo_install))
print(report([measure(s) for s in iter_sessions(since=30)]))

The two are the same engine: ruleprobe/detectors/common.yaml is the shipped six written as data, and a test asserts it produces hit-for-hit what the Python in ruleprobe/detectors/common.py produces over the corpus. Python remains the escape hatch, and the seam a third compiler plugs into is still register_compiler and from_spec.

What a matcher cannot say. It reads one event, or one of the three session shapes, and nothing else. There is no arithmetic, no counting (“more than three reads in a turn”), no reading a tool’s result, no comparing one argument with another, and no state carried across turns beyond order, absent and change. The command matcher inherits every known miss of the shell parse in ruleprobe/shell.py - a command inside a substitution is invisible, and so is a variable’s value. A rule whose shape needs any of that is a Python detector, and the report will not pretend otherwise.

A rule file is one rule. Binding is per file, not per heading: a CLAUDE.md holding twelve rules is one entry in the coverage block, not twelve. Splitting rules into files is what makes the unmeasured list mean anything.

Detector validity is measured, and the measurement is small. Every detector is scored against a hand-labelled corpus that ships with the package - see How good are the detectors? below - but that corpus is synthetic and it is six sessions, so it catches a detector that is wrong about a shape it was shown and says nothing about a shape nobody thought of. The detectors deliberately under-count: a missed hit is a quieter report, a false hit is a wrong one.

What a count is not. A detector fires on a shape in a transcript, not on an intention. whole-file-cat firing 89 times above does not prove the agent wasted context; it proves it read 89 files whole, which is a fact worth having and an argument worth starting.

Origins and neighbours

The engine was carved out of agent-harness, where it grew as a hook that measured that project’s own always-loaded rules; the detectors that were about agent-harness’s rules stayed there, and the rule-agnostic half is this package. The nearest neighbour is Burnd, which also reads Claude Code transcripts locally, for token spend rather than for rule compliance.

MIT licensed.

↑↓ move↵ openPagefind · indexed at build