Skip to content

nikicat/unblind

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

unblind

A Claude Code plugin that flags algebraic blindness — types too generic to carry the meaning of their own values (bool, bare string/int, blind Maybe/Option/null, untagged tuples) — in code Claude writes, and suggests named-type fixes. It suggests, it does not auto-edit: findings come back as text and Claude makes (or declines) the change.

Why a hook + an LLM, not a lint rule

Algebraic blindness is semantic: whether a bool or a Maybe is fine or a bug depends on what it means in your domain — knowledge a regex doesn't have. So the plugin is two-stage:

  1. Stage 1 — regex prefilter (lib.sh: ub_prefilter): catches the syntactic symptom shapes cheaply. Loose by design (recall > precision); skips the LLM on edits with no symptom at all.
  2. Stage 2 — LLM judge (lib.sh: ub_judgeclaude -p): applies the checklist and the "when to stop" guardrails to separate real blindness from a self-evident boolean / YAGNI variant / harmless newtype.

The shared rubric lives once in skills/spot-blindness/checklist.md and is used both as the judge's prompt and as Claude's remediation reference (via the spot-blindness skill).

The judge invokes claude -p with --strict-mcp-config --mcp-config '{"mcpServers":{}}', so it loads zero MCP servers — the one-shot call stays cheap and never spins up your configured MCP servers (Grafana, Playwright, etc.). It still reuses your session login. (--bare would skip even more but drops the login, so it is intentionally not used.)

Requirements

  • python3 (or python) — used for JSON parsing/serialization. No jq dependency; Python ships with macOS and virtually every Linux dev box.
  • the claude CLI on PATH (the judge runs claude -p, reusing your existing login — no separate API key needed)
  • git is optional; used for accurate per-turn diffs when present

Install

This repo is both the plugin and its marketplace, so installing is two commands:

/plugin marketplace add nikicat/unblind
/plugin install unblind@unblind

Then /reload-plugins (or restart). To update later: /plugin marketplace update unblind.

Versioning & releases

Installs are pinned to a release tag, not master. The marketplace entry's plugin source points at a GitHub ref (currently v0.1.0), so pushing to master does not change what installed users run — only cutting a new tag and bumping the ref does. The marketplace catalog is read from master; the plugin code is fetched from the tag.

To cut a release vX.Y.Z:

  1. Bump version in .claude-plugin/plugin.json (and the marketplace entry).
  2. Commit, then tag and push the tag: git tag vX.Y.Z && git push origin vX.Y.Z.
  3. Set ref in .claude-plugin/marketplace.json to vX.Y.Z, commit, push master.
  4. gh release create vX.Y.Z --generate-notes.

Users pick it up on /plugin marketplace update unblind.

For local development the github-pinned source means a local marketplace add still fetches from the tag, not your working copy. To iterate locally, run the test suite (below), or temporarily set the plugin source to "./".

Two variants

Default — Stop review (reliable, low-noise)

hooks/hooks.json wires:

  • UserPromptSubmitbaseline.sh — records pre-turn git HEAD so the diff survives commits made during the turn.
  • PostToolUseaccumulate.sh — records changed files (non-git fallback).
  • Stopstop-review.sh — once per turn, diffs the change, runs the two stages, and if it finds blindness hands suggestions back via {"decision":"block"} so Claude fixes or confirms intentional. Self-dedupes so it never traps the turn.
  • SessionEndcleanup.sh.

This is the recommended default: full-file context, one LLM pass per turn, and it uses only well-documented hook mechanics.

Alternative — per-edit async (asyncRewake)

hooks/hooks.async.json wires a single PostToolUse hook (detect.sh, asyncRewake: true) that runs in the background per edit and re-wakes Claude with a suggestion when it finds something. Lower latency to feedback, but noisier and asyncRewake on PostToolUse is experimental — verify it actually re-wakes in your version before relying on it. To switch:

cp hooks/hooks.async.json hooks/hooks.json   # then /reload-plugins

Tuning

  • UNBLIND_MODEL — judge model (default claude-haiku-4-5-20251001).
  • UNBLIND_MAX_CTX — chars of file/diff context sent to the judge (default 12000).
  • UNBLIND_WINDOW — when a file exceeds MAX_CTX, lines of context kept around each symptom line (default 40). Ensures a smell buried deep in a large file still reaches the judge instead of being truncated off the top.
  • Source extensions: edit ub_is_source in hooks/scripts/lib.sh.
  • Symptom patterns: edit ub_prefilter in hooks/scripts/lib.sh (keep it loose).
  • False positives: tighten the prompt in ub_judge / the "when to stop" section of checklist.md, not the regex.

Concurrency & state

All temp state lives under $CLAUDE_PLUGIN_DATA keyed by session_id (baseline-<sid>, changed-<sid>.txt, reported-<sid>), so concurrent sessions on the same project don't collide. cleanup.sh removes a session's files on SessionEnd and sweeps orphans older than a day.

Tests

tests/prefilter_test.sh   # offline, no creds: Stage-1 recall on positive fixtures
tests/run.sh              # end-to-end: drives detect.sh + `claude -p` over fixtures
tests/run.sh positive     # only positives   |   tests/run.sh negative   # only negatives

Fixtures live in tests/fixtures/{positive,negative}/. Positives must produce a SMELL (exit 2); negatives must come back CLEAN (exit 0). The negatives include the tricky over-narrowing cases (self_evident_bool.rs, harmless_single_bool.ts) that deliberately trip the prefilter but must be judged CLEAN — they exercise the "when to stop" guardrails. run.sh calls the real model, so judgments can rarely flake; the fixtures are chosen to be clear-cut.

Caveats

  • The judge is an LLM: it will occasionally miss or over-flag. It's an assistant, not a gate.
  • Per-edit context is partial; the Stop variant judges on full files and is more accurate.
  • Go (and other languages without sum types) gets weaker structural fixes by design — see the note in checklist.md.

About

Claude Code plugin: detects algebraic blindness (over-generic bool/string/Maybe/tuple types) in changed code and suggests named-type fixes. Two-stage regex prefilter + LLM judge, plus a spot-blindness skill.

Topics

Resources

License

Stars

0 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages