Every team has a senior developer who, during code review, writes comments like:
"We never use axios directly here — go through the apiClient wrapper."
"All errors need the AppError class, never throw raw strings."
"Files are kebab-case in this project, not camelCase."
None of these rules are in a linter. None are in a README. They live in one person's head.
When that person leaves — or when an AI assistant writes code — they get violated constantly.
I built a Claude Code skill that finds these rules automatically, writes them to a machine-readable profile, and enforces them every time Claude generates new code. I called it Codebase DNA Guardian.
Here's how it works, what I learned building it, and a real scan on a 1,388-file production codebase.
The core idea: surgical sampling, not full reads
Reading an entire codebase is wasteful and slow. A 500-file TypeScript project would blow through any context window before you extracted a single pattern.
The insight: you don't need all 500 files. You need:
- Config files (3–5):
tsconfig.json,.eslintrc,pyproject.toml— these tell you the rules the team agreed on - Entry points (2–3):
main.ts,app.ts— bootstrap patterns, global state setup - One vertical slice (4–6 files): pick the most "average" feature and read
route → service → repository → test. This single slice reveals more about a project's conventions than 50 random files. - Error handling samples (2–3): grep for
catch,throw,AppError - Tests (2–3): framework, mocking strategy, file naming
15–20 files total. Enough to extract reliable patterns across 8 categories.
What the scan produces
After running /dna-scan on a project, the skill writes .claude/dna.md:
| ID | Tier | Rule | Example | Counter-example |
|----|------|------|---------|---------------|
| A1 | 🔴 HARD | Frontend HTTP via frappe-ui createResource — no raw fetch/axios | `createResource({ url: 'hrms.api...' })` | `axios.get(...)` |
| E1 | 🔴 HARD | Python: frappe.throw() + _() i18n on all user errors | `frappe.throw(_("Invalid date"))` | `raise Exception("Invalid date")` |
| D4 | 🟢 PREF | Date handling: dayjs (frontend) / frappe.utils.getdate (Python) | `dayjs()` | `moment()` |
Every rule has a severity tier:
| Tier | What Claude does |
|---|---|
| 🔴 HARD | Stops. Explains the rule. Asks you to confirm before proceeding. |
| 🟡 SOFT | Writes compliant code, adds a one-line footnote. |
| 🟢 PREF | Silently applies it. You never see it. |
Once .claude/dna.md exists, Claude reads it before every code generation — no extra commands needed. The DNA is always active.
Real scan: frappe/hrms (1,388 files, 18 sampled)
I ran it on frappe/hrms, an open-source HR & payroll system. Python backend + Vue 3 PWA frontend + TypeScript roster service — three languages, three frameworks, one monorepo.
18 files sampled. 19 rules extracted. Scan time: under 3 minutes.
Results:
Consistency Score: 90/100
🔴 HARD rules: 10 — all passing ✅
✅ A1 frappe-ui createResource — no raw fetch/axios found
✅ D2 Ionic Vue (IonPage/IonContent) on all views
✅ E1 frappe.throw() + _() on all Python errors
✅ T1 IntegrationTestCase — frappe.tests used correctly
🟡 SOFT drift: 1
⚠️ E3 frontend/src/components/FormView.vue:536,563
console.log() on error paths — should be console.error()
🧟 Zombie dependency:
⚠️ html2canvas@^1.4.1 — declared in package.json, zero imports found
The zombie dependency alone is worth the scan. html2canvas is 0.8 MB. It ships to every user. Nobody imports it.
The dashboard
The project includes a React dashboard that renders the scan results visually — animated score ring, per-service consistency bars, zombie tracker, evolution sparkline.
It's driven by a DNA_DATA object your scan populates. Run /dna-preview inside Claude Code and it bundles, serves, and screenshots it automatically.
Monorepo support
For multi-service projects, the scan runs in waves:
Wave 1: root — shared config, CI, shared utilities
Wave 2: per-service — one vertical slice each
Wave 3: cross-comparison — diff patterns across services
Wave 4: classify — intentional divergence vs accidental drift
Output goes to .claude/dna/root.md + per-service overrides. Resolution order: service DNA > root DNA. If aegis.md says "use JOSE" but root.md says "use jsonwebtoken", Aegis wins for code inside that service.
What I found wrong in my own skill
Before publishing I ran a rigorous code review — 7 independent reviewer angles, 11 confirmed bugs. The most instructive ones:
Shell operator precedence:
# WRONG — -maxdepth 2 only applies to the first -name clause
find . -maxdepth 2 -type f -name "*.json" -o -name "*.toml" -o -name "*.yaml"
# CORRECT
find . -maxdepth 2 -type f \( -name "*.json" -o -name "*.toml" -o -name "*.yaml" \)
Without the parens, -o breaks the -maxdepth 2 constraint for .toml and .yaml. In any real project this recurses into node_modules/ and returns thousands of files.
GNU grep alternation:
# WRONG on Linux (BRE mode — matches the literal string "catch|Error|throw")
grep -rl "catch\|Error\|throw" src/
# CORRECT — use ERE mode
grep -Erl "catch|Error|throw" src/
Two-dot vs three-dot git diff:
# WRONG — includes commits on main that aren't in the branch
git diff main..feature/my-branch --name-only
# CORRECT — compares against merge-base
git diff main...feature/my-branch --name-only
Finding three shell-level bugs in a skill that's entirely instructions (no executable code) was a reminder: AI agent instructions are code. They have the same failure modes as programs.
Install
npx skills add mturac/codebase-dna-guardian
Or via npm:
npm install -g codebase-dna-guardian
GitHub → mturac/codebase-dna-guardian
The skill is MIT licensed, includes AGENTS.md for Cursor/Copilot/Windsurf compatibility, and ships with the React dashboard component.
The thing I keep coming back to: every codebase has a senior developer embedded in it — in the file history, in the naming patterns, in the error handling strategies. This skill just reads it.


Top comments (0)