Your coding agent says the feature is done. The build is green. Then a user tells you they can't log in.

This happens a lot with apps built in Claude Code or Cursor. The agent checks that the code compiles and that the tests it knows about pass. It doesn't check that a real person can still sign up, log in and pay. That part is still your job.

Here's the short answer: pick the few flows that must never break, make the agent run fast checks before it stops, and run those flows in a real browser against a preview deploy before every release. The rest of this post is the step-by-step version I use.

1. Write down your critical flows first

Before you write any test, write a list. Not every page. Only the journeys a customer would notice first if they broke.

For most SaaS apps that's something like:

  • Sign up with a new email
  • Log in and log out
  • Reset a password
  • The main "create" action (a project, a post, an order)
  • Checkout or upgrade to a paid plan
  • One settings change people actually use

Keep it under ten. Put it in a file in the repo, like CRITICAL_FLOWS.md, so both you and the agent can read it. A one-line description per flow is enough:

# Critical flows 1. Sign up: new email -> confirm -> lands on empty dashboard 2. Log in: existing user -> dashboard shows their projects 3. Create project: name it -> it appears in the list after reload 4. Upgrade: pick Pro -> test card -> plan shows "Pro" in settings 

The "after reload" part matters. A lot of agent bugs only show when the data has to come back from the server.

2. Tell the agent about the list, and about your test command

Agents follow rules files: CLAUDE.md for Claude Code, .cursor/rules or AGENTS.md for Cursor. Add two things:

## Before you finish a task - Run `npm run typecheck && npm test` and fix anything that fails. - If you touched auth, billing or the dashboard, list which items in CRITICAL_FLOWS.md your change could affect. 

The second line is cheap and useful. It makes the agent tell you the blast radius of its change, so you know what to check.

But rules are advice. The agent can still skip them.

3. Make the fast checks a gate the agent can't skip

Claude Code has hooks, which run a command at fixed points. A Stop hook runs when the agent is about to finish. If the command exits with code 2, Claude Code keeps the agent working and shows it the error.

In .claude/settings.json:

{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "npm run typecheck 1>&2 && npm test --silent 1>&2 || exit 2" } ] } ] } } 

Keep this command fast. Typecheck and unit tests, maybe your access-control tests. If it takes five minutes on every turn, you'll turn it off. Browser tests belong later in the pipeline.

Cursor has its own hooks feature too. Check its docs for the current format, since it has been changing.

4. Deploy every change to a preview URL

You can't test the app a user sees from inside the repo. You need it running.

Vercel, Netlify and Cloudflare Pages all build a unique preview URL for each branch or pull request. Turn that on. Now every change the agent makes has a real address you can open, with nothing mocked.

Use a separate test database or seeded test accounts for previews. You don't want a signup test creating real users in production.

5. Run the critical flows in a real browser

This is the step that catches the things unit tests miss: a button whose onClick was dropped, a form that submits but never saves, a redirect loop after login.

If you write code-based tests, Playwright is a good default. Here is the login flow from the list above:

import { test, expect } from '@playwright/test'; const BASE_URL = process.env.PREVIEW_URL ?? 'http://localhost:3000'; test('existing user can log in and sees their projects', async ({ page }) => { await page.goto(`${BASE_URL}/login`); await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!); await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!); await page.getByRole('button', { name: 'Log in' }).click(); await expect(page).toHaveURL(/\/dashboard/); await expect(page.getByText('My first project')).toBeVisible(); }); 

Two habits make these tests worth having:

  • Assert an outcome, not a page load. "The project is in the list" is a check. "The dashboard opened" is barely one.
  • Make sure the test can fail. Break the page on purpose once, and confirm the test goes red. A test that passes on a broken page is worse than no test.

Use role and label selectors (getByRole, getByLabel) rather than CSS classes. Agents rename classes all the time. They change visible button text much less often.

6. Check the API, not just the screen

Some bugs look fine in the browser. The API returns 200 OK, the page shows a spinner and then nothing, and the real problem is a wrong field in the JSON.

For the endpoints behind your critical flows, check more than the status code:

test('projects API returns the user\'s projects', async ({ request }) => { const res = await request.get(`${BASE_URL}/api/projects`, { headers: { Authorization: `Bearer ${process.env.TEST_USER_TOKEN}` }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(Array.isArray(body.projects)).toBe(true); expect(body.projects[0]).toHaveProperty('name'); }); 

Also try one request as the wrong user. Agent-written backends often forget to check that a record belongs to the person asking for it.

7. Wire it into CI and block the merge

Run the same checks on every pull request, whoever or whatever wrote it:

name: checks on: pull_request jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run typecheck && npm test - run: npx playwright install --with-deps chromium - run: npx playwright test env: PREVIEW_URL: ${{ vars.PREVIEW_URL }} TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} 

Getting the preview URL for each PR depends on your host. Most of them post a deployment status you can wait for, then read the URL from. Mark the job as required in your branch protection settings so a red run actually blocks the merge.

8. Feed failures back to the agent

When a browser check fails, don't just tell the agent "a test is red." Give it the flow, the step that failed, and what the page showed. Paste the Playwright error and the trace or screenshot.

A message like "After clicking Log in, the page stayed on /login and showed 'Invalid session'" gets you a much better fix than a stack trace alone.

9. Still do a quick manual pass before a release

Tests only check what someone wrote down. Before a bigger release, click through your critical flows yourself once, on a phone and in Safari. Try a payment failure in test mode. It takes ten minutes and catches the things nobody thought to automate.

The short version

  1. List your critical flows.
  2. Put the list and your test command in the agent's rules file.
  3. Gate the agent on fast checks with a hook.
  4. Deploy every change to a preview URL.
  5. Run the critical flows in a real browser and check the API behind them.
  6. Block merges on red, feed failures back to the agent, and do a manual pass before releases.

None of this needs a QA team. It needs a short list and the habit of checking it every time.

I'm building AlwaysQA, which checks login, checkout and other critical flows in a real browser after your coding agent ships a change. It only checks the flows you describe, and it doesn't replace your unit tests.