Plugin · for Claude Code
Get snag
A snagging survey for frontend finish — the numbered list a surveyor hands over after walking a finished building. It reads your source, reports one row for each of the 17 practices on the Finish work shelf, and changes nothing. Version 2.1.3, MIT.
- Claude Code 2.1 or later
- python3 on your PATH
- standard library only — no other dependencies
Two ways in
The first installs it and keeps it. The second hands you the archive and leaves your settings untouched, which is the honest way to try a tool that is about to read your source.
Install
Add the marketplace, install the plugin
Two commands inside Claude Code, then a third that measures the tool before you point it at anything of yours.
/plugin marketplace add https://gitlab.com/inadram/snag.git /plugin install snag@inadram /snag:calibrate
Run calibrate first. It is the one command that shows you the tool being wrong before it shows you being wrong.
Or try it once
Download the archive
Point Claude Code at the zip and the four skills are there for that session. Nothing is written to your settings and nothing is installed.
Download snag-2.1.3.zip39 KBclaude --plugin-dir ./snag-2.1.3.zip
The same archive works with --plugin-url if you would rather host it yourself.
The first run asks for permission
The bundled scripts live inside the plugin, which is outside your project, so Bash asks before running them. Interactively that is one approval at your first /snag:review: say yes and the survey proceeds.
Headless, with claude -p, there is nobody to ask, so the run is refused — and the survey refuses to guess rather than replaying the predicates from memory, which is correct and not useful. Grant it the resolved absolute path of the three commands.
ls -d ~/.claude/plugins/cache/inadram/snag/* # the plugin root, including its version Bash(<that path>/bin/snag-scan:*) Bash(<that path>/bin/snag-rules:*) Bash(<that path>/bin/snag-ledger:*)
Three things there are worth knowing, because none of them is obvious and all three were measured. ${CLAUDE_PLUGIN_ROOT} is not expanded inside a permission rule — the plugin's own README told you to grant it that way for two versions, and it reads as though it works and is refused every time.
A glob does not match either; only the exact path is honoured. And because the path carries the version, an upgrade needs the grant again, which is why the interactive approval is the one to prefer.
That the first run produces no survey at all is measured, not predicted. On a real four-file app it returned three blocked commands and no rows, which is what a user meets on first use.
What it surveys
The Finish work shelf, and nothing else. 17 practices, chosen by hand out of the 103 in the catalogue, every one of them with an authored rule — so there is no row the tool has to skip. The shelf page is the source of truth: add an eighteenth entry there and the build refuses until a rule exists for it.
A survey is 17 rows every time. A practice that does not apply says so, and says which predicate ran to establish it; a practice the tool could not decide says what blocked it. Nothing falls off the bottom of the report — an audit tool stops being trusted the moment it can, so the renderer compares its row count against the rulepack and exits non-zero rather than printing a shorter table.
| State | What it means |
|---|---|
| held | present, and the tool can point at it |
| missing | applies here, absent, and the practice's own confusables ruled out |
| unsure | applies, and the tool cannot decide — carries the reason it could not |
| n/a | the precondition is provably absent, and the predicate actually ran |
| scoped-out | excluded because you narrowed the survey to a section or an element |
What you get
| Command | What it does |
|---|---|
| /snag:teach | One practice: its defect running in the browser, then the same demo with the practice held, then the implementation brief. Spans all 103. Needs no repository. |
| /snag:review | The survey. 17 rows, read-only, enforced by a hook rather than promised in prose. |
| /snag:apply | One named finding, in one named file, behind seven gates. |
| /snag:calibrate | The tool measured against pages that are known-correct and known-broken. |
What it will not do
It will not sweep a page and fix it. Nothing in the rulepack is in the write tier — 0 of 17 — so no finding here can produce an unattended edit.
| Tier | Practices | What happens |
|---|---|---|
| write | 0 | Edit and commit unattended. Deliberately empty. |
| propose | 4 | The edit is drafted and you read the diff. |
| brief | 8 | The finding is located and described; the edit is yours. |
| teach | 5 | There is no mechanical signal at all. You get the practice and the demo. |
That is not timidity, it is what the shelf is. These are practices chosen because they would never be raised in review, which is a selection for judgement and therefore a selection against automation. momentum needs to know whether a rail was meant to coast, and nothing in the DOM separates a scroller that should coast from a split pane that must stop dead. per-axis-radius-correction needs a live scale value that exists only inside the animation's own loop.
Of the 17, 6 can be decided from source alone, 3 need a live page, 3 want both, and 5 have no mechanical detector in either mode. Those last five are always unsure and never silently dropped.
It also will not patch a running page. That fixes the pixels and not the repository: the change lasts until reload and is absent from the next deploy, while the team believes the defect is gone. The page-side inspector measures and returns JSON; edits happen in source.
What it scored
Against hard negatives first — five pages where a naive predicate fires and the fix would be wrong. Three of the first four produced a false positive on the first run. The fifth, a light card inside a dark page, arrived with the colour-scheme check that reads the surface behind a field rather than the page, and a check that reads any dark ancestor instead fires on it twice. There was one more, and it was the worst of them: it flagged a live drag handle, where the obvious remedy kills the control without changing a single pixel, so no screenshot, snapshot or visual-regression test would have caught it. That fixture left when the shelf was cut to seventeen, because the practice it watched is not on the shelf — the lesson stayed, the corpus did not. Each predicate was then tightened against the fixture that broke it.
Then end to end, on a four-file dashboard built as a fixture with 5 planted defects and four planted confusables, and the expected survey written down before the tool was run. It found all 5, with a file and a line each. 2 of the four confusables are locations a survey must never give as the site of a defect, and neither was given.
It also corrected the expected survey 3 times, and was right every time. One row I had written down as a defect is genuinely not applicable — the page is light-only, and declaring a colour scheme there paints a dark scrollbar over a light page, which is that rule's own first confusable. And one I had written down as exempt under WCAG 2.2 SC 2.5.8 is not exempt: the large enclosing target had no handler bound to it anywhere, so it was a phantom target painting over the real link, and two rows of that fixture did not click at all. I planted that bug by accident. And one row I had written down as not applicable is in fact held: the panel animates on a CSS transition, and a transition does retarget from its current value, which is the practice present rather than absent — the same declaration that carries the easing defect, read correctly twice.
Run to run, the 4 structural rows do not move: same practice, same file, same line, in every run that produced a survey. The row count has never moved either. The fifth — the one whose confusables turn on intent — used to land anywhere between missing, unsure and held; since the evidence clause was tightened, four independent runs have reported it identically. Those four runs are in the repository, and the build re-grades every one of them on every commit, so the tool cannot quietly stop agreeing with them.
Two runs failed outright, and both are worth more than the passes. One claimed a practice was held on the strength of an overlay that covers the control instead of enlarging it; the rule's evidence clause had permitted that reading, and it now names the case. The other never rendered a survey at all, because a model handed seventeen rows of JSON to a heredoc and the sandbox refuses {" inside one. Neither would have shown up in a unit test of the predicates, and both are why the suite drives a real agent rather than asserting about one.
Every numeral on this page is read out of the rulepack and the fixture by the build, so the page cannot drift away from the tool. The calibration fixtures themselves live in the catalogue repository, which is private; the numbers above are what they produced.
Where it comes from
The plugin is published at gitlab.com/inadram/snag, which is also the marketplace — the repository root carries the catalogue that /plugin marketplace add reads. The rulepack it surveys against is generated from the shelf page, so the page is the source of truth and the tool cannot quietly disagree with it.
There is a second copy at github.com/inadram/snag, because the Anthropic plugin directory pins each listing to a GitHub commit and nothing in its catalogue is hosted anywhere else. It is a mirror in the strict sense — same commits, same tree — and the build clones both on every commit and fails if either has fallen behind, because a stale mirror serves an old plugin and says nothing. Install from whichever you prefer; the commands above use the first.
The catalogue repository that builds it is private. Everything the tool can find is on the shelf, and everything the catalogue holds is in all 103.
snag is MIT. Plugins run with your privileges and can execute arbitrary code, so read the source before you install this or any other one — it is 16 files, 1,243 lines of code and a rulepack generated from the shelf. The rulepack is published here as snag-rules.json.