Back to the work

How I build with Claude

Not a philosophy of AI. Six habits I picked up shipping TenantDesk, TallerDesk and the floor planner, kept because they kept paying off.

Write the rules down before the code

Every project I run with Claude keeps a rules file the model reads at the start of every session: what the project is, how it deploys, what must never happen. The first line of that file is a standing instruction, not a description — ask questions until the requirement is actually clear, rather than start from a guess.

## Working agreement
Do not change anything until the requirement is clear.
Ask questions until it is.

## Deploy
Always through the script. Never by hand.

## Must never happen
Secrets committed to the repo.
Manual edits applied straight to production.

The file grows with the project. When a decision gets made once, it goes in the file so it isn't re-litigated three sessions later.

Deploy through a script, never by hand

Every project has exactly one way to ship a change, and it isn't a person typing commands on a live system. The script runs the test suite first, tags the currently running build as the rollback target, uploads the new one, rebuilds, and rolls back on its own if the new container doesn't come up clean.

run test suite
tag current build as rollback target
upload new build
rebuild
if the new build fails to start:
  roll back to the tagged build automatically

That sequence is identical whether I write the change or Claude does. The script doesn't know the difference, which is the point.

Test what actually breaks

TenantDesk runs on around 3,870 tests, the floor planner on 565. Neither number is the interesting part — what matters is that the suites cover the paths that have actually failed before. A booking bot I built has a regression suite that drives the entire conversation end to end — problem description, extraction, slot offer, slot pick, booking, owner notification — against a disposable database that gets dropped and rebuilt on every run, with the language model swapped for a scripted stand-in keyed on the incoming prompt. No network calls, no flaky model output, the same result every time. See TallerDesk for the booking flow that suite covers.

Keep a changelog that explains itself

Every deploy gets an entry: the symptom that showed up, what actually caused it, where the fix went in, and what changed once it shipped. I record it in that order because it's the order I need when I come back to a decision later and can't remember why it was made. This one is from TenantDesk's own admin dashboard, which publishes the product's marketing blog in several languages — a part of the system the case study itself doesn't otherwise cover:

## 2026-06-12

### Publish step was skipping a configured language

Issue: A post approved for publishing went live in two of its three
languages. The publish step looped over a fixed pair of languages instead
of reading the agency's actual configured list, so the third came out as
an invisible draft every time, silently.

Fix (in the publish step): loop over the configured languages instead of
a hardcoded pair. Back-filled the one post already stuck in that state.

Impact: future approvals publish every configured language at once. No
more posts stuck half-published.

Work around the tool's own failures

This one isn't a technique, it's a plain workaround. One editing path silently replaced straight quote characters with curly ones inside a Python string, which broke the file the moment it was saved. The fix wasn't "be more careful" — it was making the deploy script itself repair the quotes and compile the file before anything gets uploaded, so a corrupted file simply can't reach production.

text = fix_curly_quotes(text)
compile(text)  # abort the deploy if this fails

It's a small defect and a small fix. I mention it because it's a fair example of the actual failure mode: not the model reasoning badly, but an editing mechanism doing something silent that a script downstream can catch and a person might not.

Separate what is measured from what is assumed

On the floor planner, every position in the model is either a real measurement taken on site or a placeholder standing in for one, and the two are never allowed to look the same. Assumed positions carry a distinct marker in the interface, and a running count of how many remain shows on the checks panel — because the concrete gets poured once, and a guess that quietly passed as a fact is the one mistake that can't be edited afterward.

doorPosition: 400      // measured on site
stackAngle: null       // ? not yet confirmed

What's left is judgment, and it's mine, not the model's. I decide the architecture — what talks to what, and what stays out of the codebase entirely, like a help desk inbox bought instead of built. I decide what gets built at all, and I say no to requests that would make a system harder to reason about for a feature nobody's asked for yet. The test suite is the actual gate before anything ships, not my read of a diff — it catches what I told it to check for, not what I haven't thought of.