staysfixed
Provides regression checking for Electron desktop apps by driving them over their own debugging port, running the app through the same steps twice, and reporting unintended differences between builds.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@staysfixedRun the check and tell me what unexpectedly changed after my edits."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Stays Fixed
Prove that nothing which already worked has changed — and let the agent do the reviewing.
An AI agent changes twenty files in four minutes. What breaks is almost never the thing it was working on: it is something in a corner that had already been built, already been fixed, and that nobody thought to look at again. The agent cannot check that corner, because it does not know the corner exists.
So Stays Fixed runs your product through the same steps twice, compares it against the build you were last happy with, subtracts everything the product disagrees with itself about, and hands back only the differences nobody asked for. Everything unchanged is skipped and never mentioned. The agent already knows what it meant to change, so what is left is its work queue.
You are not in that loop, and you never approve anything. What "working" means is cut by something you already do: saying ship. You hear about it only when a difference lands in a class no agent may wave through — money, signing in, lost data, a crash, or a bug you already reported once — and then it arrives as three plain sentences inside the summary you were reading anyway.
The loop
The agent seals what it meant to change, before it runs anything.
It changes some code.
It calls
staysfixed_checkover MCP.Everything unchanged is skipped and never reaches its context.
What comes back is what changed. It already knows what it intended, so the targets are the differences it did not intend.
It fixes those and runs again. Anything it genuinely meant, it can record as intended — five times, never in a sealed class, and only inside what it sealed in step 1.
"You will call the MCP, make it run the tests, the results come back, then you decide. Whatever is unchanged will be skipped. The things that changed other than the ones you actually did yourself — those ones are the targets. So you will not burn your tokens reviewing every single thing. You will only review what actually matters."
Related MCP server: aiTest MCP Server
Install
npx staysfixed init
npx staysfixed checkNo account, no sign-up, no server anywhere, nothing uploaded. It works in any project in any language — it only needs to be able to run your product.
Requirements: Node 22 or newer, and git — your project has to be a git repository. Not for history's sake: git is how an old build is put back on the machine to be walked live, and how a difference is measured against the code you just changed. A folder with no git in it is refused outright rather than checked against a guess. Everything else depends on what you are watching, and the tool works out what it has:
npx staysfixed doctor
npx staysfixed doctor --json # the same answer, for an agentEverything each kind of product needs — the exact programs, the exact install commands, the settings keys and the licences only a person can accept — is in docs/settings.md.
doctor is the first thing you should run and the first thing an agent should
call. It says what it can check on this machine, what it cannot, what is missing,
and the exact command that would fix each gap — and it never suggests setting up
something that already works, because everything it lists as missing failed a
real check first.
Every kind of product comes back in one of four states, and the last two are the ones that matter:
State | What it means for you |
ready | It works here now. Nothing to say. |
the agent can fix this | Everything in the way installs with a command. The agent runs it and never mentions it to you. |
only a person can do this | A licence, a device, a password, a pair of hands. You get what to do, why it is needed, and what it unlocks — one sentence each, so being asked for half an hour of your time comes with what you get for it. |
not possible here | No command on this machine changes the answer. It says so, says the nearest honest alternative, and stops offering it. |
Two separate questions are answered separately, because folding them together is how a surface gets called ready while nothing is ever walked on it:
can this machine run it — is there a simulator, an emulator, a browser, a Java runtime, another machine it can already reach over SSH;
can this copy drive it — is the adapter for that kind of product actually in this build of the tool.
A Mac with Xcode on it can run an iPhone app. That says nothing about whether
there is anything here that knows how to open one, and doctor --json answers
both under surfaces and drivers.
And it never sends you shopping for nothing. A project with no phone app in it is never told to install Java or thirty gigabytes of Xcode — it is told there is no phone app here to check.
What a fresh install downloads, and what it does not
npm install staysfixed pulls three packages and no browser — pixelmatch,
pngjs and playwright-core, about 13MB together on a Mac. playwright-core is
the part that drives a browser; it is deliberately the version that downloads
none, so nothing here takes minutes and nothing lands in a shared cache behind
your back.
Checking a website needs an actual browser, and the tool looks for one you already have before asking for anything: Chrome for Testing, Chromium, Edge or Chrome. If there is none on the machine at all, that is one command:
npx playwright install chromiumMeasured on a Mac in August 2026 that is about 570MB in a shared cache
outside your project — 371MB for Chrome for Testing and 196MB for its headless
shell — downloaded once per machine, not once per project. doctor tells you if
you need it, and it is one of the things an agent can simply do without asking
you. A project that already drives its own tests with the full playwright
package is used as it is and never asked to install a second copy of the same
driver.
Checking a desktop app needs no browser at all and no download: the app is its own Chromium, and the tool drives it over its own debugging port.
Checks never open the browser you use. Given the choice they open Chrome for Testing or Chromium — a separate application — because on a Mac two copies of one browser share a single slot, and a check running in the background can end up answering when you click your own browser icon. If your machine has nothing but your everyday browser, it is used, invisibly and on a throwaway profile, and every run says so out loud rather than borrowing it quietly. Nothing it opens uses your profile, nothing it opens survives the run, and nothing it did not start is ever closed:
npx staysfixed browsers # which browser checks open, and why
npx staysfixed browsers --clean # clear up after a run that was interruptedWhat is real today
This is a repository in the middle of a rebuild, and the README is not going to pretend otherwise.
State | |
Picture checks, guards, walk, markers, flake register | Shipped. Published as |
The MCP server an agent wires up — seven tools, no door marked approve | Works, and it is what |
| Works, and it is what |
| Shipped. Three of its probes were wrong about the machine it was run on until 0.7.0 — see the silences. |
The difference engine — the address space, normalisation, wobble subtraction, clustering, ranking, causal proof, the store, the MCP tools, the self-check corpus | Works. |
Command-line tools, libraries and HTTP servers | Works. |
Reading the contract straight out of your source — routes, exports, message channels — without running anything | Works. 5,785 doors read out of one desktop app in 1.4 seconds. |
Websites, through a browser of the tool's own | Works. |
Electron desktop apps, over their own debugging port | Works. |
The reference cut when you ship, sealed intents, the waiver budget, and escalations in your closing summary | Works. This page describes what it actually does. |
The coverage ledger — every door counted, the unopened ones named, and the sentence saying so on every reply | Works. See what it did not check. |
Aiming a check at one kind of product, and refusing by name rather than checking something else | Works. |
Steps taken from your own test suite | Works. |
Steps taken from a recorded session, or rejected at birth for not repeating twice | Written, not wired. The code is in |
Android APKs on an emulator | The adapter is here. It reads everything the APK declares with nothing installed and no Java, and where there is an emulator it installs one build at a time and walks it. Whether this machine can run one is a separate question, and |
The iOS simulator | The adapter is here. It reads what the app bundle declares with nothing running, and where Xcode and a simulator runtime are present it installs one build at a time, boots it and reads what is on the screen. It is new. Paired running costs two |
Native Windows GUI (a real Win32 app, not an Electron one) | The probe is here, driven over ssh to any machine that reaches a Windows desktop — a WSL shell on one counts, and nothing is installed on it. Windows shows one desktop, so two builds can never run at once: the comparison is genuinely weaker here than anywhere else. |
staysfixed check is the front door for both. Version 1's flags still mean
exactly what they meant yesterday — --pictures, --guards and --only reach
the same code they always did, on the settings you already have, and staysfixed mcp --v1 still serves the picture tools. Nobody who installed this last week has
to change anything.
Two things did change, and they are worth reading if you installed this before today.
--watch moved. staysfixed check --watch opens version 2's panel — the one
built for a difference engine, which draws journeys, wobble, findings and a
verdict. Version 1's panel drew approved pictures side by side, which this tool no
longer has. --pictures --watch and --guards --watch still open version 1's
panel over version 1's run, so the only person whose command changed meaning is
the one who typed --watch on its own and got a picture check they did not ask
for. Everything about the panel is in docs/watching.md.
staysfixed init now writes settings for the difference engine. On a product with
no screen those settings have no app block in them, and the picture commands say
so plainly rather than telling you to invent a web address. Add an app block
yourself if you want picture checks too.
How it proves nothing changed
Three ideas, in order of how much weight they carry.
1. Measure the wobble. Never guess a tolerance.
Every product disagrees with itself a little between runs — a timestamp, an animation frame, an id. So the tool runs the new build twice. Anything that differs between two runs of the same build was not caused by your change: it is the product's own wobble, and it is subtracted arithmetically.
There is no tolerance setting in version 2 and there is not going to be one. Tolerance knobs are how tools like this die — too loose to catch the real thing, too tight to leave switched on.
Version 1's picture check is the proof, and it was caught doing exactly that.
Its default allowed 0.05% of a picture through, with a comment saying that was
"enough for font hinting noise, nowhere near enough to hide a missing
stylesheet". On a 2880×1800 screenshot, 0.05% is 2,592 pixels. Taking one
letter out of a page's main heading moves 593 — so the check reported
"Everything that worked still works" over a page anybody could see was wrong.
Ten fresh takes of that same build differ by zero pixels, because the freeze
layer underneath is thorough enough to make that true. So version 1's default is
now zero as well: a project whose product genuinely wobbles can set
tolerance.pixels, and every run then says in words how many pixels that
setting just swallowed.
It also catches a bug class no screenshot tool has ever caught. A path that was steady in the old build and wobbles in the new one means the change made something unpredictable. That is a finding, even though no single value can be pointed at.
2. Cheap suspicion, expensive proof.
Comparing against the stored record is fast and needs no rebuild, so that runs first. Every path that then looks different gets the old build booted live, on the same machine, in the same minute, and walked again. Only differences that survive that live re-run are reported.
--paired goes straight to the expensive half — old build live from the start.
That is for pre-release, and for the first run on a product with nothing recorded.
3. Sequential, never simultaneous.
Two builds at the same instant fight over ports, single-instance locks, user data directories, databases and relay slots. The value was never in the same second: it is in the same machine, same fonts, same operating system, same data, minutes apart. Runs are sequential with a full state reset between them, interleaved journey by journey so drift cannot accumulate.
What it looks at
Seven channels, all flattened to one shape — a path, a channel, and a value — so one comparison engine serves every platform. Pixels are last and are only ever evidence for something another channel already found.
Channel | What it holds |
| What the interface says a control is and does — its role, its name, whether it is on, off or disabled. Not the underlying markup, because markup changes when nothing did. |
| What the product sent out into the world: calls made, files written, processes started, things saved. |
| What the product complained about: console messages, errors, crashes, the code it exited with. |
| What the product gave back: what it printed, what it answered, what it offers other code. |
| The doors the source says exist: routes, exported functions, message channels. Read without running anything. Free, and exact. |
| Rough counts — files written, calls made, doors answered. Compared exactly. How long something took is recorded and never compared: see what it will never do. |
| What it looked like. Used to show a person a problem another channel already found. |
An address reads left to right, widest thing first:
api.GET./users.status
cli.build.exit
ipc.session:create.registered
screen.home.tree.button:Save.enabledThree kinds of difference come out, and the last one is the kind no screenshot comparison has ever noticed: changed, appeared, and vanished — a door that closed.
Where the steps come from, ranked, because this is the real workload question: read the code (free, exact) → run the project's own existing test suite under instrumentation → recorded real sessions → the agent exploring one named gap and freezing it into a replayable file → never a person clicking through an app.
What is wired into staysfixed check today is the first of those, a journeys file
you point it at, and — when you ask for it — the project's own test suite. Each
adapter reads your source and offers the journeys it can walk: routes, commands,
screens, message channels. --journeys <file> names steps by hand. --journeys suite runs each test file twice inside the scratch copy, reports every check by
name, and stops after 90 seconds naming every file it did not reach. Recorded
sessions and the flake register are written and tested in src/v2/journeys/, and
nothing on the check path calls them yet. Saying so is the point: a feature
that exists in the repository and not in the run is not a feature you have.
Keeping it quiet
Four layers before anything reaches the agent: normalise volatile shapes by rules kept in git, so a version bump in a footer reports zero differences instead of five hundred; cluster by signature, so one cause reads as one finding; rank by distance from the changed code, so a difference far from your edit sorts to the top — that is the definition of a side effect; then let the agent prove causation by reverting the suspect hunk and running again. That last step is a proof, not a heuristic.
Every normalisation rule buys quiet by making some real differences invisible.
The rule that quietens a wobbling clock also hides a genuinely wrong date. So the
rules are data, not code: they live in git, they get reviewed like any other
change, and every one carries a wouldHide field in plain English saying what it
covers up. explain() answers for any value it changed — what was replaced,
where, by which rule, and what that rule admits it might be hiding. The test
suite fails if a rule ships without one. See
docs/how-v2-works.md.
What it did not check
This is the most important thing the tool says, and the reason is arithmetic rather than modesty. A tool that reports "nothing changed" is indistinguishable, from the outside, from a tool that looked at nothing — and the more useful it becomes, the less anybody reads past the headline.
So every reply carries what was left out, in the same breath as the good news, on clean runs as loudly as on dirty ones. There is no version of the answer that omits it.
ok Nothing that worked has changed. 601 addresses checked against 0.13.0, run
live. … NOT EVERYTHING WAS CHECKED: 391 of the 452 ways into this product
have never been walked through, so nothing here says anything about them,
and 2 other things were not looked at. A clean result only covers what was
walked.
What this run did not check
391 of the 452 ways into this product have never been walked through.
A break behind any of them is invisible to this tool. Point a journey at
them - name the steps in a journeys file and pass it with --journeys.A door is any way into your product the source declares: an HTTP route, an
exported function, a message channel between a desktop app's two halves, a
command in your package.json. They are read straight out of the code without
running anything — 5,785 of them out of one desktop app in 1.4 seconds, 452 of
those message channels.
Three rules hold the count honest:
A door read out of the source is not a door that was walked. Knowing a door exists is not evidence that anything opened it, and getting that backwards would report perfect coverage on a product nobody ever ran.
Never a percentage. A percentage invites a target, a target invites gaming, and a gamed coverage number is worse than no number because somebody believes it. Counts, and the names of what is missing.
Undercount rather than overcount. Where the evidence is ambiguous the door is recorded as unopened, and the reason is written down beside it.
Even a run that walked every door it knows about refuses to claim it checked everything, because it did not: it checked every way in this tool knows about.
staysfixed check --json # coverage.doorsKnown, coverage.doorsWalked, coverage.gapsOver MCP it is staysfixed_coverage. Every staysfixed_check reply says it in
words directly under the headline, and the JSON form carries notChecked and
doorsNeverOpened as fields of their own rather than only as prose — a number an
agent has to go looking for is a number it skips.
And on a clean run only, the reply also says what a clean result on this machine actually means: "this covers your website; your iPhone app is not being checked at all, and here is why." Nothing inside a run can know that — a run only knows what it walked — so it comes from the machine survey and lands beside the good news, which is the one place it cannot be missed.
A run that compared nothing is not a pass
There is one shape of clean result that would be a lie: every journey walked on
the new build, nothing on record from the old one, zero differences found, and a
verdict reading nothing that worked has changed. It is arithmetically true and
it would let a real regression through. That run comes back as NOTHING WAS ACTUALLY COMPARED, it is not a pass, and it exits non-zero.
The cold start is the same thing, and it is now marked the same way. The very
first run on a project — before anybody has shipped once with the hook in place —
has no reference at all, rather than a reference with nothing in it. For a while
only the command line caught that: it said "Nothing to compare against yet… this
run proves nothing about what still works" and exited 2, while --json and
staysfixed_check over MCP both carried ok: true with none of those words. An
agent reads the fields, not the sentence, so the two interfaces that matter most
reported a pass over a run that compared nothing — the exact failure this tool
exists to prevent, produced by the tool itself. Both now answer ok: false with
NOTHING WAS ACTUALLY COMPARED. reference.id is still worth reading — an
empty string means nothing was compared, whatever else a reply says.
The silences that were found and closed
Every one of these left something invisible while the answer looked complete — most of them a clean-looking run, and the last four an honest-looking description of a machine. They are listed because a tool like this earns trust by naming the ways it has been wrong, not by claiming it never was. All of them were found on 2026-08-29 and 2026-08-30 by reading the whole thing looking for the same shape as the first one, and each has a case in the corpus, a test, or a probe rewritten to ask the question a different way.
It used to | Now |
Skip any source file over 2MB without a word, then report that it had found no source at all — so a desktop app whose main process is one 3.5MB bundle had all 452 of its message channels silently unread | Reads up to 24MB, and names any file it still cannot open |
Skip a folder it could not open, and every door behind it, silently | Names the folder and the reason, as missing coverage |
Keep the two ends of a huge output and a rough size, so a break in the discarded middle left a byte-identical record | Keeps the exact byte count, and says out loud that only the ends were compared |
Drop a whole adapter's journeys when it threw while listing them — a surface disappears and the verdict reads "nothing has changed" | Records it as a hole, by name, in the coverage |
Treat a | Streams the diff into a hash with no ceiling, and refuses outright rather than guessing. A folder with no git in it is refused for the same reason |
Read only the first 80 differences of a cluster when deciding whether it is one of the five things nobody may wave through — so a refund at address 150 of 300 was classified ordinary, and an agent could waive the lot | Reads every difference in the finding. There is no ceiling on the one gate that cannot have one |
Re-check the first five addresses of a finding when proving whose change caused it, then say "caused by that change" about all three hundred — a machine-checked reason for an agent to close a break it had only half explained | Re-checks every address, and says in words when a change explains part of a finding and not the rest. It costs nothing: the walk already happened |
Subtract the new build's own wobble even when the wobble had swallowed the comparison — a second run that fell over makes almost every address unsteady, everything is dropped before it is compared, and the run ends "nothing that already worked has changed" | A build that disagrees with itself about most of its own addresses gets no verdict: the run says NO ANSWER FROM THIS RUN, in those words, and is not a pass |
Keep the first of two facts written at one address and ignore the second, so a door that broke behind a duplicated address could never be compared with anything. The detector for this was written on day one and never called | Every walk is checked for it, and each clash is named in the coverage: which address, and what the ignored answer was |
Skip a folder it could not open while looking for routes, and every route behind it — the same bug as the one above, in a second place, still silent | Names the folder, and the routes behind it are reported as unread rather than as absent |
Serve version 1's picture tools when an agent wired up |
|
Ask a machine | Asks the filesystem for the three places PowerShell actually lives, using the one list the code that later drives it uses. Reads standard output only, and matches the whole line, so a host that quotes your command back cannot answer for itself |
Name only the first eight machines in an ssh config and drop the rest without a word | Dials sixteen, and anything past that is named as not dialled rather than left out |
Report Docker as present because the command is on the path, on a Mac where Docker Desktop is shut and nothing it promises would work | Asks the engine for its version, and says "installed but not answering" when that is the truth |
Read the commented-out examples in a settings file as settings. | Comments are taken away before anything is read out of the file, with strings respected so an address keeps its two slashes. The file is still never loaded — doctor must not run your code to answer a question about your machine |
Ask for a build step on a plain command-line tool. A script that runs from source is recorded as "not built", because there is nothing to build, and that was read as "it has not been built yet" — so a fresh install told its owner to name the command that builds a file sitting right there, in the same breath as offering to run it | Nothing is asked for when there is nothing to build, or when a command to run it has already been worked out |
The silences that are still open
The table above is the honest half of a habit, and it would be a dishonest half on its own: it lists what was found and closed, which reads as though nothing is left. These are the ones that are still open today, found the same way, by reading the code rather than the documentation. Every one of them can end in a clean-looking answer, and the file that would close each one is named so nobody has to take this on trust.
Seven rows left this table in one night. They are in the closed table above now: a wobble storm that needed twelve addresses before it counted, so ten of eleven wobbling passed; a desktop control compared to its first 200 characters; an unreadable folder losing every page of a website behind it; two screens with one name collapsing into one walk; a waiver pinned to the first 40 differences of a finding rather than all of them; and three iOS ceilings that stopped counting without saying so. What is below is what is genuinely left.
What is invisible | Where |
What the scratch copy leaves out is left out of both builds. A product that actually needs one of the folders named under |
|
Working out what a repository makes reads at most 300 files, four folders deep, and skips any file over 2MB. A product it never notices is never configured and never walked, so it is missing from the coverage ledger rather than named in it — the one kind of gap the ledger cannot show you. |
|
Values nested more than 64 levels deep are compared as "too deep" rather than by their contents. It will never go quiet about one, but it cannot say what inside it moved. This only ever applies to a value read back off the disk; observations made in this run refuse that depth at the door. |
|
A change buried in the middle of an output larger than 64KB can be missed. Both ends are kept and compared along with the exact number of bytes dropped, so a middle that grew or shrank shows up. One that changed without changing length does not. |
|
How long anything took is recorded and never compared. A build that got twice as slow is not a finding. This one is deliberate and permanent — a stopwatch on a shared machine measures the machine at least as much as the product — and a build that hangs is still caught, because it is stopped for taking too long and how it finished is compared exactly. | by design |
On Windows and as root, one self-check case cannot run. It takes away the permission to write, to prove the tool still answers when the disk gives out; neither honours that. The case reports itself as untested rather than as a pass, and the closing line counts it separately. |
|
The distance from a finding to your edit has three ceilings: 4,000 source files, 8 imports out, and 400KB per file. Past any of them the ranking for that finding is a guess. It is not silent — a run that hit a ceiling says so and names the files it could not read — but the order is worth less than the rest of the list. |
|
Aiming a check at one thing
By default a check walks everything your settings describe. To aim it at one kind of product:
staysfixed check --surface web --at http://localhost:3000
staysfixed check --surface electron --at ./release/mac-arm64/YourApp.app
staysfixed check --surface android --at ./app/build/outputs/apk/release/app.apk
staysfixed check --surface ios --at ./build/YourApp.appThe important half is what happens when it cannot go there. A tool that quietly ignored an option it did not understand would check whatever it was going to check anyway and hand back a perfectly clean result about the wrong thing — the most dangerous shape a reply can have. So:
aimed at a kind of product this project does not contain → it refuses, by name, and nothing is checked;
aimed at a kind of product this copy of the tool has no adapter for → it refuses, and names the adapter that is missing;
given an address no adapter would ever read → it refuses rather than dropping it;
and a run that did go where it was aimed says so, so a clean result can be trusted to be about the thing you named.
Where the approval line sits
The word "approve" was hiding four different decisions.
What counts as working — you, and only you. But never by opening this tool. The reference is cut by something you already do: saying ship. You approve in bulk, retrospectively, by shipping.
Is this difference real or is it noise — the machine, arithmetically, from running the new build twice. No judgement, nobody's opinion.
Did my own edit cause this — the agent. That is a causal claim, which is checkable: revert the suspect hunk, run again, and if the difference survives the revert the agent was wrong and it escalates.
Is an unintended difference acceptable anyway — you. This is the only thing that reaches a person, and it should be a handful of items a month.
An agent can check, and it can waive within limits. It can never decide what "working" means. That is cut by shipping, by a person, and there is no tool on the MCP surface that could move it — not refused, not on the list.
An agent's only door is a waiver, and it passes four machine-checked gates:
Sealed classes are unwaivable. Whatever the reason, whoever is asking.
The waiver has to agree with an intent sealed before the run — so the agent says what it meant to change before it sees what broke. Sealing one afterwards is refused, and the refusal says so in those words.
Five waivers between one ship and the next. Past five it is not a change with side effects, it is a rewrite, and a person looks at a rewrite. Sealing another intent does not buy five more.
Every waiver is fingerprinted to the difference it was written about and dies the moment the reference moves. Change that value and it stops covering anything. The pin is the finding's title, up to twenty of its addresses and up to forty of its differences — see the silences that are still open for what that means on a very large finding.
Every waiver is counted out loud in the reply. "Nothing changed", "nothing ran" and "everything was waived" read identically otherwise, and two of those three are a safety net quietly announcing success.
The five sealed classes, in full
These go to a person whatever any agent believes, and there is no setting that turns them off:
a bug somebody already reported once | A guard exists because this exact thing broke before and somebody had to say so. A difference here means it is back. |
a crash | The product stopped, or started stopping. Nothing about that can be intended. |
losing data | Code can be edited back. Data that was deleted cannot. |
money | A charge, a price or a refund that goes out wrong costs a real person real money. |
signing in | Getting this wrong locks the right people out, or lets the wrong people in. |
Saying ship
staysfixed ship --why "0.14.0 to TestFlight"One line at the end of your release script, after the thing has actually gone out. The build you shipped becomes the standard every later check is compared against; every outstanding waiver is retired, because what it covered has either shipped and become normal or has to be decided again.
It never fails your release. And it refuses to make a build the standard if that build was never checked, or was checked and found broken — your release still succeeds, it just tells you what "working" means did not move, and why. Cutting a broken build as the standard is exactly how a safety net turns into a rubber stamp.
What actually reaches you
A handful of things a month, in three sentences each — what changed, why no agent could wave it through, and what to do — inside the closing summary you were reading anyway. Not a report, not a dashboard, not a link:
Stays Fixed: 1 thing needs your word on terminal-deck.
1. The checkout total now says 9.99 where it said 10.00
No agent may wave this through: it touches money.
Say whether that is the amount you wanted. If it is, shipping makes it the
new normal; if it is not, nothing ships.Five things can land there and four of them are rare on purpose: a sealed class; the waiver budget running out with differences still outstanding; something that used to give the same answer every run and now does not; a check that could not run at all; and, once, the fact that you have never shipped with the hook in place so there is nothing to compare against yet. A clean run produces none of them and says one line: nothing needs your word.
staysfixed check --escalations notes.txt # the same block, in a fileFor AI agents (MCP)
Claude Code:
claude mcp add staysfixed -- npx -y staysfixed mcpCursor, Gemini CLI and most other clients take the same block — .cursor/mcp.json,
~/.gemini/settings.json, or your project's .mcp.json:
{
"mcpServers": {
"staysfixed": {
"command": "npx",
"args": ["-y", "staysfixed", "mcp"],
"cwd": "/absolute/path/to/your/project"
}
}
}Codex keeps the same fields in TOML, in ~/.codex/config.toml:
[mcp_servers.staysfixed]
command = "npx"
args = ["-y", "staysfixed", "mcp"]
cwd = "/absolute/path/to/your/project"Tool | What it does |
| Call this first, once per session. What it can check on this machine right now, what it cannot and why, what is missing that would unlock more, and the exact shape of every reply. It runs nothing. After this call an agent should not need to read any documentation about this tool. |
| Seal what you meant to change, before you run a check. This is what makes a later "that one was me" claim checkable instead of a story. |
| Run it. Returns only the differences you did not account for, ranked with the ones furthest from your edit at the top. Unchanged paths never reach you; the reply says how many were skipped, and what was never looked at. Takes |
| One finding, in depth — both values in full, the journey that reached it, the code around it, the evidence. Never pushed into a check reply, so ask for it on the two or three you intend to act on. |
| Test a causal claim by undoing a change and running again. If the difference survives the revert, your edit did not cause it and you were about to fix the wrong thing. |
| Record that a difference was intended. Not approval, and it makes nothing the new normal — only shipping does that. Four gates, and a refusal is final. |
| What was not checked: the doors no journey has ever opened, the surfaces this machine cannot reach, the surfaces this copy has no adapter for, anything refused for being irreversible, and what it can never see on any machine. Read it before telling anyone a change is safe. |
An agent can check; only a person can approve. staysfixed_approve is not
merely refused — it is not on the tool list at all unless the project explicitly
opts in, so the agent never sees a door to push on. An agent that could bless its
own results would edit the code, notice something moved, approve it, and report
success, and your safety net would have become a rubber stamp.
Every tool in that list also carries a short title and the protocol's own flags
for what it does to your machine, so a client can tell a question from an action
without reading this page: staysfixed_capabilities, staysfixed_explain and
staysfixed_coverage are marked read-only, and none of the seven is marked as
reaching the outside world, because none of them does.
staysfixed mcp serves the tools above. The version 1 picture tools are still
there behind staysfixed mcp --v1 for anybody who wired those up, unchanged.
Full wiring for every client: docs/mcp.md.
Nothing here should need a human to read documentation
Every version ships knowing, in machine-readable form and in plain English: what
it can check on this machine right now and what it cannot; what is missing that
would unlock more, and whether the tool can install it itself or a person has to;
which other machines it can already reach, found by dialling them rather than
by asking you; and the shape of its own results, so an agent can act on them
without being taught. That is staysfixed doctor --json, and it is
staysfixed_capabilities over MCP.
The nets that are already shipped
Guards — one check per bug that was already fixed once
// .staysfixed/guards/the-sidebar-still-collapses.js
export default {
name: 'the sidebar still collapses',
because: 'A CSS rename broke the toggle handler and it shipped unnoticed for four days.',
async run(app) {
await app.open('/');
await app.click('[data-action="toggle-sidebar"]');
await app.expect('the sidebar is hidden', async () => !(await app.page.visible('.sidebar')));
},
};The name is not decoration. It is what prints when the guard fails and what an
agent reads before deciding whether it broke something. So names are enforced:
sidebar_collapse_test and #4412 are refused, with an explanation and, where
one can honestly be built, a rewrite. See docs/guards.md.
Guards are the third net, for the case both engines are blind to: the old build was already wrong. A difference against a guard is sealed — it goes to a person.
Walk — the last look before a release
staysfixed walk --openOpens the real built app, visits each screen, photographs every step onto one page you can scroll in thirty seconds. Nothing is compared and nothing can fail on a pixel. This net answers the question a comparison cannot: does the thing I am about to ship actually open?
Markers — pin a known-good moment
staysfixed mark v0.15.0 --note "before the store work"
staysfixed trace billing-emptyA marker defines what "old" means. Comparison runs over stored build artifacts, so tracing a regression to a commit does not need every commit rebuilt.
Picture checks — version 1, unchanged
staysfixed check --pictures
staysfixed approve --allPixels dropped from the accusation to the evidence, but the version 1 picture check is still here, still works, and still requires a person to approve. Nobody who was using it has to stop.
The freeze layer, which everything rests on
Frozen clock, killed motion, seeded randomness, pinned fonts and text rendering, blocked or replayed network, and capture-until-two-frames-agree. Paired running does not make this redundant: paired running removes differences between the two builds, and this removes the product's own internal nondeterminism, which is what keeps the measured wobble small enough to be useful. The long version, with what each trick cannot fix, is in docs/how-it-stays-stable.md.
The network interceptor has been promoted from a determinism trick to the safety boundary. It is the answer to "what about a payment": the old build replays recorded traffic and never reaches the real world.
Settings
You should not have to write these. staysfixed init reads the repository —
package.json, the lockfile, the framework config, the folder shapes, the built
artifacts, an .app, an .xcodeproj, a gradlew, a Dockerfile, the test runner,
and every route, exported name and message channel written in the source — works
out what the repository actually makes, and writes settings with an explanation
beside every option. It never overwrites a file you already have. staysfixed init --json is the same answer as one object, which is what an agent should read.
A repository usually makes more than one thing, and a list of four is normal: one repository producing a desktop app, an iPhone app, an Android app and a website is the case this was built against.
What it writes names the product and one block per kind of thing it found —
process for commands, http for a server, web for a website, electron,
android, ios, windows. A product with no screen has no app block, and
that is correct: the picture commands (status, walk, approve, mark,
trace, check --pictures) work by opening something and photographing it, so
they need an app and say so plainly if there is not one. staysfixed check
needs no app at all.
process.skip — what to leave out of the scratch copy
Every run works in a throwaway copy of your project, so a build can write wherever it likes without touching the folder you are working in. That copy is cloned rather than copied: on a Mac and on most Linux filesystems it is one call per file that moves no bytes at all and shares the blocks until something writes to them. Measured on a twelve-gigabyte project, most of it an iOS build folder: 41.5 seconds and no disk used. Where the filesystem cannot clone, it falls back to a real copy — slower, never wrong.
If that copy still takes over twenty seconds the run says so and points you here. You can name folders to leave behind:
export default {
product: 'your-app',
process: {
// Folders not worth copying into the scratch build.
skip: ['DerivedData', '.gradle'],
},
};The bar for putting something on that list is deliberately high: only things
that are regenerated on demand and read by nothing. Caches, coverage reports,
scratch output. Anything you skip that turns out to matter makes a run pass for
the wrong reason — and a run that passes for the wrong reason is the single
failure this whole tool exists to prevent. Build output, node_modules,
lockfiles, fixtures and configuration are all copied, every time, because a check
that runs against a different set of files than the real product is not checking
the real product. If you are not sure, leave it in and let the run be slower.
skip can only ever add to what is already left behind — .git,
.staysfixed, .turbo, .nyc_output, coverage, .pytest_cache,
__pycache__ and .DS_Store. There is no way to switch one of those back on,
because doing so would only ever make runs slower.
A staysfixed.config.json works too, with a declarative steps form and no
functions — so a Rust, Python or Go project can use the tool without anybody
writing JavaScript. It may also live at .staysfixed/config.js (or .mjs, or
.json) if you would rather not have another file in the root.
Every option, and what each kind of product needs on the machine, is in docs/settings.md — every settings key per block, and for each surface the exact programs, environment variables, install commands and permissions, rather than "you may need Xcode".
Do not copy an example to set the difference engine up. Run staysfixed init: it reads what is actually in your project and writes a settings file with
every option in it — the ones that do not apply commented out rather than left
out — filled in from your own code. That is better than any example, because it
is about your project rather than somebody else's.
The three fully commented examples in examples/ — a web
app, an Electron
app and a
guard — are all version 1's
shape, for the picture check, which is the half that needs an app. Each file
says so at the top now. Copy one into a project and a plain staysfixed check
will tell you the settings do not name anything to open, which is correct and
confusing if you were not expecting it.
The design behind all of it is in docs/how-v2-works.md.
Does it actually work?
A tool that reports "nothing changed" looks exactly like a tool that is broken, and there is no way to tell the two apart from the outside. So:
It has to prove it still catches things. staysfixed check --selfcheck
builds twenty tiny products — each a real repository with a working commit and
an uncommitted change on top, which is the shape an agent actually points this
tool at — and requires the engine to behave on every one.
Twelve are breaks it must catch: a route that starts failing, a field dropped from a reply, a different exit code, a file that is no longer written, a door removed from a desktop app, a total quietly rounded, a break buried in the middle of a huge output, a value that used to be steady and is now random, a crash that only shows in what the program said, a charge that moved where nothing in the address mentions money, a run that could only compare half of itself and has to say which half, and a run whose own record could not be saved.
Four are the other half of the same promise: pairs that must produce no findings at all — two identical builds, a product that wobbles, a build that takes ten times longer, and a journey nothing could walk, which has to be named as a hole rather than counted as clean. A tool that cries wolf gets switched off, and a tool that is switched off catches nothing.
One is the third kind: a product so unsteady that the comparison is thrown away before it happens, where the only correct answer is that this run says nothing — and saying "nothing changed" there is the worst thing the tool can do.
And it has to be honest when it cannot tell. A case that misbehaves is built again from scratch and run again before that becomes an accusation. Fail twice and it is reported as a real failure. Behave the second time and it is reported as could not tell — not a pass, not a failure, exit code 2, with the machine's load printed beside it. This exists because the corpus once came back "1 of 9 wrong" with a test suite running alongside it and then passed five times in a row on a quiet machine, and a corpus that can be perturbed by a busy laptop is worth nothing on a busy laptop. The cause was found and removed — see what it will never do — and the re-run stayed, so that the next machine-shaped thing to creep in lands as "nobody knows" rather than as a false accusation people learn to ignore. Measured on 2026-08-30: eleven of eleven, three times running, with the project's own suite running in parallel and the machine's load average between 227 and 334; then twelve of twelve after the second sweep of silences, three times running, with the suite in parallel again and the load average between 208 and 343; and finally twenty of twenty, with the suite in parallel, at the end of the same day.
The unstable app. fixtures/unstable-app is a page built to be impossible to
observe consistently: a clock ticking ten times a second, an endless spinner, a
tween, a shuffled list, a random uuid, a blinking caret, a late image, and a feed
the server answers differently every time. The suite runs it twenty times and
requires every result to be identical. If that fails, nothing else in the suite
matters.
A real desktop app. Pointed at a real Electron application: eleven screens,
two guards, about twenty-five seconds a run, five consecutive runs with nothing
different. Then one line was deleted from the built app — the <link> to its
stylesheet. All eleven failed, and so did the guard written for exactly that bug,
whose reason reads: one release shipped with the whole app unstyled and every one
of its ~3,600 tests passed, because none of them could see it.
What it will never do
Honestly, so you know before you invest an afternoon.
Nothing irreversible, ever. Anything that spends money, sends a message or destroys data is watched at the moment it is asked for — the same charge, the same amount, the same place — and refused at the effect. If a bug only appears after the payment settles or the email lands, this tool is blind to it, by design and permanently. A refusal is reported as a gap in coverage, never as a pass.
A migration that destroys data is refused, not run twice.
It will not tell you your product got slower. How long something took is recorded and shown to you, and it is never compared. A stopwatch on a shared machine measures how busy the machine is at least as much as it measures the product: measured here, thirty runs of the same one-line program on an idle Mac took between 48ms and 96ms, against a bucket boundary at 100ms. Comparing that invents a slowdown nobody caused every time the machine is busy, and a tool that cries wolf gets switched off. A build that hangs is still caught — it gets stopped for taking too long, and how it finished is compared exactly.
A change buried in the middle of a huge output can be missed. Anything a product prints over 64KB has its two ends kept and compared, plus the exact number of bytes thrown away — so a middle that grew or shrank is caught. A middle that changed without changing its length is not, the whole text is written to the evidence folder either way, and the run says out loud that it only compared the ends.
A build that will not answer the same way twice gets no verdict at all. Differences at addresses the new build cannot hold steady are dropped — that is the whole design, and it has one failure shape. If the second run falls over, or the product writes hash-named files, or stamps a fresh id on every line, then most of its addresses are unsteady, almost everything is dropped before it is compared, and what is left is not an answer. A run in that state now says NO ANSWER FROM THIS RUN, in those words, and is not a pass. Fix it by writing a normalisation rule for whatever is moving, not by trusting the clean-looking run underneath it.
What normalisation rubbed out is not itemised on every run. The rules are in your repository — one file,
.staysfixed/rules.json, which you read like any other file in a pull request — and the capture is stamped with which set was used, so a run comparing against a record tidied by a different set says so. There is no command that lists them and no per-run report of what each one rewrote. Anything a rule covers is not being watched, and that is the point of the rule; just know that adding a broad one is how you go blind on purpose.Ranking reads your source, and it gives up on very large trees. Distance from the code you just changed is what sorts the list, and it reads up to 4,000 files of up to 400KB each to work it out. Past that a finding is still reported and still counted — it just may not sort where it deserves to. Nothing is dropped for being far away.
A race that already existed will not show. Subtracting the wobble floor actively hides intermittent bugs. Running the new build twice recovers half of this by flagging anything newly unstable. Only half. That is the sharpest weakness in the whole architecture and it is not going to be dressed up.
A waiver is a judgement, and judgements can be wrong. The gates make an agent's claim falsifiable — it has to be written before the damage is visible, it has to fall inside what was named, there are five of them, and they all die when you ship. What they cannot do is read the agent's mind. The five sealed classes are the answer to that: in the places where being wrong is expensive, no judgement is accepted from any agent at all.
Real phones cannot be paired, and the phone surfaces are the newest thing here. No paired run is possible on a device in your hand: two builds cannot exist on one handset at once. Real iPhones and real Android handsets fall back to comparing against the stored record, and say so out loud on every run. The emulator and the simulator are the honest answer, and both of them compare against the stored record too — for Android because two emulator snapshots restoring byte-identically is unproven, and for iOS because a paired run costs two
xcodebuildpasses and belongs before a release rather than after every edit. If you ship a phone app, askdoctorwhat it is actually covering before you trust a clean result.It is not every possible state. "Deep" means every door the code exposes and every journey it was given. Nothing can enumerate every state, and any tool claiming otherwise is lying. The coverage ledger names the doors it has never opened, so the hole is visible instead of pretended away.
Your own test suite is walked only when you ask.
--journeys suite, orjourneys: "suite"over MCP, runs each test file twice inside the scratch copy and reports every check by name, why each failure failed, and a fingerprint of the test file itself — so an edited test says plainly that the change is yours. It is off by default because running a stranger's whole suite twice on every check would make this too slow to leave switched on, and a check nobody can afford to run is a check nobody runs. It is held to 90 seconds and every file it did not reach is named, one by one, never "some tests were skipped". It earns the ask: take the penny-rounding out of atotal()and the product's own output does not move by one character — the discovered journeys say nothing has changed, and the harvest names the check that turned red.No hosted service, no dashboard, no accounts, no teams, nothing paid. It is a command and a folder of files in your repository.
Pictures still do not travel between operating systems. Text is drawn differently on every system. Pixels are evidence now rather than the accusation, which makes this matter far less than it did — but it has not gone away.
Native Windows shows one desktop, so two builds cannot run at once even in principle. Runs are one after the other and the same-minute guarantee is weaker there than on any platform. If your Windows product is Electron — most are — it is covered properly instead, over the debug port, from any machine. Nothing irreversible can be refused on Windows either: there is no way to block a compiled program's network call without administrator rights, so a journey marked irreversible is refused outright rather than walked.
Not battle-tested. It works, it is used, and it has not yet met the thousand strange apps a widely-used tool meets. If it reports something that is not true, that is the most serious kind of bug it can have — please open an issue.
Licence
MIT. See LICENSE.
Built by Asad Iqbal.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAgent trace diff and prompt regression detection — compares two agent runs to find exactly where reasoning diverged.MIT
- AlicenseAqualityCmaintenanceAI-native browser testing, directly from your coding agent.3MIT
- AlicenseAqualityBmaintenanceA deterministic behavior-compatibility layer for AI agents that checks normalized event traces against operating contracts and compares baselines with candidates to catch regressions in approval, stop, scope, recovery, and completion rules.4MIT
- FlicenseAqualityCmaintenanceEnables AI-assisted visual regression testing by capturing baselines, comparing screenshots, and using AI to judge whether visual differences are bugs or acceptable changes.4
Related MCP Connectors
Browser-backed QA with evidence and fix-ready reports for coding agents.
Verifies AI agent work end to end: real artifacts and outcomes checked, not self-reported success.
AI QA that runs your app in a browser on every pull request: projects, test targets, test cases.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/asadev/staysfixed'
If you have feedback or need assistance with the MCP directory API, please join our Discord server