A Python project template that makes Claude Code follow engineering discipline. Design docs before code. Tests before implementation. Three layers of AI guardrails so the agent doesn't drift over long sessions.
Opinionated, not prescriptive. Workflow defaults (TDD, DDD, branch protection, CI) are opt-in at init time — decline any that don't fit your team.
| Layer | Tool / Convention |
|---|---|
| Dependency management | uv — fast, per-service virtualenvs |
| Venv auto-activation | direnv — cd into a service, venv activates automatically |
| Linting & formatting | ruff (E, F, I, B, UP, RUF rules) |
| Type checking | mypy strict mode |
| Code quality | pylint (min score 10, McCabe complexity, duplicate-code detection) |
| Pre-commit hooks | All of the above + optional TDD enforcement + optional branch protection |
| CI (optional) | GitHub Actions — quality gate + per-service test matrix |
| Design workflow (optional) | Document Driven Design (DDD) with design, solution, and ADR templates |
| AI conventions | CLAUDE.md + .claude/{DDD,TDD}.md — NumPy docstrings, RED/GREEN TDD, DDD workflow, SOLID |
| AI guardrails | 3 layers: hooks (mechanical), pylint similarities (deterministic), opt-in /review agents (semantic) |
| Setup wizard | init.py — interactive, replaces all placeholders, self-deletes |
brew install uv direnv python@3.12
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc # or ~/.bashrc for bashClick Use this template on GitHub, then clone and enter your new repo:
git clone https://github.com/<you>/<your-project>.git
cd <your-project>python3 init.pyFive question categories: project identity, first service, code quality thresholds, workflow toggles (TDD, DDD, branch protection, GitHub Actions), and final confirm. Decline any toggle your team doesn't use.
make init
direnv allow .Installs dev deps + git hooks and approves the root direnv.
cd services/<your-service>
direnv allow .The service venv auto-activates from now on. No manual source .venv/bin/activate.
LLMs drift over long sessions — duplicate logic, mismatched parameter names, missed reuse, doc / code drift. The template ships three complementary layers, each tuned to a different cost / depth tradeoff. All three are removable later (see To disable below).
Claude Code hooks in .claude/hooks/, wired via .claude/settings.json:
- Reuse reminder before edits —
PreToolUseonEdit/Write/MultiEditinjects: grep for an existing utility first, match parameter names of nearby functions, refactor rather than duplicate. - Auto-
/simplifyon stop — when Claude tries to stop with uncommitted changes, blocks once-per-session and nudges to run/simplifyfirst. A per-session marker prevents looping after/simplify's own edits.
pylint similarities (configured in pyproject.toml) flags 6+ identical lines across files, ignoring imports/signatures/comments/docstrings — so logic duplication can't slip past pre-commit. The rest of .pre-commit-config.yaml handles formatting, types, and TDD enforcement.
Read-only specialist agents in .claude/agents/, orchestrated by the /review slash command. Three agents run in parallel and return a ranked report:
- dry-reviewer — duplicate logic, parameter signature drift, missed reuse. Catches what
pylint similaritiescan't: semantic duplication in different shapes. - simplicity-reviewer — YAGNI violations and the six over-production traps (
while-I'm-here,for-future-flexibility,defensive-coding,modernization,consistency,cleanup). - docs-sync — design / solution / ADR / CLAUDE drift vs current code (the one rule we explicitly couldn't enforce mechanically).
Invoke /review at the end of a feature, before a commit, or before a PR. They never fire automatically — see .claude/agents/README.md for cost, model selection, and how to add an agent.
- Hooks: edit
.claude/settings.json(or delete the file). - Pylint similarities: remove the
[tool.pylint.similarities]section frompyproject.toml. - Agents: simply don't invoke
/review— they're never auto-fired.
These are template defaults — selected (or declined) during python3 init.py. Skip the ones your team doesn't use; the rest still work independently.
- Before code (if DDD enabled) — copy
.claude/designs/_template.md, fill it, get sign-off, then implement. Full rules: .claude/DDD.md. - While coding (if TDD enabled) — RED → GREEN → REFACTOR. Pre-commit blocks a
src/commit without a matchingtests/test_<module>.py. Full rules: .claude/TDD.md. - After shipping (if DDD enabled) — solution docs auto-generate when a design doc hits
status: shipped. See .claude/solutions/README.md. - Non-obvious decisions (if DDD enabled) — capture as ADRs in .claude/decisions/.
make init # full bootstrap (install + git hooks)
make lint # run all pre-commit hooks on every file
make hooks-update # update pre-commit hooks to latest versions
make clean # remove __pycache__, .mypy_cache, .ruff_cache, .pytest_cache
/review # spawn DRY + simplicity + docs-sync agents on uncommitted changesAdding a new service
cp -r _service-template services/<new-service>
# rename src/service_module → src/<new_module>
# update services/<new-service>/pyproject.toml name + packages
# add "<new-service>" to the matrix in .github/workflows/ci.yml
cd services/<new-service> && direnv allow .Lint and type checks auto-discover the new service. scripts/lint_service.py (invoked by the mypy-services and pylint-services pre-commit hooks) groups staged files by service root and runs mypy/pylint inside each service's own venv via uv run --directory. No .pre-commit-config.yaml edits needed.
Troubleshooting Claude Code
CLAUDE.md loads once at session start. If Claude isn't following it:
- Verify it's at the repo root, not a subdirectory.
- Start a new session —
CLAUDE.mdreloads. - Re-anchor mid-session: "Re-read CLAUDE.md and confirm the conventions before continuing."
CLAUDE.md instructs Claude to scan .claude/designs/ before each implementation task. If skipped, run /clear to reload, or re-anchor: "Check CLAUDE.md's DDD rules and follow them before continuing."
DDD is two-phase: open exploration, then 3 targeted rounds (gaps, edge cases, constraints). If Claude skips: "Stop. We haven't done the two-phase questioning yet. Start with exploration."
The settings watcher only sees .claude/settings.json if it existed when the session started. Run /hooks once (opens the menu, reloads config) or restart the session.
Maintainer note: After pushing your template, go to Settings → General → check "Template repository" so the green "Use this template" button appears for visitors.