{"slug": "claude-md-no-silent-failures-testing-anti-entropy-etc", "title": "CLAUDE.md -- no silent failures, testing, anti-entropy, etc.", "summary": "A developer has published a CLAUDE.md-style engineering guideline document arguing that software systems should fail loudly rather than silently fall back to defaults, and that decisions should be committed to the repository rather than living only in chat sessions. The guidance calls for crashing on programmer errors, validating input only at IO boundaries, encoding units and constrained values in types, and using failure-aborting gate-then-commit chains.", "body_md": "- Push back on genuinely bad ideas, with reasoning. Point out bugs, misleading names, and better approaches when you see them. Direct but collaborative.\n- If requirements are ambiguous or a design decision could reasonably go multiple ways, ask rather than guess. A quick question is cheaper than reworking a wrong assumption.\n- **If a prompt looks damaged or wrong, STOP and say so** — truncated mid-sentence,\nduplicated blocks, garbled copy/paste, references to context that doesn't exist, or\ninstructions that contradict prior decisions without acknowledging they do. Do not\nexecute a best-guess reconstruction. A mangled prompt executed faithfully is worse\nthan a delay.\n- **Decisions live in the repo, not in chat.** When a ruling or plan change arrives\nmid-session, write it into the durable work file (progress notes, this file, the\nrelevant spec) and commit before executing it. If the session ended the moment\nafter the message was read, the repo alone must be enough to act on it.\n\nCrashes make bugs obvious and fixable. Silent fallbacks make bugs hard to find. Fail loudly when something goes wrong — never hide bugs behind default values or fallback behavior.\n\n- If a condition indicates a programmer error, crash (throw/panic/assert). Do NOT\nsilently fall back to a default. `port = config.port ?? 8080` hides missing config;\nassert it exists instead.\n- Do not type values as optional/nullable when they are always expected to be present. Use direct access and let violations crash — that's a bug to fix, not a case to handle.\n- Do not add defensive code for \"impossible\" cases. If a branch should be unreachable, fail with an error saying so — never a silent default. Use exhaustiveness checking in switches/matches over closed sets so adding a variant breaks the build, not the runtime.\n- Do not catch errors that indicate bugs. If parsing internal data or indexing a structure you just built can fail, that's a bug — let it crash. Empty catch blocks are forbidden.\n- Every raised error includes what went wrong and the offending values:\n`Unknown effect type \"reverb2\" in project \"demo\"` , not`invalid input` .\n- Validation happens at IO boundaries (file load, network, IPC, user input) with strict schemas — reject bad data with specifics, never repair it. Past the boundary, data is trusted and invariants are asserted, not handled.\n- Environmental failures (disk full, permission denied) are hard errors surfaced to the user, naming the operation and the OS error. Never continue in a silently degraded mode. Report the error you observed; don't speculate about causes you didn't measure.\n- Shell scripts use `set -euo pipefail` .\n- Gate-then-commit chains must be failure-aborting: `check && commit` , never`check; commit` — a red gate must make the commit unreachable, not optional.\n\n**A type's signature must guarantee that it can only be used correctly.** Making\ninvalid states unrepresentable is the best-known instance, but the principle is\nbroader: design every API so the misuse you would otherwise document, review for,\nor debug is instead a compile error or unwritable. If correct use depends on a\ncalling convention the signature doesn't force, redesign the signature. (In\ndynamically-typed languages, apply the same principle via validating constructors,\nruntime schemas, and lint rules — the enforcement mechanism changes, not the goal.)\n\n- Constrained values get their own types with validating constructors — a normalized 0…1 parameter, an ID, a hash. Construction is the only way in, and construction fails loudly on violation. Raw numbers/strings are for values with no invariant.\n- Parse, don't validate: unvalidated input crosses the boundary exactly once, becoming rich domain types. Downstream functions accept only those types, so \"forgot to validate\" cannot be written.\n- Put units in types (`seconds` ,`ticks` ,`pixels` ,`cents` ). A unit mixup must fail\nto typecheck. Names alone are NOT enough for domain quantities — use nominal/branded\ntypes or wrapper structs with unit conversions as the only constructors. Distinguish\nquantities that look alike but make different claims (integer grid positions vs.\ncontinuous positions).\n- State that must change together lives behind one object whose methods preserve the invariant; no naked setters that can desynchronize sibling fields.\n- An operation that can legitimately refuse returns a result the caller must explicitly handle — never a boolean the caller can ignore. (Bugs still crash.)\n- Detect \"did X happen\" by reading a direct fact — a monotonic counter, an identity — never a proxy (stack depth, array length, a timestamp) that can alias under saturation or reuse.\n- Resource lifetimes: when a lifetime is lexical, expose only a bracket construct (acquire, run, release in finally/defer/RAII) so leaking is unwritable. When a lifetime spans events, acquisition goes only through a shared lifecycle primitive whose signature demands everything abnormal-end handling needs; release on every exit path is that primitive's tested contract, not each call site's memory.\n- Cross-cutting policies (write gating, locking, validation, sanitization) are enforced at ONE structural chokepoint that all call sites flow through, with tooling making bypass a build failure — never by remembering to add a guard at each site. Per-site discipline produces endless hole-patching; a chokepoint makes the next hole impossible to write.\n\nWrite tests for all new functionality. Tests must rigorously verify intended behavior — vague assertions are worse than no test because they give false confidence.\n\nTests ship **in the same commit** as the code they cover — a feature without tests is\nincomplete work, not a follow-up task.\n\n- Assert exact expected values, not loose predicates like \"contains 'error'\".\n- **Cover every legitimate use case.** Enumerate the distinct ways a real caller\nexercises the feature — the happy paths, plural — and test each explicitly. One\nhappy-path test plus ten edge cases is under-tested where it matters most.\n- Test the actual contract: exact outputs, exact error messages, boundary conditions, failure modes — alongside, never instead of, the legitimate-use enumeration. Aim for branch coverage.\n- **Every claimed invariant is a property test.** Any \"never\"/\"always\" in a comment,\ncommit message, or issue resolution must exist as a test spanning the full input\nregime — including regime boundaries and crossings, which is exactly where\nhand-picked small-perturbation examples pass while the claim is false. Use\nproperty-based generation for numeric or structural domains: examples prove\nexistence; properties prove claims. Finiteness (no NaN/Inf) is part of every\nnumeric claim.\n- If a function should fail on bad input, test that it fails with the expected message.\n- **A comparison test proves nothing unless its output is SENSITIVE to the behavior\nunder test.** Saturated values, all-zero outputs, and round-trips through lossy\nidentity all pass for broken code. After building a fixture or golden reference,\ninspect what it actually produces and verify a plausible bug would move the\nresult — sensitivity is checked empirically, never assumed.\n- **Attribution is measured, never inferred.** After each fix, re-run and record what\nactually changed. \"These failures share my hypothesized cause\" is an experiment to\nrun, not a deduction to make.\n- **When refactoring, implement the change first — against the spec or reference\nbehavior — and only then run the tests as independent checks on finished work.** Never let a refactor emerge from fixing failing tests one by one: with \"make this\ntest green\" as the goal, every edit bends toward whatever the code currently does,\nand drift flows through the sanctioned channel — setup changes — so the suite ends\nup green while certifying bugs. Tests steered by the work they check are not checks.\n- Never make a test pass by weakening its assertions. Test failures are information: discuss with the user before changing either the test or the code.\n- Run tests yourself when possible, but every run must be bounded and exit — no orphaned watch modes, dev servers, or background processes. Ask the user to run anything that requires infrastructure you shouldn't start.\n- Verify UI or visual work by actually looking at output (screenshots, rendered results), not by assuming.\n\nWrite code to high-assurance standards. Code involving cryptography, authentication, parsing untrusted input, crossing process/FFI boundaries, spawning processes, filesystem access, or concurrency requires extra care and scrutiny.\n\n- Prefer secure-by-default designs over manual discipline at every call site: templating that escapes by default, parameterized queries, schema validation on arrival at every trust boundary, centralized path resolution and checking.\n- Never build shell strings, HTML, or queries from data. Spawn processes with argument arrays; render text as text.\n- Executable content and data content are different things. If a format is supposed to be data, it must never gain an eval path, dynamic import, or plugin hook for user-supplied code — that boundary is what makes untrusted content safe, and breaking it is never acceptable.\n- For cryptography: do not implement primitives or protocols — use established, audited libraries. Assume side channels exist: constant-time comparisons for anything secret-dependent, and never expose key material in errors, logs, or debug output.\n- Adding a dependency means trusting its authors with arbitrary code execution — supply-chain risk is real. Only use well-known, actively-maintained packages; for anything less established, ask the user first.\n- Think adversarially about your own designs and code. After each commit, review the diff for security issues relevant to the changed code (injection, path traversal, XSS, unvalidated input at boundaries, auth gaps, hardcoded secrets) and report findings before continuing.\n\n**Anti-Postel: be strict in what you accept AND strict in what you emit.** The\nrobustness principle is how format ambiguity, parser divergence, and security bugs\nare born. Accept exactly what the spec defines and reject everything else with\nspecifics; emit exactly one canonical encoding, never \"whatever happens to parse.\"\nLeniency in a parser is not kindness — it silently becomes part of the format,\nbecause whatever you accept, someone will ship.\n\n- Reject, don't repair: malformed input is an error naming what's wrong, never a best-effort fix-up. There is no \"probably meant\" branch.\n- No undocumented acceptance: if the parser takes it, the spec says so. If the spec doesn't say so, the parser rejects it.\n- Canonical output: one valid encoding per document. If the format promises canonicality, test that re-encoding is byte-identical.\n\n**Compatibility is a promise you make explicitly, not a default you drift into.**\nDecide per project which surfaces carry a compatibility promise (shipped file\nformats, wire protocols, public APIs) and which are internal and freely changeable.\nFor internal surfaces, the delete-don't-deprecate rule applies: change them and\nupdate every consumer in the same commit. For surfaces that DO carry a promise:\n\n- Breaking changes happen only behind an explicit version bump, and are always announced, never incidental: named in the commit message and called out when reporting the work.\n- **Changes to promised surfaces stop for user sign-off.** Present the exact\nproposed change (signature, field, semantics) and wait for approval before it\nlands. No proceed-and-inform-later.\n- Keep a fixture corpus: real artifacts from every supported version live in the repo, and each must load (and round-trip byte-identically where promised) as a permanent test. A fixture is never deleted or regenerated to make a test pass while its version is still supported.\n- Round-trip property: `decode(encode(x)) ≡ x` over generated valid inputs, not just\nhand-picked examples.\n- Every constant the format commits you to lives in one module under a snapshot test — changing one without a version bump in the same commit is a test failure, never a quiet edit.\n- Specs ship test vectors: a clean-room re-implementer must be able to verify their implementation without reading your code.\n- Determinism is tested, not assumed: same inputs ⇒ bit-identical outputs, proven by differential tests; no wall-clock reads or unseeded randomness in deterministic paths.\n- Crash consistency: persistence writes are atomic (temp + rename, stated fsync policy); every reader has torn/truncated/garbage fixtures proving corruption becomes a boundary rejection — never a crash, never a silent repair.\n\n**Mass edits are never done with a regex, and the context of any edited code must be\nunderstood before it is edited.** Edit site by site: read each one, know what it\nmeans, change it deliberately. Pattern *searching* to find candidate sites is fine —\nthe ban is on pattern-driven *writing*. A regex matches a shape; what matters is the\nmeaning, and identical text can mean different things in different domains. Only\nreading the call site tells them apart.\n\nA codebase built fast rots in predictable ways. These rules stop each one.\n\n**One way to do each thing.**\n\n- Before writing any function, component, or pattern, search for an existing one that does the job. Extend or reuse it; never write a parallel implementation.\n- Finding two near-duplicates makes unifying them part of the current task, not a someday-cleanup.\n- One canonical name per domain concept, everywhere. Never introduce a synonym for an existing concept; a new concept gets a named type in the core layer first.\n- Constants and magic values have one home. Repeating a literal is a bug waiting to desynchronize.\n- The same goes for every shared surface, not just literals: option lists, validation bounds, schemas, vocabularies. Define once; every consumer imports or composes it. A second dialog, boundary, or validator must never restate its own copy — even a copy that looks locally complete will silently drift.\n\n**Delete, don't deprecate.**\n\n- When a project has no external consumers of an interface, changing it means updating every call site in the same commit — no compatibility shims, re-export layers, or deprecation markers.\n- Replaced code is removed in the commit that replaces it. No commented-out blocks,\nno unused exports, no `-old` /`-v2` files. Version control history is the archive.\n\n**No premature abstraction.**\n\n- Write the concrete version first. Extract an abstraction only when the second real use exists — not when you predict one. Interfaces with one implementation, factories, managers, and generic parameters \"for flexibility\" are slop.\n- No configuration options, feature flags, or fallback paths nothing uses. Every branch must be reachable by a real requirement.\n\n**Type honesty.**\n\n- Escape hatches that silence the type system (`any` , unchecked casts, non-null\nassertions,`unsafe` ) are forbidden or require a comment stating why they're safe.\nPrefer type guards and schema-validated parsing at boundaries. Silencing the\nchecker is hiding a bug.\n\n**Comments.**\n\n- Comments state invariants, constraints, and non-obvious *why* — never*what* the\nnext line does, never narration, never history (that's the commit message). Most\ncode should need no comments because the names carry the meaning.\n\n**Files and structure.**\n\n- When a file grows past a few hundred lines, look for the module boundary trying to get out — but don't shatter code into fragments either; a file holds one coherent concern.\n- Respect the project's dependency direction (e.g. UI depends on core, never the reverse; core stays free of platform concerns).\n- Import from the defining module; avoid barrel/re-export layers that hide structure and breed cycles.\n- No ad-hoc documentation litter: durable docs live in the designated docs location, working state in the designated progress file. No scratch SUMMARY/NOTES/PLAN files.\n\n**Consistency beats local taste.**\n\n- Before writing in any area, read the neighboring code and match its patterns. If a pattern deserves changing, change it everywhere in a dedicated refactor commit — never fork a second style alongside the first.\n\n**Gardening is part of every milestone.**\n\n- A milestone isn't done until: no dead code, no known duplicated logic, no lint suppressions without justification, sane file sizes, dependency rules passing. Entropy is removed on the spot, not logged for later.\n\n- **Naming** : clear, descriptive names that read as plain English —`remainingAttempts` over`rem` ,`decodeStemFile()` over`procF()` . Abbreviations\nonly when universally understood (`id` ,`url` ,`config` ).\n- **Functional style** : prefer map/filter/reduce or iterator chains over manual loops\nwith mutable accumulators when they make intent clearer. Don't force it when a loop\nreads better (hot paths often should be plain loops with no per-iteration\nallocation).\n- **No incomplete code** : no TODO stubs or placeholder implementations. Every piece\nof code ships complete and functional. If a task is too large, discuss scope\nreduction rather than writing skeleton code.\n\nMake clear, atomic commits for every logical unit of work. Don't batch unrelated changes.\n\n- Start the message with a verb: Add, Fix, Update, Remove, Refactor.\n- Be concise but specific: `Add onset envelope to modulation sources` , not`Update code` .\n- Commit before moving on to the next task.\n- Never stage blindly: no `git add -A` /`git add .` . Stage explicit paths for\nexactly the files the commit is about, and read`git status` before committing.\nThe user's working files must never enter commits, gitignored or not.\n\n- Never mark an issue or task completed without explicit user verification. Resolve individual points, but closure requires the user's confirmation.\n- Always test changes before claiming they work; describe expected behavior and ask the user to verify.", "url": "https://wpnews.pro/news/claude-md-no-silent-failures-testing-anti-entropy-etc", "canonical_source": "https://gist.github.com/defuse/da01a26ffd94018043799bcd9400ecc1", "published_at": "2026-08-26 02:55:38+00:00", "updated_at": "2026-09-21 00:24:49.660352+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-md-no-silent-failures-testing-anti-entropy-etc", "markdown": "https://wpnews.pro/news/claude-md-no-silent-failures-testing-anti-entropy-etc.md", "text": "https://wpnews.pro/news/claude-md-no-silent-failures-testing-anti-entropy-etc.txt", "jsonld": "https://wpnews.pro/news/claude-md-no-silent-failures-testing-anti-entropy-etc.jsonld"}}