Security governance for vibe-coded software
A year ago, writing code was the slow part. You'd stare at a blank file, reach for docs, wire the plumbing by hand. Security fit into that rhythm: the same person who wrote the query knew it was a query, and knew it needed a parameter binding.
Vibe coding broke that rhythm in the best possible way. The code appears. A whole authentication flow, a payment webhook, a file-upload endpoint, generated in the time it used to take to name the function. The bottleneck moved. It is no longer writing the code. It is governing it, knowing what shipped, whether it's safe, and how to keep it safe as five more features land on top of it this week.
This is a governance model for that world. Not a checklist you run once and forget, a system that keeps AI-generated software honest as it grows. We'll cover the four layers that matter, why heuristics do most of the heavy lifting, where AI review genuinely helps and where it quietly wastes your time, and how to keep a team from drifting back into "we'll add security later."
The core problem: the generator doesn't know your threat model
A language model writes the most probable code for your prompt. Probable is not the same as safe. If ten thousand tutorials concatenated user input into a SQL string, the model learned that pattern too, and it will happily reproduce it when the surrounding context looks tutorial-shaped.
It's not that the model can't write secure code. Ask it directly to parameterize a query and it will. The problem is that you have to know to ask, on every endpoint, every time, forever. That doesn't scale, and it definitely doesn't survive being handed to a teammate who prompts differently than you do.
Governance is what replaces "remember to ask." It makes the safe path the default path, catches the unsafe path automatically, and gives you a clear picture of what's actually running. Four layers do this.
Layer 1 — Guardrails you commit to the repo
The cheapest, most durable control is a file the AI reads before it writes. A CLAUDE.md, a .cursorrules, an agent instruction file, whatever your tool uses. It sits in the repo, it's version controlled, and every prompt inherits it.
The mistake people make is filling it with vague virtue ("write secure code"). The model already thinks it does. Guardrails work when they're specific and mechanical:
# Security rules (non-negotiable)
- Never build SQL by string concatenation. Use parameterized
queries / the ORM's binding, always.
- Never read secrets from literals. Read from env; if a value
isn't in env, stop and ask.
- Every new POST/PUT/DELETE route gets input validation before
it touches the database.
- Auth-adjacent routes (login, reset, verify) get a rate limit.
- No secret, key, or token in any file under a public/ or
client-bundled directory.
These read like the output of a linter because that's the point. A guardrail that a machine could check is a guardrail the model can follow. The vague ones get ignored; the mechanical ones get obeyed.
This layer is free and it moves the baseline more than anything else on this page. But it's advisory. The model usually follows it. Governance can't rest on "usually," which is why there are three more layers.
Layer 2 — Heuristics that catch the known-bad, fast
Here's the load-bearing claim of this whole piece: for the problems that actually sink vibe-coded apps, a boring heuristic beats a clever model.
The failures we see most are not subtle. An .env committed to git. A hardcoded sk_live_ key in source. A Firebase project with allow read, write: if true. Supabase RLS left off. A dependency that's three majors behind and sitting on a published CVE. Every one of these is a pattern, and patterns are exactly what a regex, a version comparison, or a lookup against a vulnerability database catches instantly and explains completely.
The trade-off is real and it's worth stating plainly:
| Property | Heuristic checks | AI review |
|---|---|---|
| Precision | High, a hit is almost always real | Variable, hedges and hallucinates |
| Recall | Lower, only finds what it knows | Higher, generalizes to novel bugs |
| Explainability | Total, names the file, line, and rule | Lower, "looks suspicious" |
| Speed & cost | Milliseconds, near-zero cost | Seconds, per-token cost |
| Best at | Secrets, config, known CVEs, style | Logic flaws, semantic bugs |
A developer who gets a false positive twice stops reading the tool. High precision is not a nice-to-have; it's what keeps the tool in the workflow at all. So the governance rule is: use heuristics for everything they can cover, and reach for AI only for the class of problem heuristics structurally cannot see.
Concretely, a heuristic pass over a repo should catch, with a named file and line for each:
- A credential-shaped name (
password,api_key,secret,token) assigned a string literal that isn't an env lookup and isn't an obvious placeholder likechangeme. - Committed
.env,serviceAccountKey.json, or a.gitdirectory in a public tree. - A pinned dependency that matches a known-vulnerable version in a CVE database, or a name one keystroke away from a popular package (typosquat).
- A Dockerfile running as root, pulling
:latest, or piping a remote script straight into a shell. - A CI workflow with a secret pasted inline instead of referenced from the encrypted store.
None of that needs a model. All of it is deterministic, instant, and explainable, which means a developer can trust it, act on it, and not resent it.
Layer 3 — AI review, used narrowly and on purpose
AI review earns its keep on exactly the thing heuristics miss: meaning. A regex can't tell you that an authorization check reads the user ID from the request body instead of the session, so any user can act as any other. It can't see that a "delete old records" job has an off-by-one that nukes the newest ones. Those are semantic, and semantics is where a model generalizes past its training in a way pattern-matching never will.
The governance discipline here is to scope it tightly, because unscoped AI review is where the cost and the noise come from:
- Review the diff, not the repo. Point the model at what changed in this branch. It has the context to reason about intent, and you're not paying to re-read a codebase that didn't move.
- Make it argue against itself. A finding that survives "try to prove this is a false positive" is worth surfacing. One that doesn't, isn't. This single step removes most of the plausible-but-wrong noise.
- Let it fix, not just flag. The value of AI on the developer's side is that it can propose the patch and explain it in the same breath. A finding with a ready diff gets fixed; a finding that's just a scary sentence gets ignored.
The right mental model is a two-pass funnel. Heuristics run first and clear the known-bad cheaply and precisely. AI runs second, on a much smaller surface, looking only for the semantic problems the first pass can't represent. You get the precision of the heuristic and the reach of the model without paying for either everywhere.
Layer 4 — Runtime, because static analysis can't see traffic
Everything so far reads code. But some of what governs a live app only exists at runtime: who's actually hitting your login route, whether the request carrying a SQL payload is getting through, whether the site is even up. No amount of source review tells you an attacker is probing /.env right now.
Runtime governance is three things running continuously:
- A WAF that inspects requests and blocks (or deceives, or challenges) the obvious attacks, injection, traversal, bot probes, before they reach code the model may have gotten wrong.
- Uptime and change monitoring so you learn a page went down, or started returning a different body, from a tool instead of from a customer.
- Scheduled re-scans so a leak introduced in a Tuesday commit gets caught Tuesday, not at the next quarterly audit that indie teams never actually run.
The design principle for runtime controls in a vibe-coded stack is fail open. If the protection layer has an incident, the app keeps serving traffic. You lose coverage for the duration of the outage; you never lose availability. Security that can take your product down is a security tool you'll eventually rip out.
Putting it together: what a governed pipeline looks like
None of these layers is exotic. The governance is in wiring them so the safe path is the path of least resistance:
- Write time. The committed guardrail file biases every generation toward the safe pattern. A fast static check on the snippet the AI just wrote catches the classic mistakes before they're even saved.
- Commit time. A heuristic repo scan runs, secrets, config, dependencies, in seconds. It's precise enough to gate a merge without generating a pile of noise nobody triages.
- Review time. AI reviews the diff for semantic problems, argues against its own findings, and hands back patches, not just warnings.
- Run time. The WAF, uptime monitors, and scheduled re-scans watch the deployed app and alert a human the moment something changes.
Notice the shape: heuristics carry the volume, AI carries the nuance, runtime carries what code review can never see. Each layer covers the previous layer's blind spot, and none of them depends on a human remembering to be careful.
The failure mode to watch for: drift
The way governance dies is not a dramatic breach. It's drift. A rule gets added to the guardrail file and then contradicted in a prompt three weeks later. A scan starts throwing a false positive, someone mutes the whole category instead of tuning the one rule, and now real findings hide in the muted pile. A "temporary" hardcoded key becomes permanent because it works and nobody re-reads that file.
The countermeasures are unglamorous and they're the whole game:
- Keep the guardrail file mechanical and short. A rule you can't check is a rule that will drift.
- Tune false positives at the rule level, never by muting a category. A tool you don't trust is a tool you'll stop reading, and an unread tool governs nothing.
- Re-scan on a schedule, not on memory. The point of automation is to not rely on the discipline that vibe coding specifically encourages you to skip.
- Alert a human on change, not on state. "This is still fine" is noise; "this just changed" is signal.
Where this leaves you
Vibe coding didn't make security harder. It made it faster to skip, because the friction that used to force a pause, hand-writing the code, is gone. Governance puts a small, automatic amount of that friction back in exactly the places it matters, and nowhere else.
Commit the guardrails. Let a heuristic scan carry the known-bad, precisely and cheaply. Point AI at the diff for the semantic problems, and make it prove its own findings. Put a fail-open WAF and a monitor in front of the running app. Then let a schedule, not your memory, keep it all honest.
The AI wrote the code. It didn't govern it. That part is still yours, and the good news is that almost all of it can be automated by tools that are fast, precise, and boring, which is exactly what you want standing between a weekend project and the open internet.