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:
- 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.
- Stage 2 — LLM judge (
lib.sh: ub_judge → claude -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:
- Bump
version in .claude-plugin/plugin.json (and the marketplace entry).
- Commit, then tag and push the tag:
git tag vX.Y.Z && git push origin vX.Y.Z.
- Set
ref in .claude-plugin/marketplace.json to vX.Y.Z, commit, push master.
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:
UserPromptSubmit → baseline.sh — records pre-turn git HEAD so the diff
survives commits made during the turn.
PostToolUse → accumulate.sh — records changed files (non-git fallback).
Stop → stop-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.
SessionEnd → cleanup.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