Who Updates a Vibe-Coded App? Nobody, and That’s the Problem.
On the dependency responsibility gap in AI-built apps, and the 89 seconds that explain it.
By Tom Raef, We Watch Your Website
On March 31, 2026, a malicious version of axios went live on npm. The first machine was infected 89 seconds later.
Nobody approved it. No developer read a changelog, clicked update, or weighed a tradeoff. A build pipeline somewhere woke up, saw that a package it depended on had a newer version available, and pulled it, because that is exactly what it was configured to do. The version range said anything 1.x or higher is fine. The newest 1.x pulled in a remote access trojan. Eighty-nine seconds. No human. Done.
That detail is worth sitting with, because it cuts against the way we usually talk about security in AI-built apps. The conversation almost always lands on the same question: is the generated code any good? Does the AI write vulnerabilities? It is a fair question and it has an answer. But it is the wrong place to stop, because it treats an app as something that is built once and then simply is what it is.
An app is not a snapshot. It is a living dependency on dozens of other people’s code, and that code keeps moving long after the app ships. The axios that was safe on Tuesday was a weapon on Wednesday. What the AI generated never changed. The ground underneath it did.
So the real question is not whether a vibe-coded app is secure the day it is born. It is who keeps it secure every day after. And when you go looking for that person, you find something uncomfortable: there isn’t one.
To be clear, dependency drift is not some exotic failure mode. It is the normal weather of modern software. Every application built in the last decade sits on a tower of other people’s code, and that code gets patched, updated, deprecated, and occasionally hijacked on a schedule nobody controls. This is the cost of not writing everything yourself, and it is usually a cost worth paying.
What changes from one app to the next is not whether the drift happens. It is whether anyone is watching it.
A traditional engineering team has an answer, even if it is a quiet and imperfect one. Somewhere there is a developer who notices the dependency alert, an ops person who reads the advisory, a lockfile that somebody deliberately committed. The answer is often understaffed and behind schedule. But it exists. There is a human whose job, however loosely, includes this.
Vibe coding’s entire promise is to remove that human. That is the product. You describe what you want and an app appears, without the developer, without the team, without the pipeline anyone configured by hand. For the person building it, this is close to magic, and the magic is real.
But look closely at what got removed. The developer who would have caught the dependency alert was not only writing code. They were also, silently, the fallback owner of every problem like this one. Take them out of the loop and the loop does not get safer. It gets emptier. The work they were doing did not vanish. It just stopped being anyone’s.
That is the uncomfortable shape of it. The same efficiency that makes a vibe-coded app feel effortless is the efficiency that erased the one party who, in every previous model of software, was the answer to the question we just asked.
The contract that never got written
None of this is new as a problem to solve. The industry solved a version of it more than a decade ago, and gave the solution a name.
When companies moved to the cloud, there was an obvious question. If my application runs on Amazon’s servers and something goes wrong, whose fault is it? The answer became the shared responsibility model, and it is now so standard that most engineers can recite it. The provider is responsible for the security of the cloud, the physical machines, the network, the infrastructure underneath. The customer is responsible for security in the cloud, their own code, their data, their configuration. A clean line, drawn on purpose, written into the contract. Everyone knows which side of it they stand on.
Vibe coding inherited almost all of the cloud’s technology. The apps it generates run on the same infrastructure, lean on the same package registries, deploy to the same hosts. What it did not inherit was the line. Nobody ever sat down and wrote the shared responsibility model for an app that a non-developer described in plain English and a machine assembled. The technology arrived. The map of who owns what did not.
So the question of who keeps this app secure over time has no documented answer. Not because anyone refused to write one, but because the situation moved faster than the contracts did. And when there is no written line, every party draws their own, just inside their own boundary. Which is precisely how you end up with a vacuum.
Four reasonable parties, one vacuum
Stand in each party’s shoes for a moment, because the striking thing is that every one of them is behaving reasonably.
Start with the builder, the person who made the app. Legally, they own it. Every vibe platform’s terms of service say so in plain language: the output is yours. But the builder is, by design, not a developer. They do not know what a dependency is. They could not name a single package their app relies on, and they have no reason to think any of this is their job, because the entire pitch was that they would not need to know. They assume, not unreasonably, that the platform handles the technical layer.
Now the platform. It chose the dependencies. It wrote the version ranges. It runs the build. Of everyone in this story, it has the most direct control over the problem. But its terms of service, the same ones that hand the builder ownership, also hand the builder responsibility. The party that selected the components is, on paper, not answerable for them. This is not villainy. It is the standard shape of a software license, the same disclaimer that ships with nearly every tool ever made. It just happens to land in a place where the user genuinely cannot pick up what it sets down.
Then the hosting provider, the company whose servers the app actually runs on. They operate by the shared responsibility model we just described, and that model puts application-layer security, your code and your dependencies, squarely on the customer. By the letter of it, a compromised package is not their concern. And yet some of them act anyway. When chalk and debug were poisoned in September 2025, Vercel went and identified affected projects, purged its build caches, and notified the customers who might have pulled the bad versions. Nothing required them to. That is the tell. When the people best positioned to help are helping out of goodwill rather than obligation, you do not have a system. You have a gap that kind people are papering over.
Last, the registry, npm and the platforms behind it. To their credit they are the ones actually moving, building provenance and trusted publishing so a release can be cryptographically tied to the code it claims to come from. But axios is the cautionary note. Axios had already adopted trusted publishing. The protection was in place. It did not matter, because an old long-lived access token was still sitting in the maintainer’s account, and the attacker simply used that instead, walking around the front door that had just been reinforced.
Four parties. Each one acting sensibly, within its own boundary, by its own lights. And in the space between their four reasonable positions, no one is watching the thing that matters. That space is the vacuum. It is not anyone’s fault, which is exactly what makes it dangerous, because a problem that is nobody’s fault is also, by default, nobody’s job.
Two jobs, and the loophole nobody watches
Here is where it gets specific, because “keeping an app secure over time” is not one job. It is two, and they are easy to confuse.
The first is updating: pulling in the new version of a component when one arrives. The second is verifying: deciding whether that new version is safe before you pull it in. In a serious engineering shop these are different acts handled by different mechanisms. Updates flow through a pipeline. Verification sits at a gate in front of it, a scan, a staging environment, a human who can say not yet. The gate is the whole point. Updating without verifying is just installing whatever the internet published most recently and hoping.
Now look at what a vibe-coded app actually does.
There is no gate. There is barely an act. The update is not a decision anyone makes, it is a side effect, something that happens when the platform rebuilds the project or the user types a new prompt and clicks regenerate. Nobody sat down to update axios. The build resolved to whatever the version range allowed, which on that Tuesday in March was a remote access trojan. The verifying job did not fail. It was never assigned.
Then there is the version range itself. Most vibe-generated projects pin dependencies with a caret, the little ^ that means “this version or anything newer in the same major line.” It looks harmless. It is a trap with no safe side. Leave the range loose and the app auto-resolves to whatever shipped most recently, which is how a build swallows a trojan 89 seconds after it goes live. Pin everything down hard instead and the app freezes in place, quietly accumulating the known, already-patched flaws the loose range would have carried it past. Loose means exposed to tomorrow’s attack. Tight means exposed to yesterday’s. The only safe path runs down the middle, where someone verifies each update before it lands, and that someone is the one we already established does not exist here. The range is not a choice the builder made. It is a default the platform wrote, and most builders will never see it.
And now the part that should bother even the teams who think they have this handled. Suppose you do run a scanner on every build. You read its output, you patch what it flags. You would still have installed the malicious axios, and the poisoned chalk and debug before it. Because those attacks do not appear in a vulnerability scanner at all.
A normal vulnerability gets a CVE, a public identifier saying this version of this package has this flaw, and scanners work by matching your dependencies against that list. But the axios compromise was not a flaw in axios. The code was fine. The maintainer’s account was what got taken. There was no bug to assign a CVE to, so there was no CVE, so the scanner had nothing to match. The only way to catch these is to know the exact bad version numbers and look for them on purpose, and that list lives outside the CVE system, in advisories most tooling never reads.
That is the loophole almost nobody is watching. The attacks that move fastest and hit hardest are the ones built to be invisible to the very tool you were relying on. Catching them is not a matter of scanning more often. It means deliberately carrying the list of known-bad versions the standard tool does not have, and checking for them on purpose.
The platform holds the strongest hand
So who closes the gap?
The honest answer is that one of these four parties is holding nearly all the leverage, and it is the platform. Not because the others are off the hook, but because the platform sits at the one point in the chain where every fix is simply a default waiting to be changed. The builder cannot act, they do not know the problem exists. The host and the registry can only reach so far from where they stand. But the platform writes the scaffold, runs the build, and chooses the settings, which means most of what needs doing is not a research project. It is a configuration the platform is uniquely placed to ship.
And the specific fixes are not exotic. Every one of them is a known, boring piece of supply chain hygiene that careful teams already practice. The only thing missing is someone flipping them on by default for people who will never know to ask.
- Commit a lockfile, every time. A locked dependency tree means a build resolves to the exact versions that were vetted, not whatever happened to be newest at build time. This single default would have closed the 89-second window entirely.
- Add a cooldown on brand-new versions. Do not let a build pull a package version that is only hours old. Make it wait a few days, long enough for the ecosystem to notice if it has been poisoned. Nearly every fast-moving supply chain attack lives and dies inside a window this would have shut.
- Turn off install scripts by default. The axios payload did not run because someone used axios. It ran because a dependency’s install hook fired automatically. Most apps never need those hooks. Off by default, on by exception.
- Carry a known-bad list. Check every build against the exact compromised versions that never get a CVE. That list exists. It just has to be wired in and kept current.
- Show the builder a posture they can read. Not a wall of CVEs. A plain line that says your app depends on this many components, here is when they were last checked, here is whether anything looks wrong. Ownership starts with visibility, and right now the builder has none.
None of this slows the magic down. A builder still types a prompt and watches an app appear. They simply get one assembled with the same care a careful engineer would have used, which is exactly the promise the platform made them in the first place. The platform is not the weak link in this story. It is the strongest hand at the table, and most of the game is still theirs to win.
The part no default can cover
Here is the catch in everything above. Those fixes are defaults, and defaults get set once, at build time. The cooldown is the perfect example, and it is also where the platform’s reach runs out.
The cooldown works beautifully right up until the day a package you already depend on turns out to be vulnerable, and the only fix is a version published an hour ago. Now the very delay that was protecting you is standing between you and the patch you urgently need. The answer the ecosystem has settled on is to split updates by type: hold routine upgrades back, but let genuine security fixes jump the line. Sensible, and already the default in the major tools.
But look at what that exception actually requires. To fast-track the fix, something has to know, right now, that the version you are running is the vulnerable one. And to do it safely, that something cannot simply trust the new version’s own claim to be a security patch, because “this is an urgent fix” is exactly what an attacker writes on the label to skip the line. The decision to override the delay has to be driven by an independent advisory, from outside the package, weighed against this specific app’s actual dependencies, at the moment the advisory drops.
That is not a setting. It is a judgment, and it has to be made continuously, on the world’s schedule rather than the build’s. No build-time default can make it, because the information that triggers it arrives after the app has shipped. This is the part of the job that the platform, for all its leverage, cannot close from where it sits. It is the one corner of the room still dark after every light above is switched on.
Which is the whole point. Somewhere in this chain there has to be a layer that keeps watching after the app goes live, that tracks the running app’s components against the advisories as they break, that can tell a real fix from a poisoned one wearing a fix’s clothes, and that separates the handful of findings that matter from the flood that do not. That layer does not exist anywhere by default. It is not the builder, who never knew. Not the platform, whose job ended at build time. Not the host or the registry, standing too far from the app to see inside it. It is simply missing.
It does not have to be. This is the work some of us already do, taking a torrent of raw signals and verifying each one down to the few that are real, then watching what happens to an app long after everyone else has stopped looking. The point of naming it is not to sell it. It is to say out loud that the role exists, that it is nobody’s by default, and that an app is only as safe over time as whoever, finally, decides to own it.
A gap worth tending
So where does that leave the four parties and the gap between them?
Not in a blame match, which was never the useful frame. The builder cannot own what they cannot see. The platform can close most of the gap and should, and the parts it closes are real wins for everyone downstream. The host and the registry are each moving in the right direction from their own corner. And the last piece, the watching that happens after the app ships, belongs to whoever is willing to stand in that corner and do it. None of these parties replaces the others. Each holds a piece, and the app is safe over time only when the pieces are held at once.
That is the actual ask of this whole argument. Not that any one party carry the weight, but that the industry stop assuming the weight is already being carried. The vacuum is not the failure of any single company. It is a gap that opened because the technology arrived faster than the agreements about who tends it. Gaps like that close one way, by the people on either side deciding, together, that the space between is worth tending.
The 89 seconds we started with were not a fluke. They are what an unwatched chain looks like under load. The good news, and the only news that really matters here, is that none of the fixes are mysterious. The lockfile, the cooldown, the disabled scripts, the known-bad list, the patient eye that keeps watching after launch. Every one of them already exists. They are waiting for someone to decide they are theirs.
* * *
