Skip to content

fix(hooks): make pre-commit anti-pattern detection precise - #24

Merged
iammukeshm merged 1 commit into
mainfrom
fix/antipattern-hook-precision
Aug 3, 2026
Merged

fix(hooks): make pre-commit anti-pattern detection precise#24
iammukeshm merged 1 commit into
mainfrom
fix/antipattern-hook-precision

Conversation

@iammukeshm

Copy link
Copy Markdown
Member

Closes #23.

@jtreher asked why pre-commit-antipattern.sh greps instead of using a Roslyn analyzer. The premise was right — and the kit already had the better tool sitting in the MCP server.

The problem

The hook grepped whole staged files with bare patterns. On a fixture set covering both true and false positives it produced 13 findings, 7 of them wrong:

  • Fired on commented-out code, string literals, verbatim strings, and block comments
  • Flagged result.Result on a domain Result<T> — the very pattern the kit recommends
  • Flagged test fixtures, EF migrations, and seeders that the Roslyn detectors deliberately exempt
  • Scanned whole files, so one legacy DateTime.Now made every future edit to that file un-committable
  • Header comment claimed it used dotnet build diagnostics; it did not

Why it stays text-based

A git hook runs in a bare shell — Program.cs is an MCP stdio server with no CLI entry point, so detect_antipatterns is unreachable. Shelling out to dotnet build per commit costs seconds and fails outright on a red build, which is exactly when WIP gets committed.

So there are three tiers, each placed where it can actually run:

Tier Where Cost Purpose
pre-commit-antipattern.sh git hook, bare shell ms, no build last gate before history
detect_antipatterns (Roslyn MCP) Claude Code session workspace load the authoritative pass
Analyzer packages in your build IDE + CI full compile team-wide enforcement

What changed

New hooks/lib/antipattern-scan.awk carries the rules; the shell script handles staging, classification, and reporting.

  • Only the lines a commit adds are checked — a legacy violation in an untouched line is not this commit's problem
  • Comments and string literals stripped by a state machine tracking block comments, verbatim strings, and raw strings across line boundaries
  • .Result reported only when the receiver is visibly task-like; new HttpClient(handler) no longer reported (the Roslyn detector grades it medium-confidence)
  • Generated files skipped, test and migration sources exempted per rule, mirroring SourceClassifier
  • Rule IDs and severities match the Roslyn detectors (AP001–AP004), so a hook finding and an MCP finding are the same finding
  • New // cwm:ignore AP004 line suppression and CWM_ANTIPATTERN_WARN_ONLY=1 report-only mode
  • Runs correctly from any subdirectory

Design rule, recorded in ADR-006: tier 1 accepts false negatives to guarantee zero false positives.

Verification

Fixture repo with production, test, migration, and seeder sources, covering both directions.

Findings Wrong
Old hook 13 7
New hook 7 0

The false-positive file — commented-out DateTime.Now, new HttpClient() inside a log string, @"...DateTime.Now...", a multi-line block comment, result.Result on a domain type, DateTimeOffset.UtcNow, a raw string literal, and an (object, EventArgs) async void — produces zero findings.

Also confirmed: an innocent line added to a file with four legacy violations passes clean; CWM_ANTIPATTERN_WARN_ONLY=1 reports without blocking; LF line endings hold (#12's problem).

Docs

  • ADR-006 — the three-tier model, alternatives considered, and the process for adding a new rule
  • hooks/README.md — tier table, hook options, and the grep-vs-analyzer rationale for users who skip ADRs
  • CHANGELOG under [Unreleased]; README and SPEC references updated

🤖 Generated with Claude Code

The hook grepped whole staged files with bare patterns, so it fired on
commented-out code, string literals, a domain Result<T>, test fixtures, and
EF migrations — and one legacy DateTime.Now made every future edit to that
file un-committable. On a fixture set covering both directions it produced
13 findings, 7 of them wrong.

A git hook runs in a bare shell where the Roslyn MCP server is unreachable,
and shelling out to dotnet build in front of every commit costs seconds and
fails on a red build, which is exactly when WIP gets committed. So the hook
stays text-based, but now earns it: comments and string literals are
stripped with state carried across line boundaries, only the lines a commit
adds are checked, and rule IDs, severities, and SourceKind exemptions mirror
the Roslyn detectors exactly. Where a pattern cannot be resolved without a
semantic model the hook stays silent — tier 1 takes false negatives to
guarantee zero false positives, and detect_antipatterns catches the rest.

Same fixtures now produce 7 findings, all correct.

ADR-006 records the three-tier model and the process for adding a rule.

Closes #23

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@iammukeshm
iammukeshm merged commit 1057a67 into main Aug 3, 2026
11 checks passed
@iammukeshm
iammukeshm deleted the fix/antipattern-hook-precision branch August 3, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Why the grep on pre-commit vs. a Rosyln Analyzer?

1 participant