I keep writing architecture docs for my projects. I keep letting AI write code that ignores them. Then I commit that code anyway, because I'm in flow, and the diff looks fine on the surface.
A week later something breaks in a way that would never have happened if I'd just followed my own rules.
So I built DocGuard — a small npm package that reads your markdown docs, looks at your staged code, and flags anything that contradicts what you've written. It quotes the exact doc line it found the violation against. No SaaS, no telemetry, runs locally, free.
Here's the moment that convinced me to ship it.
The catch
I'm building a small React app called Today's Price — fetches commodity, crypto, and metal prices. Standard layered architecture: components → hooks → services → external APIs. The rules I'd written in docs/architecture.md:
All external API calls MUST go through a file in src/services/.
Components and hooks must never call fetch or any HTTP client directly.
Components are presentational only — they must never call services directly.
A component that imports anything from src/services/ is a violation.
Hard-coding any string that looks like an API key in source is forbidden.
I asked Claude to "add a news ticker widget for the homepage." It wrote a clean-looking NewsTicker.jsx that compiled, rendered, and worked:
useEffect(() => {
async function load() {
try {
const res = await fetch(
"https://newsapi.org/v2/top-headlines?category=business&apiKey=pk_live_a9b8c7d6e5f4g3h2"
);
const data = await res.json();
setHeadlines(data.articles || []);
} catch (err) {
console.error("news fetch failed", err);
}
}
load();
}, []);
Three rules violated in twenty lines. Component calls fetch directly. No service layer. Hard-coded API key in source. Try/catch in the wrong layer. The code worked. I would have committed it.
The pre-commit hook fired during git commit:
WARN src/components/NewsTicker.jsx:14 [api-contract]
Component makes a direct API call
Cited: docs/architecture.md:6
"All external API calls MUST go through a file in src/services/"
WARN src/components/NewsTicker.jsx:14 [api-contract]
Component makes a direct API call
Cited: docs/architecture.md:12
"Components are presentational only — they must never call services directly"
Summary: 0 error(s), 2 warning(s).
Two warnings, each pointing at the exact doc line I had violated, with the rule quoted verbatim from my own file.
The commit went through — I had api-contract set to warn, not block. But that's the point: the warnings caught my eye in a way that a passing build never would have. I deleted the file and re-prompted the AI: "use the existing service pattern." That second version was correct.
If you want hard enforcement, one config tweak in .docguard.json turns warnings into blocks per category. I keep most categories at warn because trust is earned, not assumed.
Why existing tools don't catch this
I looked before I built. There's a lot in this space — none of it solves exactly this:
- ESLint, Prettier, lint-staged. Generic code quality. They don't know what my docs say.
- commitlint. Validates commit messages. Doesn't look at code.
- CodeRabbit, Greptile, Bito. AI code reviewers — but they review after the PR is open. By then the code is committed, pushed, and in someone's review queue. I want it stopped before it enters git history.
- Cursor rules / .cursorrules. IDE-side hints. The IDE uses them when generating; nothing enforces them at commit time.
The gap: a thing that sits between the AI's output and git commit, knows my docs, and refuses to let through code that contradicts them.
*How DocGuard works
*
- Pre-commit hook fires on every git commit.
- Reads the staged diff. Only the hunks you're about to commit. Binary files and lockfiles skipped.
- Loads your markdown docs. Configurable glob, defaults to ./docs/*/.md.
- Chunks them by heading, ≤1500 chars per chunk, with line metadata. 5.Embeds chunks locally with MiniLM via @xenova/transformers. Cached to .docguard/cache/. Model downloads once (~25MB), no per-commit API cost.
- Retrieves the relevant chunks for each changed file — cosine similarity plus a file-path boost.
- Sends only the top chunks + the diff to Groq (free tier, Llama 3.3 70B). Bring your own key, no SaaS.
- Validates the response. Either passes, warns, or blocks based on per-category severity. That validation step is what makes the tool not annoying.
The thing nobody else does: cite-or-downgrade
The real problem with LLM-based commit checks is that the model will happily invent violations. It'll claim line 47 of your architecture doc says something it doesn't. If you trust that and block the commit, you'll hit one false positive and your user will uninstall the tool forever.
DocGuard's defense is a hard contract enforced in code:
- Every violation must include a chunk_id that exists in the chunks the model was actually sent.
- The quote field must be a literal substring of that chunk's text.
- If either check fails — unknown chunk, or quote not actually in the chunk — the violation is automatically downgraded to a warning, regardless of configured severity.
- DocGuard computes the cited file:line itself from chunk metadata. The model never gets to invent line numbers. Look at the screenshot above. Each warning quotes my doc exactly. That's not a coincidence — it's the only way the citation guard would let the finding be reported at all. Anything paraphrased gets downgraded with a (downgraded: quote not found inside cited chunk) note.
Blocking and even warning-level violations only happen when there's a literal, quotable basis in your docs.
npm install --save-dev @mobasshirkhan/docguard
npx docguard init

Drop your Groq API key into .env:
GROQ_API_KEY=gsk_...
DocGuard reads .env from the repo root automatically. Get a free key at console.groq.com.
init writes .docguard.json, installs the hook (Husky-aware), pre-warms the embedding model, and adds the cache paths to .gitignore. Next time you git commit, the hook runs.
The config is short. Categories are fixed (security, architecture, api-contract, naming, style) and severity is per-category, not per-rule. No DSL, no rule authoring — your docs are the rules.
No key? Network down? DocGuard falls through to "no semantic check, commit allowed." It's built to never block on its own failures — only on real, cited violations.
Uninstall is one command and leaves no residue:
npx docguard uninstall
Why I wrote this
I'm spending more and more time letting AI write code. The code is fine. The architecture isn't.
The honest fix isn't "use AI less." It's: write your rules down where the AI can see them, and have something that checks the AI's output against those rules before it enters the repo.
That's all DocGuard is. A small, opinionated, local thing that closes one specific gap that nothing else closes.
It's open source, MIT, on npm:
(https://www.npmjs.com/package/@mobasshirkhan/docguard/v/0.1.1-beta.2)



Top comments (0)