ccdwyer/assertion-guardian

テストファイルを弱くする編集を拒否する Claude Code モッドです。アサーションの削除・緩和、ケースの削除、skip の追加、失敗の握りつぶし、スナップショットの書き換え、shell 経由のテスト改変を検出し、プロンプト上部に一度だけ許可する上書き欄を表示します。
ccdwyer/assertion-guardian

Assertion Guardian は、テストを弱くして通過させようとするエージェントを止める Claude Code プラグインです。テストファイルへの Edit、Write、NotebookEdit が実行される前に、変更前後のファイル全体を計算し、テストが弱くなっていれば変更を拒否します。軽量スキャナーはコード、コメント、文字列リテラルを分離するため、docstring、コメント、fixture 文字列内のアサーションは数えません。削除されたアサーション、削除されたテストケース、変更または削除された期待値、緩和または置換された完全一致マッチャー、曖昧なマッチャー、定数アサーション、極性の反転、削除された表の行、追加された skip や focus マーカー、コメントアウトされたアサーション、握りつぶされた失敗、デッドブランチ、手動で書き換えたスナップショット、一括スナップショット更新、shell 経由で変更されたテストファイル(rm、mv、sed -i、>、tee、git rm、find -delete、git checkout、inline python -c/node -e)を検出します。削除されたテストファイルは git で最後のコミット版と比較するため、最初から書き直しても検査を回避できません。拒否時には発火したシグナルを一覧にし、テスト対象のコードを修正するかユーザーに尋ねるようモデルへ伝えます。通常のリファクタリングは通過します。プロンプト上部の一度だけ許可/閉じる欄、または /allow-test-change コマンドで上書きできます。上書きは正確なツール呼び出しに結び付きます。テストファイルは一般的な命名パターンとスナップショットのパスで認識されます。Hooks は session.start、command.run{allow-test-change}、tool.call、ui.render{AbovePrompt} です。ネットワークやテレメトリーなしで完全にローカル実行されます。ライセンスは MIT です。
まず作者の README で marketplace とプラグイン名を確認してください。コマンドはリポジトリの構成によって変わる場合があります。
claude plugin marketplace add ccdwyer/assertion-guardian claude plugin install assertion-guardian

The model is asked to change an expected value so a failing test passes. Assertion Guardian refuses and names the signal, the band above the prompt offers allow once, and the real bug gets fixed instead. MP4
A Claude Code mod that stops the agent from making tests pass by making the tests weaker.
Before any Edit, Write or NotebookEdit to a test file runs, the mod works out the whole file before and after the change. It refuses the change if the tests got weaker. A light scanner first separates code from comments and string literals, so an expect( inside a docstring, a comment or a fixture string is never counted as code.
| Signal | Example |
|---|---|
| Assertions removed | expect(...), assert, assert.strictEqual, XCTAssert*, assertThat, t.Error, #expect |
| Test cases deleted | it(...), test(...), def test_*, func Test*, @Test |
| Expected values changed or dropped | Jest, Node assert, unittest/pytest, AssertJ, chai, RSpec, XCTest: toBe(3) changed to toBe(4), toHaveBeenCalledWith(1) changed to toHaveBeenCalled(), an edited inline snapshot, assert f() == 1 changed to == 2 |
| Exact matchers replaced or loosened | toBe(3) changed to toBeTruthy(), toBeGreaterThanOrEqual(3) or toBeCloseTo(3), toStrictEqual changed to toEqual, toBeGreaterThan(3) changed to toBeGreaterThanOrEqual(3) or toBeLessThan(3), toBeCloseTo(3) changed to toBeCloseTo(3, 0) |
| Vague matchers added | toEqual(0) changed to toEqual(expect.any(Number)) |
| Constant assertions added | expect(3).toBe(3), assert.strictEqual(3, 3), assert 3 == 3, XCTAssertEqual(3, 3), assert True |
| Assertion polarity flipped | .toBe(3) changed to .not.toBe(3), strictEqual changed to notStrictEqual, assert x == 3 changed to != 3, is None changed to is not None |
| Test data rows removed | a row deleted from a Go table test or a test.each table |
| Skips added | it.skip, xit, it.todo, test.fail, this.skip(), @pytest.mark.skip/xfail, @Disabled, @Ignore, t.Skip, XCTSkip |
| Focus added | .only / fit(, which silently drops every other test |
| Assertions commented out | a line that held an assertion reappears as a comment |
| Assertion failures swallowed | a try around an assertion whose catch / except / rescue doesn't rethrow, pytest.raises(AssertionError), expect(() => expect(...)).toThrow() |
| Dead branches | if (false) { expect(...) }, return before the assertions |
| Snapshot files rewritten by hand | *.snap, __snapshots__/ |
| Snapshots updated in bulk | jest -u, npm test -- -u, --update-snapshots, cargo insta accept, INSTA_UPDATE=1, loki update, backstop approve, chromatic --auto-accept-changes. Wrappers such as env, npx, pnpm exec, pnpm --filter and python -m are looked through |
| Test files changed through the shell | rm, mv, sed -i, perl -pi, > redirects, tee, git rm, rm -rf tests, find ... -delete, git checkout <rev> -- file, bash -c "...", one-off python -c / node -e scripts that name a test file |
Suppose a test file has been deleted and is then written again from scratch. The new version is compared with what git last committed, so deleting the file doesn't get around the check.
The refusal lists which signals fired. It tells the model to fix the code under test, or to ask you if it thinks the test itself is wrong.
Normal refactors pass: renaming the value under test, reformatting, reordering, moving assertions between tests, adding tests, switching quote style, swapping in stronger matchers, and changing toBe(null) to toBeNull().
Override: a band above the prompt shows the last blocked change, with allow once and dismiss buttons. Or run /allow-test-change, which runs immediately, even mid-turn. An allowance is tied to that exact call (same tool, same file, same content), so the model has to retry the identical change. A different edit to the same file is checked from scratch.
This is a guardrail, not a sandbox. It checks what tool calls say they'll do. A determined process could still change files some other way, such as a script that never names the test file.
Test files are recognised by *.test.*, *.spec.*, test_*.py, tests.py, *_test.{py,go,rb,ex,dart}, *_spec.rb, *Tests.swift, *Test.{kt,java,cs}, and snapshot paths. Code files under __tests__/, test/, tests/ or spec/ count too, except those inside fixtures/, support/, helpers/, factories/, __mocks__/ or testdata/.
/plugin marketplace add ccdwyer/claude-mods
/plugin install assertion-guardian@ccdwyer-mods
/reload-plugins
claude plugin validate .
claude plugin test .
Events this mod hooks, as claude plugin validate reads the module:
session.startcommand.run{command=allow-test-change}tool.callui.render{component=AbovePrompt}Engine calls it makes: $.command.register, $.fs.exists (via previous), $.fs.read (via previous), $.process.run (via previous), $.state.get, $.state.set, $.ui.resolve.
A tool.call hook sits in the middle of every tool call: it can see the call, refuse it, or add context to its result. This mod uses that only for the behaviour described above.
It runs entirely on your machine. It sends nothing over the network. It runs git locally to read the previous version of a test file.
The mod collects no analytics or telemetry, and its author receives no data from it.
Full policy: PRIVACY.md.
MIT