Andy Stanish

June 28, 2026

Teaching Claude to Code Like Me

After 20+ years of writing code, I've got a pretty good sense of what I like and what works. So when I started having Claude write code alongside me, I wanted to make sure it built things the way I would — not the generic average of everything on the internet, but my way.

The only way to get there was to write it all down: a rules file Claude reads automatically on every task. That turned out to be the valuable part — a lot of what I'd carried around as "this just feels cleaner" had to get concrete enough for a machine to actually follow. Hand-waving doesn't compile.

It starts big-picture, in CLAUDE.md — here's the relevant parts:

# System Protocols

## Workflow & Planning
Complex features → design spec first. Small fixes/features → code directly.
Commit per logical unit; terse subject + a "why" body when the change isn't obvious.
Never rewrite history on a shared branch; if a force-push is unavoidable, use `--force-with-lease`.
Update README and relevant docs at each breakpoint.

## Environment & File Rules
4 spaces for new files; match existing indentation when editing. Follow language conventions (Makefile tabs, YAML 2-space).
Max line length 140 columns (code only); defer to any project formatter/.editorconfig.
Generate README if missing.
Create `.claudeignore` using @.claudeignore rules.
Create `.gitignore` with reasonable defaults. Enforce: `.idea`, `.memsearch`, `.firecrawl`.

## Architectural Principles
Modular and DRY (modularity wins ties).
Prefer functional paradigms: pure functions, immutability, composition over inheritance.
Twelve-Factor App methodologies (environment config, isolation, statelessness).
SOLID principles.
YAGNI: add abstraction, layers, or config only for a present need, never speculation. Match rigor to value; don't gold-plate.
Web/browser: prefer the platform (vanilla HTML/CSS/JS, Web APIs) over frameworks and runtime deps; add a dep only when it earns its place. Let modern-web-guidance pick the native feature.
Default to boring tech: when a dep/tool is warranted, prefer mature (good docs, track record, community) over new/shiny — maturity, not age.
Rob Pike's Five Rules of Programming:
  1. Do not assume bottlenecks; measure first.
  2. Do not tune for speed until measured and one part dominates.
  3. Fancy algorithms are slow when n is small (n usually is). Keep it simple.
  4. Fancy algorithms are buggy and hard to implement. Use simple data structures.
  5. Data dominates. Well-organized data structures make algorithms self-evident.

Then the nitty-gritty, in general.md:

# General code style

Cross-language principles. Language rules stack on top; CLAUDE.md holds architecture (functional paradigms, YAGNI, Pike, DRY).

## Mindset
* brevity is readability — optimize for the reader, who is often you in six months
* optimize for *readability over writability*; do the hard thinking at write-time so reading is cheap
* prefer *simple* (cheap to reason about correctly) over *easy* (cheap to write) — "hard and simple" beats "easy and complex"
* minimize accidental complexity (what the code imposes); spend the budget on the problem itself
* code is read in small chunks — structure every unit to fit in working memory: small, labeled, composable pieces
* keep code continuously clean — fix small messes before they rot

## Functions
* one concept per function; do exactly what the name says, nothing more — if the name feels dumb to type, it shouldn't be a function
* build a vocabulary of small, composable functions
* aim short; split when over ~20 lines or high cognitive complexity — but never split a single concept just to hit a number
* entry points (`main`, scripts) may stay long once pure logic is factored out — locality wins there
* limit heterogeneous positional params (≤3, hard max 4); beyond that use named/keyword args — meaningful names over meaningless positions
* prefer explicit inputs and outputs over hidden state mutation — you understand a unit by what goes in and comes out

## Control flow & shape
* flatten — deep nesting is a smell; use guard clauses / early returns, and isolate unavoidably deep logic in its own function
* keep conditionals short; don't mix `&&` and `||` in one test — extract or split
* prefer declarative (*what*) over imperative (*how*); prefer `map`/`filter`/`reduce` (pure, produce output) over `forEach`/loops (side-effecting)
* break long call/method chains into well-named intermediate variables or helpers
* favor familiar, consistent patterns (least surprise); don't reach for exotic syntax/sugar that taxes the reader

## State & effects
* treat data as immutable — code as if you can copy but not change it (mutate during construction, then freeze)
* no action-at-a-distance: no global mutable state; behavior should be understandable by reading locally
* isolate side effects (I/O) at the edges; keep core logic pure
* keep variable liveness short — declare near first use, minimize the span; avoid long-lived cross-function mutable vars (use an object or refactor)

## Naming
* name by purpose — never meaningless names (`value`, `data`, `temp`)
* functions are verbs; variables are nouns; pluralize collections; single-letter names only in tight iteration
* use visually distinct names (avoid `i`/`j`, `item`/`items` confusion); never shadow
* descriptive names don't excuse bad design — and over-long names are bloat too
* `_`-prefix marks module-private
* sort alphabetically wherever order is otherwise arbitrary

## Abstractions
* build composable, *trustworthy* abstractions — ones that obey the laws they imply (consistent equality, no surprising special cases)
* a leaky or misleading abstraction or type explodes cognitive load; a lawful one reduces it

## Errors
* errors should never pass silently, unless explicitly silenced
* throw to let the caller decide — don't swallow at the point of occurrence
* catch only the specific condition you can handle; re-throw the rest
* don't return empty/`undefined` for an essential missing value — throw

## Comments & docs
* comment the *why*, not the *what* — if a "what" feels needed, first make the code clearer (hard math/algorithms/perf excepted)
* comment the non-obvious: anything a reader would ask about, anything you had to re-derive, non-specific catch blocks, and especially a hack you hadn't time to refactor
* don't omit comments out of staleness-fear — names go stale too; prefer assertions over comments documenting assumptions
* colocate docs with what they describe; read nearby commentary before editing
* cite sources (say what a URL is *for*); mark temporary/workaround code with removal criteria; flag non-obvious cross-file coupling on both sides
* generate API docs from source, don't hand-maintain a parallel copy; tests are documentation

## Testing
* new and refactored code gets meaningful tests; every production bug gets a regression test
* a bug is a signal of excess complexity — fix the root cause and structure, not just the symptom
* set up mocks/spies in the test that uses them (locality over DRY)

## Hygiene
* no dead or vestigial code (unused imports, params, variables)
* no stray debug output in committed code (`console.log`, `print`); error logging is fine
* LF line endings

Scoped by language

The two files above are the always-on layer — Claude reads them on every programming task, whatever I'm working on. But most of what I care about is language-specific, and I don't want the Python rules sitting in the context while I'm editing a stylesheet. So the language rules live in their own files under ~/.claude/rules/, each gated by a paths: glob in its frontmatter. Claude Code only pulls a file in when I actually touch something that matches — which keeps the context lean and the token bill down as a nice side effect, but mostly just keeps the guidance relevant to the code in front of it. Here's the Python one:

---
paths:
  - "**/*.py"
  - "**/*.pyi"
---
* always use uv
* ruff for format + lint
* annotate every function signature — all params + return (incl. `-> None`)
* avoid `Any` — prefer precise types, protocols, or generics; assume a strict checker (mypy/pyright)
* modern typing: built-in generics (`list[str]`, `dict[str, int]`) and `X | None`, not `List`/`Optional`

They stack, too. TypeScript inherits the JavaScript and object-shape rules by sharing their globs, then layers its own on top — so opening a .ts file loads the general principles, the JS rules, and these, and not a word about Python or CSS:

---
paths:
  - "**/*.ts"
  - "**/*.mts"
  - "**/*.cts"
---
* inherits js.md/objects.md (shared globs); TS-only additions below
* never `any` — default to `unknown`, then narrow
* lean on inference for locals; annotate boundaries (exported signatures, public APIs)
* never cast (`as`, `<T>`, `!`) to paper over a type — narrow, guard, or fix the source type

None of this is law. It's a set of defaults I trust enough to follow without re-litigating every time — which is the whole point. And to be clear, this is a post about teaching Claude, not about my semantics: I know full well that half of these are religious wars with no winner. I won't argue any of them with you — unless you're picking up the bar tab.

Influences

Some things that've influenced my thinking over the years: