Probity ships five built-in rules. Each is a factory called with its options in probity.config.ts.
Blocks a write unless the session's recent history shows a failing test that the pending implementation would address, and the write is the minimum needed to make that test pass. Uses an AI validator — via the agent's official SDK — to judge the pending action against the transcript and the file's current content.
It also enforces the refactor step: starting the next test is blocked only when the prior green left an unmistakable, downside-free refactor undone. Anything less clear-cut lets green stand.
Tip: In tricky or unclear cases, your judgment can override the validator: reply in the session asking for the change to be let through, and it's allowed on the next attempt. The override is temporary; for lasting adjustments, use custom
instructions.
An opt-in deterministic fast-path can skip the AI when a write adds exactly one new test node to a recognised language; see Fast-path below.
- Applies to: write actions
- Supported agents: Claude Code, OpenAI Codex, GitHub Copilot
| Option | Type | Default | Description |
|---|---|---|---|
instructions |
string | ((defaults: string) => string) |
built-in three-rule TDD spec | Overrides or extends the default TDD rules text. Pass a string to replace outright, or a function (defaults) => ... to extend (e.g. append a project addendum without forking the whole spec). |
maxEvents |
number |
10 |
Keep at most this many of the most recent session events when building the validator prompt. Caps token usage on long transcripts. |
maxContentChars |
number |
6000 |
Truncate any single event's text/output longer than this with a head + marker + tail replacement, so the validator still sees both edges. |
fastPath |
boolean |
false |
When a write to a recognised language adds exactly one new test node, return pass without calling the AI. Off by default so single-test writes still reach the validator; set to true to skip the AI on them. |
Scope to specific paths by wrapping in a { files, rules } block:
{
files: ['**/src/**'],
rules: [enforceTdd()],
}Replace the default rules outright:
enforceTdd({
instructions: `Rules:
1. Tests must use the project's custom assertion helpers.
2. ...`,
})Extend the defaults with a project-specific addendum:
enforceTdd({
instructions: (defaults) => `${defaults}
### Project rule
Tests must use the project's custom assertion helpers.`,
})Skip the AI on single-test additions by opting into the fast-path:
enforceTdd({ fastPath: true })The fast-path is opt-in: pass fastPath: true to enable it. When enabled, a write to a recognised language that adds exactly one new test node returns pass without consulting the AI. It is off by default because a single-test write is the green-to-red boundary where the validator checks whether the prior green left a refactor unmade, and fast-passing it would skip that check. Multi-test writes, implementation writes, and unrecognised file types fall through to the AI regardless.
| Language | Extensions | Required pack | Test patterns recognised |
|---|---|---|---|
| TypeScript | .ts, .tsx |
(built-in) | it() / test() and their .skip, .only, .each variants |
| JavaScript | .js |
(built-in) | it() / test() and their .skip, .only, .each variants |
| Python | .py |
@ast-grep/lang-python |
def test_* (pytest convention) |
| C# | .cs |
@ast-grep/lang-csharp |
[Fact] / [Theory] (xUnit), [Test] (NUnit), [TestMethod] (MSTest) |
| Ruby | .rb |
@ast-grep/lang-ruby |
it / specify / xit / fit (RSpec), def test_* (Minitest) |
| PHP | .php |
@ast-grep/lang-php |
function test* (prefix), #[Test] attribute, @test PHPDoc (PHPUnit) |
Languages with a "Required pack" entry need the corresponding @ast-grep/lang-* package installed in the same scope as Probity. Use npm install -D <pack> for project-local or npm install -g <pack> for global. If the pack isn't installed, writes in that language silently fall through to the AI path (no install, no error, no fast-path).
Matching writes that don't fast-path trigger an AI call. Scope with a { files, rules } block so the rule fires only on the code you care about.
Bump maxContentChars when working with large source files so the validator doesn't see context lopped mid-region. Bump maxEvents for tasks that span many tool calls. Pushing either too high crowds the prompt — the model may miss recent events or stop producing the JSON verdict format.
Blocks a write whose filename doesn't match the configured casing style. Passes non-write actions through.
- Applies to: write actions
- Supported agents: Claude Code, OpenAI Codex, GitHub Copilot
| Option | Type | Default | Description |
|---|---|---|---|
style |
'kebab-case' | 'camelCase' | 'snake_case' |
required | Casing style the filename must match. |
enforceFilenameCasing({ style: 'kebab-case' })
// To scope to specific paths, wrap in a `{ files, rules }` block:
{
files: ['**/src/**', '**/test/**'],
rules: [enforceFilenameCasing({ style: 'kebab-case' })],
}Blocks a command whose text matches match — a literal substring or a RegExp. Passes non-command actions and non-matching commands through.
- Applies to: command actions (bash / shell tool calls)
- Supported agents: Claude Code, OpenAI Codex, GitHub Copilot
| Option | Type | Default | Description |
|---|---|---|---|
match |
string | RegExp |
required | Pattern to match against the command text. |
reason |
string |
required | Surfaced back to the agent when the rule blocks. Make it actionable. |
forbidCommandPattern({
match: 'npm install',
reason: 'Use pnpm install instead',
})
forbidCommandPattern({
match: /rm\s+-rf/,
reason: 'Avoid destructive rm',
})Blocks a write whose content matches match — a literal substring or a RegExp. Passes non-write actions through.
- Applies to: write actions
- Supported agents: Claude Code, OpenAI Codex, GitHub Copilot
| Option | Type | Default | Description |
|---|---|---|---|
match |
string | RegExp |
required | Pattern to match against the file content being written. |
reason |
string |
required | Surfaced back to the agent when the rule blocks. |
Block timer code in source:
forbidContentPattern({
match: 'setTimeout',
reason: 'No timers in source code',
})Scope to specific paths by wrapping in a { files, rules } block:
{
files: ['**/*.md'],
rules: [
forbidContentPattern({
match: /\p{Extended_Pictographic}/u,
reason: 'No emojis in documentation',
}),
],
}Gates a command action on a prior command appearing in canonical session history. By default the required command must be the most recent event; the optional after filter relaxes this by naming the events that invalidate the required command if they appear after it.
- Applies to: command actions (bash / shell tool calls)
- Supported agents: Claude Code, OpenAI Codex, GitHub Copilot
| Option | Type | Default | Description |
|---|---|---|---|
before |
{ kind: 'command'; match: string | RegExp } |
required | Which actions this rule gates. Only commands whose text matches before.match are evaluated; everything else passes through. |
command |
string | RegExp |
required | The prior command pattern that must satisfy the gate. Matched against canonical command events in session history. |
after |
{ kind: 'write' } | { kind: 'command'; match?: string | RegExp } |
any event | What invalidates the required command if it appears after the most recent matching command. Without after, any event after the required command invalidates it (the required command must be the most recent event). With { kind: 'command' }, omit match to invalidate on any command. |
reason |
string |
auto-generated | Surfaced when the rule blocks. Defaults to a message naming the required pattern. |
Block commits unless npm run lint was the most recent event:
requireCommand({
before: { kind: 'command', match: /git commit/ },
command: /npm run lint/,
})Allow non-write events between lint and commit; a write since lint invalidates the gate:
requireCommand({
before: { kind: 'command', match: /git commit/ },
command: /npm run lint/,
after: { kind: 'write' },
reason: 'Run lint after every change before committing.',
})