Two-layer enforcement (pre-commit hook + CI mirror) for small teams using AI agents — catches debug leaks (print, console.log, breakpoint, pdb), unbounded file growth, nested-if hell, silenced exceptions, hardcoded secrets/tokens, and stray .env or private-key files before they merge. The same lib/check-* scripts run in both layers, so the hook and CI can't drift apart and --no-verify doesn't become the escape hatch.
Agent-agnostic: works with Cursor, Claude Code, Copilot, Cline, Aider, or no AI at all. Built for Python/FastAPI + optional TypeScript/React projects; adapt freely for other stacks.
This scaffold came out of working on a large federated geospatial pipeline — Python/FastAPI backend with TypeScript on the front, agents writing in both. The intended audience is small teams (2–5 devs) using Claude Code or a similar agent, often with the AI filling the senior-engineering role on a real codebase.
That setup hits four compounding failure modes that ordinary linting alone doesn't catch:
-
AI writes inconsistent or conflicting patterns across sessions. A teammate prompts the agent Monday and it picks one convention; on Wednesday, a different teammate prompts the agent on the same area and it picks a different one. Without machine-checkable rules, the codebase grows three flavors of the same thing — different error-handling shapes, different import styles, different naming. Tools that fail the build on rule violations are the only thing that survives across sessions.
-
Files grow unboundedly. Agents add to existing files rather than extract new modules — every request becomes a new function in the same file. Past a certain size the agent can no longer fit the file in context, and the bugs that follow are subtle (the agent can't see the whole file either, so it stops noticing the duplication and inconsistency it introduced). The 500-line cap is calibrated well below that threshold so extraction stays cheap.
-
Debug statements ship silently.
print(),console.log,breakpoint(),pdb.set_trace()— agents add them while diagnosing a bug and forget to remove them on the way out. They survive code review because they look like intentional logging at first glance. Commit-time rejection is the only layer that catches them every time. -
Forbidden patterns recur. Agents reach for old import paths, deprecated service names, and outdated idioms because their training data still has them. A per-stack regex deny-list (
backend.txt,frontend.txt,secrets.txt,shell.txt) is the only durable fix — the agent can't be talked out of recurrent muscle memory, but the build can fail on it.
This scaffold ships the enforcement layer that addresses all four directly. Two layers are live: commit-time (the pre-commit hook) and merge-time (the CI mirror), both running the same lib/check-* scripts. A third layer — agent-runtime hooks that block bad patterns before they're written — is deferred; see RECOMMENDATIONS.md for the design space and tradeoffs.
What the scaffold doesn't try to solve: parallel-session collisions, context-window discipline across long projects, and spec-first workflows. Those belong to git workflow (git worktree per session), nested CLAUDE.md files, and project-specific spec docs respectively. Recommended patterns for each are documented in AGENTS.md and RECOMMENDATIONS.md.
Short doc rule list humans remember + full tool enforcement for the rest. If the build breaks on ruff C901, the fix is forced — no one needs to remember that nested-if depth matters.
The file-size rule (max 500 lines) is the one rule to never raise. Every other rule has tradeoffs in specific cases; unbounded file growth is how projects rot.
Enforcement runs in two places, sharing the same scripts:
- Pre-commit hook — blocks the commit locally. Fast feedback, skippable with
--no-verify. - CI workflow — blocks the PR server-side. Unskippable.
Both invoke the same four lib/check-* scripts (check-size, check-patterns, check-filenames, check-secrets). The hook and CI can't drift apart because there's nothing to keep in sync — they call the same code. Each script is also runnable on its own (git ls-files | .githooks/lib/check-secrets), so you can wire it into Husky, lefthook, or any other orchestrator without rewriting the logic.
Clone the scaffold somewhere stable:
# Recommended: pin to a tagged release for reproducibility
git clone --branch v0.5.2 https://github.com/Sting25/ai-coding-rules-scaffold ~/src/ai-coding-rules-scaffold
# Or track main if you want the latest changes
git clone https://github.com/Sting25/ai-coding-rules-scaffold ~/src/ai-coding-rules-scaffoldSee Releases for available tags.
From your project root:
~/src/ai-coding-rules-scaffold/install.shThe script auto-detects Python (pyproject.toml / requirements.txt / setup.py) or frontend (package.json) and installs the matching pieces. If neither is present, it exits — pass the stack explicitly:
./install.sh --python # Python only
./install.sh --frontend # TS/JS only
./install.sh --both # both stacks
./install.sh --force # overwrite existing files
./install.sh --no-verify # skip the post-install linter check
./install.sh --help # show usageAt the end, install.sh verifies that ruff and/or eslint are installed and that their configs load. If either is missing, it prints the install command.
Install the linters:
pip install ruff # Python
npm i -D eslint @eslint/js typescript-eslint # TS/JSIf your project already uses Husky or lefthook, install.sh detects the existing core.hooksPath and won't overwrite it. Two ways forward:
- Switch to
.githooks— pointcore.hooksPathat.githooksand migrate any existing hooks into it. Simplest if your existing hooks are minimal. - Chain — keep your existing tool and have it invoke the scaffold hook as a step. Husky example:
# .husky/pre-commit .githooks/pre-commit
Either way, the four lib/check-* scripts in .githooks/lib/ are also runnable directly (git ls-files | .githooks/lib/check-secrets), so you can wire them into any orchestrator.
| Scaffold file | Installed as | Purpose |
|---|---|---|
AGENTS.md.template |
AGENTS.md |
Primary agent doc: git discipline + project section |
CLAUDE.md.pointer |
CLAUDE.md |
One-liner pointing Claude Code at AGENTS.md |
coding-rules.md |
coding-rules.md |
Short list of code-level rules that aren't tool-enforceable |
operational-rules.md |
operational-rules.md |
Process and collaboration rules — failure modes that no linter can catch |
ruff.toml.template |
ruff.toml |
Python lint config |
eslint.config.js.template |
eslint.config.js |
TS/JS lint config (flat config, ESLint 9+) |
githooks/pre-commit.template |
.githooks/pre-commit |
Hook orchestrator — invokes the four lib/check-* scripts |
githooks/lib/check-{size,patterns,filenames,secrets}.template |
.githooks/lib/check-{size,patterns,filenames,secrets} |
Reusable check scripts; the same scripts run from CI so hook and CI can't drift |
.github/workflows/lint.yml.template |
.github/workflows/lint.yml |
CI mirror — invokes the same lib/check-* scripts as the hook |
forbidden-patterns/backend.txt.template |
.forbidden-patterns/backend.txt |
Python patterns consumed by hook + CI |
forbidden-patterns/frontend.txt.template |
.forbidden-patterns/frontend.txt |
TS/JS patterns consumed by hook + CI |
forbidden-patterns/secrets.txt.template |
.forbidden-patterns/secrets.txt |
Secret/credential patterns, scanned across all file types |
forbidden-patterns/shell.txt.template |
.forbidden-patterns/shell.txt |
Dangerous shell patterns (curl | bash, rm -rf /, chmod 777) for *.sh and *.bash |
Scripts (stay in the scaffold repo):
| Script | Purpose |
|---|---|
install.sh |
Copy templates into your project, wire core.hooksPath, verify linters |
uninstall.sh |
Remove unmodified scaffold files, unwire the hook |
The scaffold follows the cross-tool AGENTS.md convention — a single file at the project root that multiple agents already read (Cursor, Aider, and others). For tools that read a different filename, install.sh or a one-line pointer handles it:
- Cursor — reads
AGENTS.mdnatively. Nothing else needed. - Claude Code — reads
CLAUDE.md.install.shdrops a one-lineCLAUDE.mdcontaining@AGENTS.md, which pullsAGENTS.mdinto context. - Aider — add to
.aider.conf.yml:read: - AGENTS.md - coding-rules.md - operational-rules.md
- Cline — create
.clineruleswith one line:Follow the rules in AGENTS.md, coding-rules.md, and operational-rules.md. - Continue / Copilot / other — point the tool at
AGENTS.mdvia whatever config it supports.
You can use operational-rules.md (and/or coding-rules.md) standalone, without the linter / hook / CI scaffolding. Drop the file(s) into your project root and reference them from your AI tool's config:
- Claude Code — add to
CLAUDE.md:The@operational-rules.md @coding-rules.md@directive auto-loads on session start. - Cursor / Aider / Cline / etc. — add the filename(s) to whatever config the tool reads every session (
.cursorrules,.aider.conf.yml,.clinerules).
No install.sh, no hooks, no CI — the docs are useful in isolation. The full scaffold layers on the enforcement (commit hooks + CI mirror) that turns the rules into machine-checkable failures.
Root-level AGENTS.md is reread on every turn, so its token cost is paid for every prompt. For codebases over ~50 files, drop a CLAUDE.md in each major directory (app/api/, app/web/, lib/) with area-specific gotchas. Claude Code reads the nearest one walking up from the file being edited — root-level context stays small, area context stays relevant. Same applies to Cursor's nested .cursorrules.
For parallel agent sessions, use git worktree add ../proj-feat-x -b feat-x so each session has an isolated working tree on its own branch. Two agents in the same checkout will overwrite each other.
The pre-commit hook now invokes ruff / eslint against staged files
when their configs are present and the tool is on PATH — so most of the
build-breaking rules below also fire at commit time, not only in CI.
Linters are silently skipped if not installed; CI is the authoritative
backstop.
Build-breaking (ruff / eslint, on every lint + commit + in CI):
| Concern | Rule |
|---|---|
| Nested control flow > 3 deep | ruff C901, eslint max-depth: 3 |
| Cyclomatic complexity > 10 | ruff C901, eslint complexity: 10 |
os.path.join / string path math |
ruff PTH100-208 |
Blind except Exception: pass |
ruff BLE001 |
| Missing public-API return types | ruff ANN201 |
| Function size > 80 statements (Python) / 80 lines (TS/JS) | ruff PLR0915 (max-statements), eslint max-lines-per-function |
| Too many branches in a function | ruff PLR0912 (max-branches) |
| Line length > 100 | ruff E501 |
| Unsorted / unused imports | ruff I, F401 |
any in TypeScript without comment |
@typescript-eslint/no-explicit-any |
Commit + CI-breaking (pre-commit hook + lint.yml):
| Concern | Check |
|---|---|
print(), breakpoint(), pdb.set_trace(), ipdb.set_trace() in Python files |
regex |
console.log / debugger / alert in TS/JS |
regex |
| File size > 500 lines | wc -l per staged file |
| TODO/FIXME without ticket ref | regex (opt-in; commented in template) |
| Secret / credential leaks (AWS keys, GitHub tokens, private keys, URLs with embedded credentials, hardcoded password=/token= assignments) | regex (case-insensitive, all files) |
Committed .env / *.pem / SSH private keys (id_rsa, id_ed25519, id_ecdsa, id_dsa) |
filename check (.env.example / .env.sample / .env.template allowed) |
When a regex match is intentional — a CLI entry point that needs print,
a docs example showing an AWS key prefix, a fixture with a synthetic
credential — append scaffold-allow (any case, in a comment) on the
matched line. check-patterns and check-secrets skip lines containing
the marker; check-filenames and check-size are file-level and
unaffected. See forbidden-patterns/README.md for examples.
Reviewers: every PR that adds or moves a scaffold-allow marker is
suppressing a guardrail. Treat new markers like new # noqas — confirm
the suppression is justified before approving. Audit the full set with
git grep -i scaffold-allow.
After install, confirm the hook rejects bad code:
echo 'print("test")' >> some_module.py
git add some_module.py
git commit -m "should be rejected"
# → hook prints: ✗ some_module.py: Use structlog (or the project's logger), not print()coding-rules.md— short by design. Add a "Project-specific" section at the bottom for stack rules (SQLAlchemy column quirks, import conventions, architectural constraints).AGENTS.md— theProjectsection is meant to be edited: stack, entry points, gotchas. Keep it tight; agents reread it on every turn..forbidden-patterns/*.txt— simpleregex|descriptionlines. Add deprecated import paths, old service names, etc. Lines starting with#are comments; an opt-in TODO/FIXME pattern is pre-seeded as a comment.ruff.toml— enablesE,F,I,W,B,UP,SIM,PTH,ANN,BLE,C90,PL,PT,RUF. Trimignore = [...]if a rule fights your style.- Pre-commit hook —
MAX_LINES=500by default. Override per-invocation:MAX_LINES=800 git commit. Edit the hook to change permanently. The CI workflow reads the same env var. - Adopting on an existing codebase — the CI size check runs against all tracked source files, not just changed ones. If the repo already has files over 500 lines, the first PR will fail. Either extract the offenders first (preferred — this is the debt the rule is meant to catch) or set
MAX_LINEShigher temporarily in both the hook and CI, then ratchet it down as you refactor.
Update: the project's configs are local forks of the templates. install.sh --force overwrites them, including any edits. Diff first:
diff ~/src/ai-coding-rules-scaffold/ruff.toml.template ruff.toml
# merge in the changes you want; leave your customizationsA git pull in the scaffold clone picks up new rules / patterns upstream.
Uninstall:
~/src/ai-coding-rules-scaffold/uninstall.sh # safe: only unmodified files
~/src/ai-coding-rules-scaffold/uninstall.sh --dry-run # preview
~/src/ai-coding-rules-scaffold/uninstall.sh --all # also nuke AGENTS.md, coding-rules.md, patternsSafe mode only removes files whose content matches the current scaffold template byte-for-byte, so local edits are never lost. AGENTS.md, coding-rules.md, and .forbidden-patterns/ are kept unless you pass --all. CLAUDE.md is treated as a regenerable pointer and removed if unchanged.
- macOS / Linux: first-class.
- Windows: use Git Bash or WSL. The pre-commit hook is
bash; Git Bash (bundled with Git for Windows) runs it fine.chmod +xis a no-op on NTFS, but Git for Windows treats shell scripts in.githooks/as executable regardless.
| Concern | Where it lives instead |
|---|---|
| Architecture / module boundaries | Your project spec or design doc |
| Framework-specific rules (React Query, specific import paths) | coding-rules.md "Project-specific" section |
| Test coverage thresholds, logging conventions | Per-project decision |
Formatter enforcement (ruff format, prettier) |
Drop-in if you want; the scaffold stays opinion-light here |
Spec-first workflow templates (SPEC.md) |
Out of scope — see RECOMMENDATIONS.md |
Claude Code agent-runtime hooks (.claude/settings.json PreToolUse) |
Deferred — see RECOMMENDATIONS.md for design space and tradeoffs |
git worktree orchestration for parallel agent sessions |
Documented in AGENTS.md; not automated |
The scaffold works fine without any AI tool. Drop the files in, run the hook — same enforcement. coding-rules.md is just a named place to put the rules humans should read.
MIT — see LICENSE.