The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Leakguard MCP listing page.
Squawk for backtests. A local-first MCP server that static-analyzes agent-generated Python code and flags lookahead bias & data leakage before the backtest runs.
Works for any time-series ML code — quant trading (crypto, equities, forex, futures),
demand forecasting, energy, weather, IoT sensors — wherever a wrong .shift() or a
global normalization silently poisons your results.

AI agents (Claude Code, Cursor) write feature engineering and strategy code faster than humans can review it. But they introduce lookahead bias at scale — subtle time-boundary errors that backtest perfectly and fail catastrophically in live trading or production:
leakguard catches these in the same agent loop — before the backtest runs:
The agent reads the finding + fix snippet and self-corrects in one turn. No human review needed.
Add to your MCP config (~/.claude/claude_desktop_config.json or .claude/settings.json
in your project):
Or if installed via uv:
Restart Claude Code. leakguard's tools are now available to the agent.
Add the same block under mcpServers in your Cursor MCP settings file.
| Tool | Description |
|---|---|
lint_code(code) | Analyze a code string, return findings |
lint_file(path) | Analyze a file on disk |
lint_paths(glob) | Analyze all matching files |
list_rules() | List all rules with severities |
explain_rule(rule_id) | Full rationale + fix patterns for a rule |
The same scanner is available as a CLI — handy for a pre-commit hook or CI step (exits non-zero when leakage is found):
It prints each finding with its severity, line/col, and a concrete fix snippet — the same output shown in the demo above.
All 10 rules active, no tiers:
| ID | Severity | Pattern |
|---|---|---|
| LG001 | 🔴 | Future shift as feature: shift(-n) / diff(-n) / pct_change(-n) |
| LG002 | 🔴 | Centered windows: rolling(center=True) |
| LG003 | 🔴 | Global-fit scaling: StandardScaler().fit(full_df) / hand-rolled mean-std before split |
| LG004 | 🔴 | Shuffled time-series split: train_test_split default, KFold, cross_val_score(cv=n) |
| LG005 | 🔴 | Label leakage: future-derived target column reused in features |
| LG006 | 🟡 | Whole-history aggregates as features: .max() / .mean() over full series |
| LG007 | 🔴 | Backfill imputation: bfill() / fillna(method='bfill') |
| LG008 | 🔴 | Forward asof-joins: merge_asof(direction='forward'/'nearest') |
| LG009 | 🟡 | Resample label/closed mismatch on bar timestamps |
| LG010 | 🟡 | groupby().transform()/agg() spanning train/test boundary |
Each finding includes a concrete fix snippet so the calling agent can self-correct immediately.
Measured on two labeled corpora, 49 snippets total.
Reproduce with uv run python -m benchmark.run.
Honesty note: the trading corpus was written by the tool's author — treat its numbers as regression fixtures, not independent validation. The general-ML corpus is one arm's length removed in domain (author-composed reproductions of widely documented leakage anti-patterns, not a downloaded public dataset). The corpus deliberately includes adversarial snippets the scanner is known to miss; they are counted against it.
Trading corpus — 39 snippets (23 leaky, 16 clean + hard negatives):
| Rule | Precision | Recall | TP | FP | FN |
|---|---|---|---|---|---|
| LG001 | 75% | 100% | 6 | 2 | 0 |
| LG002 | 100% | 100% | 5 | 0 | 0 |
| LG003 | 75% | 100% | 3 | 1 | 0 |
| LG004 | 100% | 100% | 4 | 0 | 0 |
| LG005 | 100% | 100% | 5 | 0 | 0 |
| LG006 | 100% | 100% | 5 | 0 | 0 |
| LG007 | 100% | 100% | 5 | 0 | 0 |
| LG008 | 100% | 100% | 2 | 0 | 0 |
| LG009 | 75% | 100% | 3 | 1 | 0 |
| LG010 | 100% | 100% | 2 | 0 | 0 |
| Overall | 91% | 100% | 40 | 4 | 0 |
General-ML corpus — 10 snippets (LG003/LG004/LG010): Precision 88%, Recall 100% (TP 7 / FP 1 / FN 0).
Combined: Precision 90.4%, Recall 100% (TP 47 / FP 5 / FN 0).
Recall is 100% on this corpus — every adversarial miss exposed has since been fixed
(constant propagation, hand-rolled normalization, cv=<int>, drop-based selection).
Leak shapes not yet in the corpus are still missed — see Known Limitations below.
y —
pure AST cannot distinguish a target column from a feature.cross_val_score(...) with cv omitted (defaults to KFold).df.loc[:, 'col'] = ... or df.assign(col=...).These sets are pinned in tests/test_benchmark.py: any new miss or silent fix fails the
suite until docs and corpus are updated to match.
The scanner core lives in leakguard/core/ (pure, no MCP imports); server.py and
cli.py are thin wrappers. Each rule has a fixture pair under tests/fixtures/.
Contributions welcome: new corpus snippets (especially real bugs you've hit) strengthen the benchmark more than new rules do.