Show HN: Context Machines – walks your web app like a new user to find bugs Developer Hugh Hellings released Context Machines v0.1.4, a command-line tool that crawls a running web app as a new user to surface bugs and hands a prioritized fix list to a coding agent. Run as `npx contextmachines learn localhost:3000`, the tool explores for up to 3 minutes plus up to 2 minutes at a sign-in, holds back any action it cannot confirm is read-only, and on the example invoicing app found 1 proven issue across 7 screens in 50 seconds using 11.3k tokens on the user's Claude Code plan. The tool is pre-1.0, so its output and `map.json` format may change between minor versions such as 0.1 to 0.2, and it downloads a roughly 270 MB browser on first run. Finds what to fix in your web app. In a few minutes it walks your product the way a new user would, and gives you a short list to hand to your coding agent: problems it proved, and predictions marked as such, each with a file and line where it can find one. The new-user walk works on your own machine where the browser decides who's signed in: supabase-js in a client-rendered app, or a client-rendered app that asks its own /api . For anything else, use --login with a test account. From your app's folder, while it runs: npx contextmachines learn localhost:3000 Run from the folder, so each fix gets its file and line. The first time, it offers to download its browser once, about 270 MB . It uses your Claude Code or Codex sign-in if you have one Bring your own AI bring-your-own-ai ; --ai none sends nothing to an AI. npx contextmachines init sets the folder up once, and then plain npx contextmachines learn is enough. It works your project out: which app, how it starts, where it's running, how people sign in, and which AI reads it. If the app isn't running, it offers to start it, and stops it again afterwards. It asks whether your AI may read your code, which is how screens behind a sign-in get mapped. It lists exactly which files it will change and asks first. On a local app a stand-in user usually gets past the sign-in for you; anywhere else, you sign in once yourself in a browser window with --login . Behind a sign-in behind-a-sign-in says how. In CI, run npx playwright@1.61.0 install chromium first. Note Context Machines is 0.x. What it prints, its options and the shape of map.json may still change between minor versions 0.1 to 0.2 , never in a patch 0.1.0 to 0.1.1 ; CHANGELOG.md https://github.com/hughhellings/contextmachines/blob/main/CHANGELOG.md says what changed. The GitHub Action's @v0.1 tag moves only to 0.1.x patches. A browser follows links, opens menus and pop-ups, fills in forms with test values and presses their buttons. Saves are answered in the browser, and anything it can't be sure is only a read is held back what that covers, and what it can't prove how-it-stays-read-only . Then it works out what it saw. On the example invoicing app https://github.com/hughhellings/contextmachines/blob/main/examples/invoice : ℹ learning localhost:3000 · AI: your Claude Code sign-in · saves stopped up to 3 min exploring, up to 2 min more at a sign-in · v0.1.4 ✓ browser ready 1.1s ✓ public 7 screens · sign-in at / · 1 found 37s × /dashboard 8.7s 1 found ⚑ "Import invoices" does nothing when pressed ✓ / 6.1s ✓ /clients/new 9.0s ✓ /invoices/new 3.1s ✓ /reports 3.3s ✓ /settings/payments 4.2s ✓ /invoices/:id 2.9s ✓ new user no sign-up seen 2s ✓ ai review 7 reviewed · none predicted 12s ai 11.3k tokens · 0% cached ──────────────────────────────── Fix first 1 ───────────────────────────────── 1. proven "Import invoices" does nothing when pressed where index.html:100 seen /dashboard evidence contextmachines/screens/dashboard.jpg Findings 1 proven Screens 7 seen AI 11.3k tokens · 0% cached · 2 asks claude-haiku-5-5 + claude-opus-5-5 · on your Claude Code plan Start at 22:34:28 Duration 50s · first finding at 34s → Hand contextmachines/FIX.md to your coding agent The page: contextmachines/report.html That's the short version: every item's evidence and instruction are in FIX.md , and journeys, setup steps, every action and what happened to each save are in what it writes to ./contextmachines . --verbose prints them too. The last line is a menu, at a terminal only, and it stays until you press Enter or q , Esc , so a stray key never reaches your shell: | Key | What it does | |---|---| | f | Starts the coding agent the run used --ai claude-code starts claude , --ai codex starts codex right there, on your repository, with one short ask: fix the problems in FIX.md , proven first, checking each one's evidence. The run then ends with the agent's exit code. Not offered with an API key, when that agent isn't on your PATH , or when there's no app code in the folder you ran it from | | o | Opens report.html in your browser | | a | Lists the rest of the items there, with more than five, numbered as in FIX.md or add --all | | c | Copies FIX.md , to paste into any other coding agent where there's a clipboard tool: pbcopy , clip , wl-copy , xclip , its photos named from the folder you ran it in | When you're done, the menu's place says where the list and the page are. In a pipe, CI, under --json or run by a coding agent, there's no menu: the hand-off line, then one plain line naming report.html . NO COLOR keeps the menu. In a terminal that opens links iTerm2, WezTerm, VS Code, Cursor, Ghostty, kitty, Windows Terminal, and Terminal on macOS 15 , each item's where opens its file at its line, in VS Code and Cursor's own terminals and its evidence is its photo's short name screens/fix-….jpg , linked. Run it from your app's folder: from anywhere else it says there's no app code to point a fix at on a local app, unless --no-code , and it never writes into a contextmachines/ folder it didn't make one that holds other files and no .contextmachines.json , so give it --out there. Its colours are your terminal's own 16, as a test runner's are, so they follow your theme: each part's badge is black words on one of them the AI review's is your theme's own text and background swapped , green ticks, running times in bold yellow, and finished times and details dimmed. The AI row says what your AI took, as it said it: the tokens and how many were read from its cache, how many asks, and the models that answered. On a Claude Code or Codex sign-in that's on your plan, and no dollars are shown; with an API key, the cost only where the provider reports one. Each part that asked the AI says what it took at the end of its line, and map.json keeps the totals learner.ai . | File | What's in it | |---|---| | FIX.md | What to fix, proven first: each item with where to change it, its evidence and a suggested change. Hand it to your coding agent | | PRODUCT.md | The product explained, for people and for coding agents: what it's for, the main action, setup steps, journeys, every screen and every action with the request it sends, and the issues it found | | report.html | The page to read, plain like a test runner's report: one line with the site, when, how long, how many problems proven and predicted, the plain rules' predictions counted as on the terminal , the AI and any big gap public pages only, a time limit , and links to the files beside it with how many screens and journeys; then the problems in FIX.md's order, filtered by proven, predicted or new, each with where to change it each place with its own copy button, and the line as it stands , what it was seen on, its evidence and its screenshot the problem boxed in red where it was measured, opened full size with a click , and "Copy for your coding agent"; then what this run didn't check; then the screens, the journeys grouped by how they came out, issues the list doesn't already say, and what it understood, folded away. Light and dark, readable on a phone and with scripts off, and nothing on it loads from the web | | map.json | The same, as data --json prints it instead | | screens/ | A photo of each screen | examples/invoice https://github.com/hughhellings/contextmachines/blob/main/examples/invoice has that run's PRODUCT.md , map.json and photos, and the app itself, to try it on. - From asking Claude Code or Codex to find bugs: they read your code. This uses your app, in a real browser, and only calls something broken when it saw it happen. Then it hands them what it proved, with the evidence and the line. - From Playwright or Cypress tests: there's nothing to write or keep up to date. It explores like someone who has never seen the app, so it finds what nobody thought to test. - From hosted AI testing services: those run the steps you describe, on their servers. This explores with no script, on your machine, with the AI plan you already pay for. It's free and open source, with no account. It isn't a replacement for your tests. It's the new user you don't have, before your real ones arrive. On a local address, nobody has to sign in. As soon as a look through the public pages shows a sign-in in the way, learn carries on by itself, while it goes on pressing through the public pages in a browser of their own: 1. It reads your code , if you said yes at init or pass --code . Your AI lists the screens and actions behind the sign-in. Each claim has to cite a real line of code to be kept. 2. It walks those screens as a stand-in user with made-up data. The browser is told it has a user, and every data call is answered in the browser, with rows your AI made up or an empty result. ✓ 12 screens and 40 actions read from code, none seen Trying a stand-in user with made-up data Screens 15 seen · 11 with made-up data · 12 read from code Sign-in not needed for this run: a stand-in user Be clear about what that is: - Stand-in screens are your real screens with made-up data. They show how the product is built, and nothing about any real account. That pass only looks: it presses nothing, and ends once every screen it can reach has been looked at. They're marked wherever they're shown. Numbers, names and rows on them are never judged. Their layout, fixed wording and flow are real, so what's measured there can be on the fix list, marked as seen with a made-up user, and those items are compared by diff . Nothing about real accounts is proven. - "Read from the code, not seen" is what it says. A cited line exists, but that doesn't show what's said about it is right. It stays in a section of its own fromCode in map.json , and diff ignores it. - It reaches nothing real. That browser can't reach anything, your dev server included. Node reads only your dev server's own files and hands them over. No request with that user reaches any server how https://github.com/hughhellings/contextmachines/blob/main/PRIVACY.md the-stand-in-user . - On your machine, the browser stays on your dev server, from the first request. The public pages are walked too, but the browser reaches no other host, and an address your project's config hands on to another server a dev proxy counts as one: not a hosted database or sign-in, not fonts or trackers. Your dev server itself runs as usual and talks to whatever database it's set up for, so point it at a local or test one details https://github.com/hughhellings/contextmachines/blob/main/PRIVACY.md the-stand-in-user . The result lists the hosts it stayed away from, and a public page that needs one may look different. --login and --create are the exceptions, as they ask for the real thing. - A screen counts only when it really drew. A sign-in of any kind, a paywall, an error screen, a spinner or a blank page isn't a screen, and it says which it was. Nor is anything drawn at two addresses, or what your app also draws at an address that can't exist: a screen needs made-up data of its own on it, or something no other address draws. - It works where the browser decides who's signed in: supabase-js in a client-rendered app, keeping its session in browser storage, or a client-rendered app that asks its own /api who's signed in. Where your server decides server-rendered pages, other sign-in libraries for now the stand-in says so and stops, the code read still works, and the result names --login with a test account as the next step. 1. It walks your journeys. Beside all of that, sealed browsers of their own go through what a new account is offered, as far as each journey goes: onboarding past the first step, adding a record, an import, and the rest. Finished: "Add an invoice" from /invoices, 4 steps, and what was saved showed afterwards. 1 stops at a file choice: "First run" on /welcome. A person would choose a real file there, and none was given. Where the walk took one of several choices to get there, it's that choice that ends, not the journey. 1 stops at an outside account: "Connect a bank" on /payments. It reads like it connects an outside account, so the walk stopped before it. Journeys 9 found · 3 finished What the walk says, and what it doesn't: - "Finished" means the screens moved on with saves answered in the browser. It says nothing about your real server. The browser pretends each save worked and keeps it, so a list shows what was added where it can. A save only "showed afterwards" when a fresh load drew it from a read of what was kept, not from the app's own memory. Otherwise it's unknown. - An ending it can't follow is the walk's, not your app's. A pretend answer can make an app stop in a way a real server wouldn't, so it says the walk couldn't follow it, past which step, and no more. - What's real is "stopped here". Payments, emails, invites, outside accounts banks, OAuth, webhooks , a real file and any other site are never pressed, or the walk ends the moment a press heads for one, by the seal's record of what the page tried, or when a card box shows up or the screen says it's taken money or sent mail. A DELETE ends it the same way, whatever the button said. Nothing reaches any of them. Word lists can be wrong, so what it went by is worded "reads like". Where the walk took one of several choices and that led to a stop, it says so, tries the other choices if it has time, and the stop is that choice's, not the journey's. - Destructive and sign-out controls are left alone, and counted. Forms get plainly made-up values, a file chooser is never given a file, and every question a page asks gets a no. - It only comes from what was walked. Every journey this run found is listed with its screen, walked or not, with what couldn't be reached and why and what a time limit left undone. What's found isn't every way through an app. Steps to first value are counted from that walk up to the first save, and it says where the count starts and what's left out. - Nothing here is proven. No check or finding comes from the walk. It can add two predicted items, each resting on a line really drawn: an upgrade asked of a new account on a screen with nothing on it, and a wait for value. - The list never waits for it. It starts as soon as the new account has drawn where it begins, in one browser, takes the new account's other screens as they're drawn, and once that walk is over goes on in up to three browsers side by side fewer on a small machine , each under a seal of its own. Each journey starts from nothing, whichever browser it's in: what an earlier one saved, its session and what it left in the browser's storage are cleared first. Each journey is listed in the order it began, whichever ended first. It stops when the list is written, at 2m 30s at most, and on a run with nothing else left it's given 30 seconds. It runs where the stand-in does: --login , --session and --create press nothing and say so, and any other address says why there's no walk. --no-journeys leaves it out. On any other address there's no stand-in user. It says what it can offer instead: run your app locally, --code , or --login with a test account. --no-code and --no-stand-in turn each part off. The result opens with what to fix, at most five in the terminal, and all of it in FIX.md . Proven and predicted are never in one list. Proven means the browser saw it or measured it, with no AI: the issues below, and what's measured wrong on a screen a label cut off, something sitting over a field or button, a plural bug like "1 trades", a page running off sideways at phone width, a control out of reach, a broken image, an error logged as the page loads . Predicted means an AI's reading, and never more. With an AI, it reviews what was drawn on each screen, the rules in your code and what a brand-new account found, and asks one question: where does a person get stuck, wait, pay before they see value, or lose work, and what should change? It sends text, not photos, since only text can be checked. Everything it says is checked by the CLI: - Each item must quote words really on that screen, or a line really in that file. No evidence, no item. - Anything built on a made-up number or row is dropped. - So is taste, and any claim about what real users do. Nobody real was watched. It would rather miss something than tell you something wrong. map.json records how many items were raised and why each was thrown away, under learner.review . A rule can add a predicted item with no AI: an empty state a new account met with nothing to do next, or a first-run screen your code names that a new account never reached. Where to change it comes from searching your repository for the drawn words, on your machine, with no AI: a file and line when it's sure, the files to look in when several hold the same words, and nothing when it can't tell. A file your app really runs comes before a mock or a generated copy of it, a label in a shared menu goes to the menu, and an item about something covered names what's on top. It needs to run in your project's git folder. The answer to a same-looking product is kept in .review.json , so an unchanged app isn't asked about again. When it finds nothing it says so, with what it looked at and what it couldn't see. That's never a clean bill of health. Run into the same folder again and FIX.md marks what's new since the last run, and lists what the last run had and this one doesn't: gone from the list isn't the same as fixed. While it explores, it notes what it saw go wrong: | Kind | What happened | |---|---| | Does nothing | A button was pressed and nothing happened: no change on the page, no request, no navigation, no dialog | | Crashes | The page's own script threw an error after a press or a page load | | Request fails | A request to the site or its API got a 5xx other than a 503, a busy server's passing state or no answer at all; a read got a 404 from a framework's error page, or the page then said it failed a bare 404 is often how an app says "none yet" ; or an image of the site's own is on show, broken | | Broken link | Pressing a link to another page on the same site leads to a 404, 410 or 5xx other than a 503 , or to a page that only says it isn't there. A 404 for an address a router in the browser draws isn't one | Only what the browser saw happen, each with the screen and the button, link or request involved. Never opinions about design or wording. A false alarm costs more trust than a missed issue, so each kind is tested against traps that look like it but aren't a button that opens a menu, a request the page expects to 404 and stays quiet on them. After the issues, it lists a few places where a new person is likely to get stuck. These are predictions, not findings : nothing was seen to go wrong, and both the terminal and PRODUCT.md label them that way. Each comes from a plain rule over what the run mapped, and says what it rests on: - the first screen asks people to sign in, and no way to sign up was seen anywhere - an action needs a setup step first, but the journey to it never passes that step - a button whose words "Save", "Submit" don't say which of several saves on the screen it is - something destructive sits beside the main action - a long journey with no step count A map too thin to judge gives none. No AI decides these, and no opinions about taste are made. They are in map.json under predictions , and diff lists the ones that are new or gone in a section of their own, never as issues. Learn before and after a change, and compare: npx contextmachines diff before/map.json after/map.json On the example app, before and after a change that renames "Send invoice", adds "Duplicate invoice" and moves "Export CSV" to its own screen node examples/invoice/server.mjs --changed : What this change does to the product - ➕ New screen /settings/exports - ➕ New action Duplicate invoice POST /api/invoices/:id/duplicate - ✏️ Send invoice is now Email invoice POST /api/invoices/:id/send - ↪️ Export CSV moved: /reports → /settings/exports It also says which items on the fix list are fixed and which are new, measured ones on stand-in screens included and marked. Predictions sit apart: one that's gone is "no longer raised", never fixed, and an item on a screen the other run didn't see isn't judged. It's markdown, ready for a pull request comment --json for builds, --fail-on-new-issues to fail one . The GitHub Action learns your app before and after the change and keeps one comment on the pull request up to date. Add .github/workflows/contextmachines.yml : name: Product diff on: pull request permissions: contents: read pull-requests: write jobs: contextmachines: runs-on: ubuntu-latest steps: - uses: hughhellings/contextmachines/action@v0.1 with: url: https://pr-${{ github.event.number }}.preview.yourapp.com the app after this change base-url: https://yourapp.com the app before it anthropic-api-key: ${{ secrets.ANTHROPIC API KEY }} optional It learns the apps at the addresses you give it; it never builds or runs the pull request's code. The same text goes to the job summary, the screen photos are uploaded as an artifact, and changed and issues are outputs, for labelling or failing the job fail-on-new-issues: true does that for you . An unchanged product reuses its last AI reading: no cost, same words. On a pull request from a fork the token can't comment, so the result goes to the job summary only. action/action.yml https://github.com/hughhellings/contextmachines/blob/main/action/action.yml lists every input. For a preview deployed by another workflow, run this after it, with that workflow's preview address as url . A user name and password or a bypass token in url ?x-vercel-protection-bypass=… is used to get in and left out of the comment and the summary; keep it in a repository secret so GitHub masks it in the log too. @v0.1 moves to each 0.1.x patch release and never to 0.2, which may change the comment or the inputs; moving up is your choice. Pin @v0.1.0 or a commit SHA to never move. action/README.md https://github.com/hughhellings/contextmachines/blob/main/action/README.md has the rest. Coding agents read your code, not your product. Add one line to CLAUDE.md or AGENTS.md : @contextmachines/PRODUCT.md Then "add a filter to the invoices screen" means something: the agent knows which screen, what's on it, and what it saves. To have the agent run Context Machines itself learn the app, check a pull request's product diff, find broken buttons , add the skill: npx skills add hughhellings/contextmachines It teaches the agent when to use learn and diff , how to read PRODUCT.md and map.json , and never to use --create on production. It also tells it to fix what FIX.md marks as new since the last run first, then run learn again to confirm each fix is gone from the list. --json prints the result for an agent to read, with progress on stderr. With an AI, it says what the product is for, names every action and picks the main one. It uses the first one you already have, your subscription before any key: | You have | It uses | |---|---| | Claude Code, signed in | Your Claude subscription. No key. | | Codex, signed in | Your ChatGPT subscription. No key. | | ANTHROPIC API KEY | Anthropic, billed per token to that key | | OPENAI BASE URL | Any OpenAI-compatible server: Ollama, LM Studio, your company's proxy with --model | | OPENAI API KEY | OpenAI, billed per token | | GEMINI API KEY or GOOGLE API KEY | Google Gemini, billed per token | | AI GATEWAY API KEY | Vercel AI Gateway any model it serves , billed per token | | OPENROUTER API KEY | OpenRouter, billed per token | Choose with --ai and --model ; --ai none uses none. With a local model --ai ollama , nothing leaves your machine. With a signed-in Claude Code or Codex, only the question is sent: none of your settings, tools, skills or connected servers are loaded. The AI sees screen titles and headings, button and field labels, and each action's request and field names. For its review it also sees the text drawn on pages reached without a sign-in, on a local app too, with table cells left out, so names and amounts drawn outside a table can go, and so can a name in a button's label, one in a table row included. Never what's in a field, and never the photos. To send none of it, use --ai none . PRIVACY.md https://github.com/hughhellings/contextmachines/blob/main/PRIVACY.md has the details. An unchanged product reuses its last reading, so repeat runs cost nothing. Without an AI you still get every screen, action and issue. npx contextmachines learn --help and npx contextmachines diff --help list them all, with their ranges and defaults. npx contextmachines init sets a folder up, and saves your answers in contextmachines.config.json : { "url": "http://localhost:3000", "ai": "claude-code", "readCode": true } . It holds no key or password, so it's safe to commit. Add "model" to choose the AI's model, as --model does: for Codex, a model your plan has. learn uses it for whatever you don't type: an address or --ai on the command line wins. init --yes --url