Skip to content

Sting25/ai-coding-rules-scaffold

Repository files navigation

ai-coding-rules-scaffold

Latest release License: MIT

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.

Why this exists

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Philosophy

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.

Install

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-scaffold

See Releases for available tags.

From your project root:

~/src/ai-coding-rules-scaffold/install.sh

The 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 usage

At 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/JS

Pairing with Husky / lefthook

If your project already uses Husky or lefthook, install.sh detects the existing core.hooksPath and won't overwrite it. Two ways forward:

  1. Switch to .githooks — point core.hooksPath at .githooks and migrate any existing hooks into it. Simplest if your existing hooks are minimal.
  2. 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.

What lands in your project

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

AI agent integration

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.md natively. Nothing else needed.
  • Claude Code — reads CLAUDE.md. install.sh drops a one-line CLAUDE.md containing @AGENTS.md, which pulls AGENTS.md into context.
  • Aider — add to .aider.conf.yml:
    read:
      - AGENTS.md
      - coding-rules.md
      - operational-rules.md
  • Cline — create .clinerules with one line:
    Follow the rules in AGENTS.md, coding-rules.md, and operational-rules.md.
    
  • Continue / Copilot / other — point the tool at AGENTS.md via whatever config it supports.

Use the rules without the rest of the scaffold

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:
    @operational-rules.md
    @coding-rules.md
    
    The @ 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.

Scaling context across a large codebase

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.

What the tooling enforces

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)

Per-line escape valve

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.

Verify it works

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()

Customize per project

  • 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 — the Project section is meant to be edited: stack, entry points, gotchas. Keep it tight; agents reread it on every turn.
  • .forbidden-patterns/*.txt — simple regex|description lines. 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 — enables E,F,I,W,B,UP,SIM,PTH,ANN,BLE,C90,PL,PT,RUF. Trim ignore = [...] if a rule fights your style.
  • Pre-commit hookMAX_LINES=500 by 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_LINES higher temporarily in both the hook and CI, then ratchet it down as you refactor.

Update & uninstall

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 customizations

A 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, patterns

Safe 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.

Platform notes

  • 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 +x is a no-op on NTFS, but Git for Windows treats shell scripts in .githooks/ as executable regardless.

What this scaffold deliberately omits

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

Using this without an AI

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.

License

MIT — see LICENSE.

About

Two-layer enforcement (pre-commit hook + CI mirror) for small teams using AI agents. Catches debug leaks, file growth, secrets, and forbidden patterns before they merge.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages