The security problem with vibe coding is not that agents write insecure code on purpose. It is that they write plausible code fast, and plausible code passes the only test most vibe coding applies: it runs. Security holes do not stop an app from running. They sit there quietly until someone finds them, and the someone is rarely you.
I want to be precise here, not alarmist. Your weekend app is probably not being actively targeted. But the moment it holds a real user’s data or takes a real payment, the gap between “it works” and “it is safe to operate” becomes your problem, and that gap has a predictable shape. Generated code that nobody reviewed tends to leak in the same three places. Let me walk each one, with the mechanism, so you can close it instead of worrying about it.
Hole 1: secrets in the code
The most common and the easiest to fix. You ask an agent to connect to a database, an email service, a payment provider. It writes working code, and to make it work it puts the API key right there in the source. Sometimes it hardcodes it in a config file. Sometimes it commits your .env because nothing told it not to.
The mechanism is simple: the agent optimizes for the code running now, and a literal key runs. A key read from an environment variable also runs, but it requires a second step the agent has no reason to prioritize unless you asked. So it takes the shortcut, and the shortcut ends up in your git history, where it stays even after you delete the line.
Closing it is mechanical. Move every credential out of source and into environment variables or a secrets store. Add the secret files to .gitignore before the first commit, not after. If a key ever touched a commit, rotate it, because git remembers. And scan the history, since the key you hardcoded in week one is still sitting in the log you forgot about.
Hole 2: input that nobody validated
This is the expensive one, and the one an agent is least likely to handle unprompted.
Every place your app accepts input from outside is a place something can go wrong: a form field, a URL parameter, an uploaded file, a request body. Secure code treats all of it as hostile until proven otherwise. Generated code usually treats it as well-formed, because the happy path is what the prompt described. The agent built the feature you asked for. You did not ask it to assume the user is an attacker, so it did not.
The result is the whole OWASP Top 10 reading like a checklist of things your app probably does wrong: injection because a query was built by concatenating a string the user controls, broken access control because an endpoint checks that you are logged in but not that the record is yours, data exposure because an error handler returns the full stack trace. None of these break the app. All of them are doors.
The fix is not glamorous and it is not optional. Validate and sanitize input at the boundary, following something concrete like the OWASP input validation guidance rather than your own intuition. Use parameterized queries so user input is never concatenated into a command. Check authorization on the record, not just the session. These are the patterns an agent will apply well once you make them part of the contract, and skip entirely when you do not.
Hole 3: dependencies nobody vetted
An agent reaches for libraries the way you would reach for a search result. It picks a package that solves the problem, adds it to your manifest, and moves on. It does not check whether the package is maintained, whether it has known vulnerabilities, or whether it is even the package you think it is.
That last point matters more than it sounds. Attackers publish packages with names close to popular ones, betting on a typo or a confident autocomplete. An agent generating a dependency list from memory is exactly the kind of confident autocomplete they are betting on. You can end up importing something nobody audited into the core of your app, and it arrived because a model suggested it and you did not look.
The fix is to put a gate between “a dependency was added” and “the dependency ships.” Scan your dependency tree for known vulnerabilities, and do it continuously, because a package that was clean when the agent added it can be found vulnerable next month. This is exactly the practice I wrote about in securing AI code with Snyk: the dependency layer is where generated code inherits other people’s mistakes, and the only defense is to check rather than trust.
Why review is the thing that broke
Notice what all three holes share. None of them is exotic. A developer reading the code would catch the hardcoded key, question the unvalidated form, and raise an eyebrow at the unfamiliar dependency. The holes exist because nobody read the code, and nobody read it because the entire appeal of vibe coding is not having to.
That is the real tension. The speed comes from skipping review, and the security comes from review. You cannot keep the speed and delete the risk. What you can do is move the review from “read every line” to “check the three things that actually leak,” which is a much smaller job.
This is also where the line between vibe coding and AI-assisted engineering shows up in the sharpest possible way. Vibe coding trusts the output because it runs. Engineering treats security as part of the definition of done, and refuses to call a feature finished until the boundary is validated, the secrets are out of source, and the dependencies are clean. Same agent, same speed, different contract.
The debt framing, applied to security
Unclosed security holes are a specific kind of vibe coding debt, and they behave like the worst kind: cheap to carry, catastrophic to settle. A missing input check costs nothing until the day it costs everything, and unlike a slow feature, you do not get a warning that the bill is coming. Someone else schedules the payment for you.
That is why security is not something you bolt on at the end of the journey from vibe coding to production. It is one of the gates that defines whether you have a product or a liability with a nice UI.
Where PaellaDoc fits
The reason these holes survive is that “did we check the secrets, the input, the dependencies” is a question nobody wrote down, so nobody answered it. The check lives in a person’s head, and heads forget, especially when the agent moved faster than the reviewer.
PaellaDoc turns those checks into part of the contract for a change and keeps the evidence that they ran, attached to the code they cover, instead of relying on you to remember. The point is not to slow the agent down. It is to make sure that when a change closes, the boring, load-bearing security questions have real answers on record, not an assumption that the code was fine because it ran.
FAQ
Is vibe-coded software inherently insecure? No. It is unreviewed, which is different. The same agent writes secure code when security is part of the contract, and leaky code when the only requirement is that it runs. The risk comes from skipping review, not from the model.
What is the single most important thing to check first? Secrets. Make sure no API keys or credentials live in your source or git history, because that hole is the easiest to exploit and the easiest to close. Then move to input validation and dependencies.
Do I need a security expert to fix this? For most vibe-coded apps, no. Getting secrets out of source, validating input at the boundary using established guidance, and scanning your dependencies closes the common holes. A specialist matters once you handle sensitive data at scale, not on day one.