code-guardrails
Catch silent fallbacks and test doubles that AI coding tools sneak into your production code.
AI tools generate working code fast. But behind the scenes, they swallow exceptions with pass, leave mock objects in production, and paper over failures with ?? "default". Miss it in review, and it ships.
code-guardrails parses every file save with a Rust engine plus ast-grep and warns Claude immediately when these patterns appear.
Install
Prerequisites: Claude Code, ast-grep 0.14+, ripgrep 14.0+, Rust 1.77+
brew install ast-grep ripgrep
curl https://sh.rustup.rs -sSf | sh
Marketplace
Run inside Claude Code:
/plugin marketplace add Wisteria30/code-guardrails
/plugin install code-guardrails@code-guardrails-marketplace
Restart Claude Code. Verify with /scan.
Share with your team (optional)
cp -Rf ~/.claude/plugins/code-guardrails .claude/plugins/code-guardrails
rm -rf .claude/plugins/code-guardrails/.git
git add .claude/plugins/code-guardrails && git commit -m "chore: add code-guardrails plugin"
Add CLAUDE.md rules (recommended)
The hook detects violations after the fact, but CLAUDE.md prevents them from being written in the first place. Add the following to your project's CLAUDE.md:
## AI Code Policy
code-guardrails hook is active. Every Edit/Write is scanned for violations.
- NEVER write `except: pass`, empty `catch {}`, or `.catch(() => null)` — log the error and re-raise or wrap
- NEVER use `mock`, `stub`, `fake` identifiers in production code — test doubles belong in test files only
- NEVER add silent defaults (`.get(key, default)`, `?? value`, `|| value`) without spec approval — fail explicitly or mark with `# policy-approved: REQ-xxx <reason>`
- NEVER leave TODO/placeholder/stub implementations — implement fully or raise NotImplementedError
- Unspecified fallbacks are bugs. If the spec doesn't say "default to X", don't default to X
Adapt the rules to your project. The key is specific, verifiable constraints — vague guidance like "handle errors properly" doesn't change behavior.
What It Catches
Test doubles in production code
Flags mock / stub / fake identifiers and unittest.mock imports in non-test files. Test files are always ignored.
# NG — mock left in production code
mock_client = MockHttpClient()
from unittest.mock import patch
# OK — inside test files (test_*.py, **/tests/**, etc.)
mock_client = MockHttpClient()
Unapproved fallbacks
Flags patterns that silently swallow errors or substitute default values.
Proper error handling (logging, re-raise, error wrapping) is not flagged.
Python
# NG — swallowed exception
try:
connect()
except ConnectionError:
pass
# NG — silent defaults
timeout = config.get("timeout", 30)
name = user_name or "unknown"
port = os.getenv("PORT", "8080")
val = getattr(obj, "attr", None)
# NG — suppressed error
with contextlib.suppress(KeyError):
process(data)
# OK — exception handled properly
try:
connect()
except ConnectionError as e:
logger.error(f"Connection failed: {e}")
raise ServiceUnavailable("DB unreachable") from e
# OK — .get() without a default
value = config.get("timeout")
# OK — or in a condition, not an assignment
if user_name or fallback_name:
greet()
TypeScript
// NG — silent defaults
const port = config.port ?? 3000;
const name = input || "default";
options.timeout ||= 5000;
cache ??= new Map();
// NG — swallowed errors
try { await fetch(url); } catch (e) { return []; }
try { parse(json); } catch {}
fetch(url).catch(() => null);
// OK — exception handled properly
try {
await fetch(url);
} catch (e) {
logger.error(e);
throw new FetchError("request failed", { cause: e });
}
// OK — catch with re-throw
promise.catch((e) => { throw new AppError(e); });
Approval model
Intentional fallbacks can be approved with an adjacent comment:
# policy-approved: REQ-123 explicit locale default
lang = payload.get("lang", "ja-JP")
// policy-approved: ADR-7 demo-mode fallback
const label = apiValue ?? "demo";
Accepted prefixes: REQ-, ADR-, SPEC- followed by an identifier.
How It Works
| Trigger | Behavior |
|---|
| PostToolUse hook | Scans the changed file after every Edit / Write. Warns Claude on violations |
/scan command | Full project scan on demand |
17 rules (9 Python + 8 TypeScript), validated against 34+ test fixtures.
The hot path uses a Rust engine, and full-tree scans use ripgrep to prefilter candidate files before invoking ast-grep.
Test paths (**/test/**, **/tests/**, **/*_test.py, *.test.ts, etc.) are excluded from all rules.
Detection Rules
Python