# Claude Code Without the Hype: How This Site Gets Built

> How I build this site with Claude Code agents: worktrees, adversarial verifiers, a night queue and a smoke test. Plus the three-month contact form 500.

**Short answer:** I hand work off in batches, and each task goes to an agent in its own git worktree, and the roles run on models of different sizes and prices: one researches, one builds, one tries to break what was built, one fixes whatever survives, and an advisor looks at the whole batch. Big tasks queue up overnight as labeled GitHub issues. There is no CI. I run the checks locally, and after deploys that touch routing I run a smoke test by hand against production, for the things my machine can't see. I built that last step because the contact form returned a 500 for three months while every local check was green.

A quick note up front: an agent drafted this post from the repo; the incidents are mine. How it was made is further down.

The form failed on every submission from June 28, 2026 until October 6. Tests, types, build: all green on my machine the whole time. I saw nothing, because my machine couldn't see the failure. I found it on October 6. I honestly don't remember how, and I'm not going to make it up. What I do know is that nothing alerted me, because nothing was checking production.

The cause is almost embarrassing. The edge middleware forwards dynamic requests by fetching `/_render` on the same host. With `trailingSlash: "always"`, Vercel responded to that fetch with a 308 to `/_render/`. A GET follows a redirect fine. A POST with a streamed body can't be replayed, because the body has already been consumed. `GET /api/geo/` kept working. The form's POST didn't. The fix, on October 6, was a small build integration that puts the `/_render` route ahead of the trailing-slash redirect in Vercel's output config.

Neither a unit test nor the build ever sees that redirect: it's Vercel's routing, which runs on every Vercel deployment, previews included, and never on my machine.

What follows is what I built after that, plus what was already in place before it happened.

## With no CI, the gate is me before the push

This repo has no GitHub Actions. Everything runs locally, and Vercel builds `main` when I push. That's written down in `docs/CI_CD.md`.

A pre-push hook does the work. In order: `astro check`, the regex lint, `docs:check` (which fails when a doc cites a file that doesn't exist), the Vercel config check, the public-images check, the blog lint and validation, and the unit tests. If any of them fails, no push.

The unit suite is 475 tests in 33 files; measured on October 28, 2026, it takes 3 to 7 seconds depending on machine load. It's fast enough that every agent can run it after every change.

The full build isn't run per agent. It runs once at the end, on the merged result.

## The smoke test covers what my machine can't see

`pnpm smoke` hits the production routes whose behavior depends on Vercel's routing: the home page and the blog, the geolocation endpoint, the contact form and the firewall. For daytime work I run it by hand after any deploy that touches `src/pages/api/`, `astro.config.mjs` or `vercel.json`; the night agent runs it on its own, before and after each of its releases.

Two decisions that came out of using it:

- **It sends no email.** Both form POSTs either get caught by the time trap (no `ts` field) or fail validation. I'm checking that the route responds; whether Resend delivers is a separate question.
- **A 429 from the form is logged as a skip.** The firewall allows 5 contact-form POSTs per hour per IP, and each smoke run spends 2. Otherwise a third run within the hour would turn a healthy site red.

It only works from an IP with a firewall System Bypass: with bot protection on, a script gets the challenge regardless of its user agent, and a challenge tells you nothing about whether the site is healthy. To check a single route by hand without the bypass, use `vercel curl --deployment`. The script also keeps the request count low, because a burst of `curl` from my IP trips Vercel's DDoS mitigation and locks me out of my own site for a while.

## Each handed-off task gets its own worktree and five roles

Per task, the chain is short. A researcher on a small model returns facts with a source, the date it was read and a list of what it couldn't confirm. A builder on a mid-size model implements on an `agency/<id>` branch in a worktree of its own. A reviewer on a larger model is told to find what's wrong (I haven't measured how much it actually catches; more on that below). Only if a major or blocking issue survives does a fixer step in, in that same worktree. At the end an advisor reads the whole batch and tells me what to keep, what to cut and what to do next.

Three rules I don't bend:

- Verifiers are read-only. They look at the worktree; they don't touch it.
- In the normal pipeline, agents commit to their branch and nothing else. No push, no merge.
- No pipeline agent touches the main checkout. It stays on `main`, clean, and it's where I work: I do the merging there, run the full build once, then delete the worktrees and the merged branches. Anything I hold back stays on its branch.

That rule already existed, and an agent broke it. On the morning of October 28, 2026 I ran a daytime batch: three issues plus a type-check cleanup. The first run used the harness's built-in worktree isolation, which refused to start because `.claude` is a symlink in this repo. Meanwhile, a research agent I'd asked to do research only implemented a fix and left it on a branch in the main checkout. I caught it when I reviewed the batch. On the second run, the research prompt said explicitly that it was read-only, and each builder created its own worktree by hand in a sibling folder. I still check "don't touch main" with `git status`.

## The night queue: issues with a label

Small things go in `.skeptic/backlog.md`, one line each with how it's verified. Big things are GitHub issues labeled `night`. A night agent picks up both, builds each one with the same chain, and leaves a report for the morning. Unlike the normal pipeline it does publish what survives: it watches production for a while using the smoke routes and reverts if a route fails or errors climb.

The part I was most careful about is what stays out. Environment variables, secrets, deploy config, domains and billing are never built at night. They go to the backlog as proposals for a human, because a test can't prove them safe and some can't be undone. It also ignores any issue from someone without write access. My repo is private, but the rule is written for a public one, where anyone can open an issue with instructions inside.

Every acceptance criterion has to be checkable with a test or a command the repo already has. If it isn't, the task goes back to the backlog marked "needs testable criteria" instead of being built blind.

A concrete case: on the night of October 27, 2026 I opened six issues labeled `night`. The night agent built and released three of them by about 1:15 a.m. UTC: a test that checks every `relatedSlug` pair and every home anchor resolves, accessibility coverage for several new pages, and the cookie consent module. It left the other three alone. Removing the Sentry leftovers and an obsolete config option both touch build or deploy config, so they're my call. Upgrading Vitest to version 5 is a major version bump, and no e2e test covers the test runner itself, so it skipped that one too. The next morning they went into the daytime batch I described above. The pipeline did the Sentry cleanup and the Vitest upgrade, and the config option is the very fix the research agent implemented without being asked. I reviewed it (the built HTML came out byte-identical) and merged it as is. One night isn't a base rate, though: I haven't counted how often the night queue ships something it shouldn't or skips something it could have done.

## I pulled BotID from the form

This is a separate incident, even though its second round landed on October 6, the same day as the 500 fix. I tried Vercel BotID on the form twice, and both times it broke it. The first time it blocked real visitors. The second time, on October 6, it was live for about 17 minutes and blocked me, with "Message blocked" in a regular browser. It's gone. The honeypot, the time trap, the link filter and the firewall's per-IP limit remain.

The rule I took from it: a protection I can't test from a regular browser isn't protection, it's a bet.

## What this doesn't fix

Four problems, and the first one bothers me most.

For daytime work I run the smoke test by hand after the production deploy, so I still find a routing bug only after the site is live. It isn't automated, and it doesn't run against previews yet. And I only run it when the change touches `src/pages/api/`, `astro.config.mjs` or `vercel.json`: a change outside those three paths could bring a 500 back and nobody would see it, because I wouldn't run the check at all. And the smoke test only runs when I run it: nothing runs it on a schedule or alerts on production errors, so a failure that doesn't come from a deploy (a platform change, an expired key) would go unseen the same way.

With no CI, my machine is the only gate, and the hook can be skipped with `--no-verify`. If I'm in a rush one day and skip it, or an agent does, nothing sits behind it to stop that push.

I haven't measured how many bugs the verifiers catch on real tasks. I use the workflow anyway, but the repo keeps no per-batch record of what each verifier found.

And the checks I write today are still the ones I think of. The ones I don't think of, I find when something breaks, like the form did.

## How I made this

The agent used the `write-article` skill. What I asked it for, in essence: Spanish first, then English, each written in its own language rather than translated; nothing internal from my day job; only figures measured in this repo, with dates; and a claims list with proof for each one.

The agent read the git history, the repo docs, the smoke script, the pre-push hook, the night backlog and the `night` issues, and ran the unit suite to measure it. Anything it couldn't confirm in those sources, it left out. Then a panel of reviewers with no context read it. One of them caught that the draft had the October 27 night wrong, and it was corrected against that night's report.

The pipeline roles are short prompts: inline in the workflow script for the day pipeline, `roles/` files in the night skill. What changes between them is the model and the sentence saying what's expected: the researcher returns sourced facts, the verifier's job is to find failures.

## Sources

Everything in this post comes from this site's private repo: git history, docs, the smoke script, the pre-push hook and the issues. You can't open those, but the incidents in this post (the contact form 500, BotID and the research agent that wrote code) are logged in the [lab](/en/lab/), each with what happened, how it was caught and what changed. The test and issue figures were measured on October 28, 2026.

## Your first move

Don't copy my pipeline. Do one thing this week: write a twenty-odd-line script that hits the two or three production routes your local setup can't reproduce (redirects, dynamic endpoints, firewall rules), and fails if any of them doesn't respond as expected. Here's the skeleton:

```ts
const BASE = process.env.SMOKE_URL ?? "https://your-domain.com";

const post = (path: string, fields: Record<string, string>) => {
  const body = new FormData();
  for (const [k, v] of Object.entries(fields)) body.append(k, v);
  // Browsers send Origin on a form POST; without it a CSRF check answers 403.
  return fetch(BASE + path, { method: "POST", body, headers: { origin: BASE }, redirect: "manual" });
};

const checks: [string, () => Promise<Response>, number][] = [
  ["home", () => fetch(`${BASE}/`, { redirect: "manual" }), 200],
  // A POST with a body is where my 500 lived. A valid `ts` and a bad email
  // get past the time trap and fail validation, so no email goes out.
  ["form", () => post("/api/contact/", { ts: String(Date.now() - 60_000), name: "x", email: "bad", message: "short" }), 400],
];

let failed = 0;
for (const [name, run, want] of checks) {
  const res = await run();
  const ok = res.status === want;
  if (!ok) failed++;
  console.log(`${ok ? "ok  " : "FAIL"} ${name}: ${res.status}`);
}
process.exit(failed ? 1 : 0);

export {}; // makes it a module, so top-level await works
```

Swap in your own routes and status codes: the bad POST should get your endpoint's validation error, not a 500. I ran this exact script against this site on October 28, 2026 and both checks printed `ok`. If you have bot protection, it will challenge the script. Add your IP to the firewall bypass list, or all you'll see is challenges and 429s. Run it after every deploy, and on a schedule too, so a break that doesn't come from a deploy still shows up. If you have agents working in the repo, add a second thing: a `night` issue label and a rule that every issue carrying it says how it's verified.

**Related reading:** [how I split subagents by stories and worktrees](/en/blog/claude-code-subagents-stories-worktrees/), and why [a verifier can lie too](/en/blog/ai-agent-verifier-false-claims/).

---

**P.S.** Ever had a bug your local checks couldn't see, that you only found out about from a user? Tell me on [LinkedIn](https://www.linkedin.com/in/oscargallegoruiz).
