# Rules from Claude's mistakes

From https://www.vibeapp.win/rules. Each rule came out of something Claude got wrong while building real apps, and links the post that shows it. Written with Claude; Henning Malmin answers for it. Licensed CC BY 4.0: use it freely, with a link back.

## A check that could never fail

A guard, watchdog, filter or test that looked like protection but could never trigger, or sat on a path the code no longer took.

- Check hiding rules against the real on-disk spelling: write covers with the real spelling, match excludes case-insensitively, and use ASCII mount names with a junction when needed. ([why](https://www.vibeapp.win/posts/windows-docs-and-docs-same-folder-linux-container-did-not))
- A revision may add to the published record, never edit or remove it silently. The autopilot compares text, not the revision's own description, and pages with nothing new are skipped. ([why](https://www.vibeapp.win/posts/autopilot-revision-dropped-a-published-point))
- Decide duplicates by the commits a post cites, not by id or title: a published post citing more than half of a draft's commits makes it a duplicate, and publishing stops at two shared commits. ([why](https://www.vibeapp.win/posts/engine-retold-the-same-mistakes-under-new-ids))
- Measure what the watchdog is meant to detect, test it against a known failure before trusting its green, treat silence as a symptom, and count a fault only when the system could have acted and didn't. ([why](https://www.vibeapp.win/posts/watchdogs-that-could-never-fire))
- A watchdog must measure the outcome, not the setup. Count subscribers and check that recent alerts went out. Feed that to fault tracking and show it somewhere that doesn't depend on the checked channel. ([why](https://www.vibeapp.win/posts/alert-channel-dead-seven-days-status-green))
- Every filter or check needs a test showing it can fail, and `|| true` should not be used by default. ([why](https://www.vibeapp.win/posts/sitemap-filter-that-could-never-filter))
- A decision isn't a gate until code enforces it: freeze the exam outside training, make training refuse to run without it, include misses, and measure with the production code path. ([why](https://www.vibeapp.win/posts/the-gate-was-never-code-and-pointed-the-wrong-way))
- Verify a change alters behaviour in the running code, put guards in the one place every action passes through, and use a sensitivity row when a setting might never bind. ([why](https://www.vibeapp.win/posts/guards-and-dials-that-never-ran-in-the-live-path))
- Put the guard in the one function every order goes through, for both buys and sells, check our own order reference, count orders from the same cycle, and don't rely on the broker. ([why](https://www.vibeapp.win/posts/double-order-guard-in-the-legacy-path))
- Verify against markup only the tested thing can produce, such as the border-b-2 class, and count the elements (four buttons). A string that can appear elsewhere on the page proves nothing. ([why](https://www.vibeapp.win/posts/verification-matched-page-title-tab-bar-missing))
- If a check cannot fail on the errors you fear, it is not a check; CI now boots the server and tests real endpoints, and you should ask what change would turn a green step red. ([why](https://www.vibeapp.win/posts/ci-syntax-check-could-not-catch-broken-server))

## Failing silently

Something broke and reported success, or an error was swallowed into a normal-looking empty state.

- Every failure shows as a banner on every admin page, raised with raise() and cleared with resolve(). A watchdog also checks outcomes every ten minutes, such as whether the post went out. ([why](https://www.vibeapp.win/posts/dead-instagram-token-two-days-of-missed-posts))
- Pick sitemap children by freshest lastmod, decompress by content-encoding and strip null bytes, commit work in the same session, and treat a source returning zero every time as a symptom. ([why](https://www.vibeapp.win/posts/three-weeks-of-a-source-delivering-nothing))
- Give every silent failure mode its own alert, check the model call log after each run, and make sure a test backs any comment about what should wake you. ([why](https://www.vibeapp.win/posts/auth-failure-recorded-as-successful-run))
- Build with visible errors, and before deciding a fix doesn't work, check that it is actually in the output. ([why](https://www.vibeapp.win/posts/the-fix-that-was-never-built))
- Run against the real token first, treat success with an empty result as unverified, and change the design rather than widen the token. ([why](https://www.vibeapp.win/posts/rest-design-one-permission-token))
- A test now checks the journal against the SQL files in both directions. After a migration, query for the thing it should have created. ([why](https://www.vibeapp.win/posts/migration-skipped-silently-crashed-trading-cycle))
- Never show absence as all ok. Use a typed map over every verdict value so a new value breaks the build, and give abnormal states an exit code. ([why](https://www.vibeapp.win/posts/missing-fields-rendered-as-all-is-normal))
- Healthy does not mean the backup works, so check backup freshness and test restores into an empty database. Lock shell scripts to LF in .gitattributes and show backup age in the app. ([why](https://www.vibeapp.win/posts/image-backup-silently-dead-seven-weeks))
- Keep errors apart from empty results and throw when everything fails; a silent-zero sweep now flags all-zero columns unless registered with a reason and expiry date. ([why](https://www.vibeapp.win/posts/errors-swallowed-into-legitimate-empty-state))
- Don't use an `as` cast to force a field into existence; add the field to the query's select and let the type system check it, so a missing field becomes a compile error. ([why](https://www.vibeapp.win/posts/type-cast-hid-missing-field-empty-result-column))
- For every constant that is a point in time, check it is in the past and whether a wrong value gives a wrong or a missing answer; if missing, add a guard, a visible health check and a failing test. ([why](https://www.vibeapp.win/posts/strategy-epoch-15-hours-in-the-future))

## Measuring the wrong thing

A number that answered a different question than the one asked, and made things look better or worse than they were.

- A group flag must separate the group's own movement from signal quality, and stay quiet if results flip with signal direction. Flag text may only state what the system actually does. ([why](https://www.vibeapp.win/posts/self-audit-paris-exchange-beta-not-signal-failure))
- When a data source is shared between sites, filter every query on hostname (requestHost_in). Then check that the paths, referrers and hostnames in the results can exist on your site. ([why](https://www.vibeapp.win/posts/status-page-counted-another-sites-traffic))
- Count totals from the source, here git, with method and date stored beside them. Hand-written page facts are only a fallback, and items with no number must show as missing, not drop out of the sum. ([why](https://www.vibeapp.win/posts/front-page-said-across-4-apps))
- A status check should measure the outcome, not the setup: read whether recent alerts were delivered, add a second place to show failure, and let the channel re-subscribe itself. ([why](https://www.vibeapp.win/posts/alert-channel-dead-seven-days-status-read-configuration))
- Use the mean as the null for excess return and the median only for hit rate. Placebo and time-travel checks now run as standing audit guards: measure the measuring setup, not just the sources. ([why](https://www.vibeapp.win/posts/placebo-median-benchmark-52-percent-false-edge))
- Test every check on real data, match names as whole words with Unicode-aware lookaround, and treat noise in a check as a bug. ([why](https://www.vibeapp.win/posts/quality-gates-flagged-correct-text-real-error-got-through))
- When a labelling error is found in training data, check the exam for it and rebuild the exam from corrected rows. Mark the score break, since older regression and background scores no longer compare. ([why](https://www.vibeapp.win/posts/exam-poisoned-by-same-wrong-boxes-as-training-set))
- Run the detector on the full frame plus four overlapping quadrants, send empty results to a second check, and treat comments about what a model sees as unverified until measured. ([why](https://www.vibeapp.win/posts/comments-claimed-enough-pixels-detector-squeezed-4k-to-320))
- When you write that we "have" data, check the database, not the code. Where a rule can be made runnable, make it a check, such as the new six-check docs command run by a Stop hook. ([why](https://www.vibeapp.win/posts/docs-claimed-nine-metrics-database-held-two))
- A row is not a decision: deduplicate on the decision (with the replacement instrument in the key) before counting. Show row count beside decision count, and add an audit check that fails if they drift. ([why](https://www.vibeapp.win/posts/exit-measurement-counted-simulations-not-decisions))
- Group by decision and count distinct instruments. Use the same yardstick for every source. Verify a measure against a case where you know the answer. ([why](https://www.vibeapp.win/posts/measurements-that-answered-the-wrong-question))
- A baseline must be prequential, per tier, measured on the same rows as the model, and reported with the row count, or say there are too few rows. ([why](https://www.vibeapp.win/posts/baseline-made-model-look-12-times-better))
- Check that a number means what its label says, including scale and definition, and compare numeric values as numbers, not strings. ([why](https://www.vibeapp.win/posts/badge-counted-55-bottles-score-shown-as-out-of-5))

## Written from memory

Names, facts or API details written from what the model believed instead of checked against the source.

- Check every factual claim against the primary source, comments included. If the source says planned, write planned. If there is no source, leave the claim out. ([why](https://www.vibeapp.win/posts/calendar-comment-claims-not-supported-by-sources))
- Take names from the source's own page, find feed URLs with a tool, and take version-specific facts from the installed version. ([why](https://www.vibeapp.win/posts/everything-written-from-memory-was-wrong))

## Settings that did nothing

A setting, weight or input that existed in the code and the interface but never reached the decision.

- Delete dead code and correct or mark the documentation as historical in the same change, so nobody debugs a component that does not run. ([why](https://www.vibeapp.win/posts/dead-llm-ranking-code-that-looked-like-it-was-running))
- Measure each dial's effect through the engine with impact, add a sensitivity row, and check that every code path passes the value. ([why](https://www.vibeapp.win/posts/dials-and-weights-that-never-took-effect))

## Fine in the test, broken in production

Handled in a smoke test, a local run or one environment, and missing where it mattered.

- Test both Start and Stop on an app with a non-ASCII path before calling a Compose feature done. Turn bake off for such paths and use a config-only composeFiler list when the override is dev-only. ([why](https://www.vibeapp.win/posts/start-failed-where-stop-worked-non-ascii-folder-dev-override))
- Synthetic time series in tests need real timestamps and a real calendar, with weekends and holidays. When you check a measurement, count it per day or per weekday: a gap that repeats on a fixed period is a calendar bug, not missing data. ([why](https://www.vibeapp.win/posts/every-wednesday-missing-from-the-five-day-measurement))
- Check the built result, check that canonicals answer without redirect, and check shared defaults on the pages nobody opens. ([why](https://www.vibeapp.win/posts/canonical-urls-pointed-at-redirects))
- When removing an article for an error, replace every field that repeats it, including the lead, and keep a regression test that checks this. ([why](https://www.vibeapp.win/posts/unpublished-article-kept-error-in-description))
- Clear every table tests write to, between every test, whatever production retains. Write down exceptions, and make the reset refuse to run unless the database is the test one. ([why](https://www.vibeapp.win/posts/tests-measured-a-cache-that-was-never-cleared))
- Run tiling on every frame next to the box crop and keep the best result. Test the path production takes, with the inputs production sends. ([why](https://www.vibeapp.win/posts/tiling-worked-in-test-never-ran-in-production))
- A known limitation handled only in the test tool is not handled, so the fix goes into the production path: an engine pause between orders and a 429 retry. ([why](https://www.vibeapp.win/posts/rate-limit-handled-only-in-smoke-test))
- Build and deploy the whole stack together, read new backend fields defensively as optional, and wrap the routes in an error boundary. ([why](https://www.vibeapp.win/posts/version-skew-blank-health-page))
- Open connections with autocommit on, use explicit transactions for writes, and verify saved data through a separate connection set up the way production sets it up. ([why](https://www.vibeapp.win/posts/production-writes-silently-rolled-back-tests-could-not-see-it))
- Declare any package needed at import time explicitly with a comment saying why, and let a clean CI install judge whether the dependency list is complete. ([why](https://www.vibeapp.win/posts/green-locally-red-in-ci-undeclared-transitive-dependency))

## Data read the wrong way

Units, matches, merges or escaping that turned correct input into wrong data.

- Use concrete words, not short stems. Test each entry on words it should match and on names and everyday words it should leave alone. Write the list in the text's alphabet and keep one shared list. ([why](https://www.vibeapp.win/posts/rus-stem-surname-wine-reel-sober))
- Any text reaching a channel, generated or typed, goes through that channel's preparation step right before sending. Length limits are checked after it, on the exact outgoing text. ([why](https://www.vibeapp.win/posts/hand-edited-captions-bypassed-escaping))
- Write only the keys the caller actually sent, using settingsPatch() and not settingsSchema.partial(). Read the server's answer, not the component's state, before calling it saved. ([why](https://www.vibeapp.win/posts/every-save-reset-every-other-setting))
- Know what a timestamp records before you judge by it. Share the marker between the code that writes a row and the check that reads it, and show what a check leaves out instead of hiding it. ([why](https://www.vibeapp.win/posts/booking-time-read-as-event-time))
- The server copy is the source of truth at startup. Seed from local only when the server has nothing, never after a network error, and keep one shared sync hook. ([why](https://www.vibeapp.win/posts/sync-merged-lists-brought-back-deleted-items))
- Match text against keys using the longest key and word boundaries, and test short keys, substring keys and keys inside unrelated words. ([why](https://www.vibeapp.win/posts/brand-matching-wrong-brand-and-inside-words))
- When you add a new dimension such as variants, review every place that uses the old id as a key. Store the new level so old saved data still works. ([why](https://www.vibeapp.win/posts/marking-one-variant-as-owned-marked-the-whole-brand))
- Use every field to tell candidates apart and check its unit, set a confidence floor before linking, and don't fall back to name search when an existing link can't be fetched. ([why](https://www.vibeapp.win/posts/centiliters-parsed-as-milliliters-wrong-bottle-size))

## Words that stopped being true

A promise, label or message that was right when written and quietly became wrong when the system changed.

- Alert text must describe what actually happened in that case. The alert function now takes an optional title, and a reused title must be checked against each new case. ([why](https://www.vibeapp.win/posts/auth-alert-said-run-stopped-but-it-hadnt))
- Don't assume a static site is rebuilt daily. Compare the story date with today's date in Oslo time, and if they differ or can't be checked, show "Siste sak" with the date. ([why](https://www.vibeapp.win/posts/todays-story-label-showed-two-day-old-story))
- When a feature changes, search every text that promises something about it and update them together. Check absolute claims against what the site loads, and state the exception. ([why](https://www.vibeapp.win/posts/about-page-privacy-promise-broke-with-analytics))

## No plan B when a dependency fails

A call to a model, service or disk with no timeout or fallback, so one failure took the whole feature down.

- Enrichment must never block. When it fails, degrade with degraded=True, tell the user, and renormalise the remaining weights so scores and thresholds keep their meaning. ([why](https://www.vibeapp.win/posts/pairing-500-when-local-embedding-model-was-down))
- Give every external call an explicit timeout, even local ones. Write files atomically: write to a temporary file, then rename it into place. ([why](https://www.vibeapp.win/posts/local-llm-calls-had-no-timeout))

## The wrong diagnosis

A fix aimed at the obvious suspect, while the real cause was somewhere else.

- What gets approved must be what gets sent: rebuild the Reel when the draft changes, hold a Reel with no script for a human, and allow quote cards only for text the story marks as a quote. ([why](https://www.vibeapp.win/posts/previewed-reel-was-not-the-reel-that-shipped))
- Give every verb a time limit and kill the whole process tree. A lock counts only with a fresh heartbeat. Log errors instead of crashing. Portal reads the heartbeat and says when the guard is down. ([why](https://www.vibeapp.win/posts/job-guard-looked-dead-while-alive))
- For a job tied to a time of day, don't use an interval timer. Tick every ten minutes, let the job check the clock against its slot, and post only once per day. ([why](https://www.vibeapp.win/posts/hourly-timer-counts-from-container-start))
- Read the error text before trusting the status code, let the most specific signal decide, use 429 only as fallback, and add a test that names the case that went wrong. ([why](https://www.vibeapp.win/posts/a-429-that-meant-quota-empty-was-retried-as-rate-limit))
- Never mark a draft as given up for a reason that says nothing about its text. If quota or login fails, stop the run and resume three hours later from where it stopped. ([why](https://www.vibeapp.win/posts/usage-limit-message-treated-as-doubt-about-the-text))
- Run heavy work in a child process and give long jobs their own queue. When an external failure repeats weekly, check our own job history for what ended then before blaming the supplier. ([why](https://www.vibeapp.win/posts/weekly-search-idle-priority-blocked-broker-login))
- Verify in the built result, not just in the source. Check the patched index.html in the build and fetch the real manifest over HTTP. ([why](https://www.vibeapp.win/posts/home-screen-icon-missing-page-icon-was-not-the-cause))
- Every serve must trace back to a producer source with a URL, because comparing with a secondary source, or finding it too poor to use, is not verification. ([why](https://www.vibeapp.win/posts/ten-signature-serves-wrong-until-checked-against-producers))

## Other

Mistakes that do not fit the patterns above.

- Give a model that writes one item at a time the recent history as data, and make the fallback rules avoid the same repeats. ([why](https://www.vibeapp.win/posts/director-gave-four-of-six-reels-same-gold-light))
- Refuse to write an already-written candidate, save marks under a lock on a freshly re-read list, and detect duplicates by shared commits rather than title, slug or id. ([why](https://www.vibeapp.win/posts/parallel-writers-overwrote-marks-duplicate-posts))
- Write shared state under a lock and re-read the list right before saving, never from a copy loaded before slow work. Refuse to write a candidate that is already written. ([why](https://www.vibeapp.win/posts/three-parallel-writers-overwrote-each-others-marks))
- Use one key per entity everywhere. A new page isn't done until it is linked in and indexed, which includes a check in URL Inspection. ([why](https://www.vibeapp.win/posts/person-pages-competed-with-tag-pages-plates-differed))
- Log the aiming decision at info, zoom only after a box was seen, stop when the box matches the last round, and wait for focus to settle with a ceiling instead of a fixed pause. ([why](https://www.vibeapp.win/posts/camera-aimed-twice-at-stale-box-zoomed-blind))
- Store the forecast that was actually used next to the prediction it fed, and always compare against a simple persistence baseline. ([why](https://www.vibeapp.win/posts/prediction-ignored-turnover-average-week))
- Use placeholders, not examples, in prompt schemas. Add a deterministic echo-guard, prefer null to a guess, validate against known bottles, and check every prompt when one copies. ([why](https://www.vibeapp.win/posts/prompt-examples-copied-verbatim))
