A small tool that keeps your AI coding assistant honest.
When you build with an AI agent (Claude Code, Cursor, Copilot…), it forgets things
between sessions. It picks a different library than the one you agreed on. It writes
the same helper function twice under two names. It does the opposite of what your
CLAUDE.md says. You often can't see it, because you didn't write the code.
keel reads your code and points out exactly where this has happened:
Your
CLAUDE.mdsays you use Zod. Your code imports Yup. You haveformatDatewritten twice. You're fetching HTTP with both axios and ky. keel finds this in milliseconds, for $0 — no AI, no cloud, no API key.
It gives you one number — a Coherence Score out of 100 — plus a short list of exactly what's wrong and where.
| What it means | Example | |
|---|---|---|
| Drift | Your notes say one thing, the code does another | "Says Zod, but 7 files import Yup" |
| Library conflicts | Two libraries doing the same job | "axios in 4 files, ky in 1 — pick one" |
| Exact duplicates | The same function written more than once | "formatDate and dateToString are identical" |
| Near duplicates | Almost-the-same function copy-pasted and tweaked | "buildSettingsA is ~91% similar to buildSettingsB" |
| Doc drift | Your docs reference files that no longer exist | "docs/api.md links to src/old-parser.ts — it's gone" |
Everything is computed by reading the code directly — no AI model is involved, so the results are exact, repeatable, and free.
$ keel check
Coherence Score: 84 / 100
Drift -7
Library conflicts -5
Duplication -4
Findings
✗ DRIFT CLAUDE.md: "validation: Zod" — but code imports yup (1 file)
✗ CONFLICT http: axios (1 file), ky (1 file)
✗ DUP formatDate / dateToString are identical implementations
~ DUP~ buildSettingsA() ~91% similar to buildSettingsB()
scanned 5 files · 3.9ms · $0.00
Loud findings (✗) are high-confidence. Gentle findings (~) are advisory — they
might be intentional. keel never blocks your work unless you explicitly ask it to.
The terminal shows a capped summary. For the complete picture — every finding with its exact location and cause, plus a prioritized "how to improve" — write a Markdown report:
keel check --output-md # writes keel-report.md
keel check --llm --output-md # + plain-English explanations and an AI improvement planThe report is written to where you run keel, never into the scanned repo.
You'll need Node.js version 20 or newer.
npm install -g @curiousnerd/keel
keel check /path/to/your/project # any JavaScript, TypeScript, or Python projectThat's it. Run it again any time — it remembers what it already scanned, so repeat runs are nearly instant.
From source (clone & build — for contributing, or to run without installing from npm):
git clone https://github.com/aditya20t/Keel.git keel && cd keel
npm install # install dependencies
npm run build # compile TypeScript to dist/
npm link # optional: makes `keel` available globally
keel check /path/to/your/project # if you ran `npm link`
node dist/cli.js check /path/to/your/project # otherwise, run the built file directlykeel check [path] # scan a project (defaults to the current folder)| Option | What it does |
|---|---|
-v, --verbose |
Show extra detail under each finding (file + line). |
--json |
Print the result as JSON, for scripts or CI. |
--output-md [file] |
Write a full Markdown report (every finding + locations + causes + how-to-improve) to a file (default keel-report.md). |
--limit <n> |
Max findings to print in text mode (default 25). |
--no-cache |
Don't read or write the .keel cache — a fully read-only run. |
--no-gitignore |
Scan files even if they're listed in .gitignore. |
--no-python |
Skip Python files (don't load the Python parser). |
--llm |
Add plain-language explanations to findings (off by default — see below). |
--fail-under <n> |
Exit with an error if the score is below <n>. Off by default. |
--facts |
Print the raw data keel extracted (for debugging). |
The score is graduated: each category (drift, conflicts, duplication) adds up but eases off as problems pile up, so a repo with 30 duplicates scores low without instantly hitting zero.
keel scans only the code you actually maintain. It respects your .gitignore (root
and nested), and on top of that skips generated/minified files (bundles, Prisma runtimes,
etc.) and node_modules, dist, build, and similar. Duplicate detection only considers
named functions, so inline callbacks don't create noise.
To catch drift, keel needs to know what you decided. Add a short block to your
CLAUDE.md or AGENTS.md (keel reads both):
## Stack
- Validation: Zod
- Database: PostgreSQL
- Package manager: pnpm
- Naming: camelCase
## Constraints
- never use `any`keel checks each line it understands against the real code. Lines it can't check, it simply ignores — it never guesses.
False alarms are the fastest way to make a tool annoying, so keel makes them easy to silence — three ways, from most specific to broadest:
1. A comment in the code (best for an intentional duplicate):
// keel-ignore: this copy is intentional
export function dateToString(value: Date): string { /* ... */ }2. A .keelignore file in your project root:
# We're mid-migration from axios to ky — don't flag it yet.
bucket:http
# Don't look at old code.
src/legacy/**
3. The config file keel.config.json:
{
"duplication": {
"minTokens": 20,
"near": { "enabled": true, "threshold": 0.85 }
},
"libConflicts": { "ignoreBuckets": ["test"] }
}minTokens— ignore functions smaller than this (avoids flagging tiny one-liners).near.threshold— how similar (0–1) two functions must be to count as near-duplicates. Higher = stricter.0.85is the conservative default.ignoreBuckets— capability groups (likehttp,date) to never flag.
It would seem natural for a tool like this to use an AI model. It deliberately doesn't. A study from ETH Zurich found that AI-generated context files actually made coding agents worse — lower success rate, higher cost. So keel does the opposite: it reads the real code with plain static analysis. The answers are exact and cost nothing.
The detection is always deterministic. keel check --llm adds two opt-in extras:
- A one-line plain-English explanation under each finding.
- Doc-claim drift — it reads your README/CLAUDE.md prose (not just a structured
block), extracts the stack you say you use ("validation: Zod", "database: Postgres"),
and checks it against the code. Here the LLM only turns English into a claim — the same
deterministic drift engine does the verifying, so the score never depends on the
LLM's judgement and stays reproducible whether the LLM is on or off. By default it shells out to the
claudeorcodexCLI you already have (no API key, no separate bill); setANTHROPIC_API_KEY/OPENAI_API_KEYto use an API instead. Responses are cached, and if nothing's available keel just skips them.
keel check is a verifier you run after the fact. The MCP server flips that around:
it lets your AI agent ask keel before it writes code, so duplication and
inconsistency are prevented at generation time instead of caught later. It exposes three
tools, all answered from live, deterministic facts about your code — no AI, no cost:
| Tool | What the agent gets |
|---|---|
check_before_write(intent) |
Existing functions it might be about to reinvent, and the libraries already in use for that job ("you already have formatDate; you already use axios — don't add ky"). |
get_conventions(path) |
The real conventions for that area — naming style, language, libraries in use — derived from the code, not a stale doc. |
report_drift() |
The project's current drift + library conflicts, so the agent doesn't add to them. |
Register it with Claude Code (after npm install -g @curiousnerd/keel):
claude mcp add keel -- keel mcpOr drop a .mcp.json in your project (works in Cursor and other MCP hosts too):
{ "mcpServers": { "keel": { "command": "keel", "args": ["mcp"] } } }The server serves the current directory by default; pass a path (mcp /some/repo) to
serve another. It re-reads the code on every call (the file-hash cache keeps that
near-instant), so answers always reflect the current state.
Working today. Drift + doc drift, library conflicts, exact + near duplicates, Coherence Score, suppressions, caching, an opt-in LLM layer, and an MCP server — across JavaScript, TypeScript, and Python. Python is parsed with tree-sitter (WebAssembly), so there's nothing to compile at install time.
Coming next: a pre-commit hook and GitHub Action · semantic duplication · more languages.
Want to help or understand the internals? See CONTRIBUTING.md.
Built by Aditya, with the help of awesome Claude 🤖.