← Writing
AI & Process

Your AI Assistant Has No Security Instinct

I asked an AI assistant to connect a small side project to a database. It opened the connection file, pasted the key straight into the code, in plain text, and moved on. The app ran. The connection worked. From the assistant's point of view, the task was done.

That is exactly the problem. It worked, so nobody looked at it again.

This is the third post in a series I'm writing for people who build software with AI assistance without a formal engineering background (vibecoders, for lack of a better word). The first two covered the mindset shift from "someone who ships a working demo" to someone who thinks about the system underneath it. This one is about a specific habit that will bite you if nobody tells you it exists: hardcoded secrets.

What the AI actually did

Ask an AI to "connect the app to Postgres" or "add Stripe" or "call this API," and in most cases it will do the most direct thing possible: it writes the key, token, or connection string exactly where the code needs it. Not because it's careless. Because that's the literal, functional interpretation of what you asked for.

You asked for a connection. It built a connection. Nowhere in that instruction did you specify how the credential should be stored, so the model has no reason to invent an extra layer of indirection you never requested. It optimizes for "the code runs," not for "the code runs and the secret stays out of version control."

If you've read the second post in this series, this will sound familiar: it's the same gap between functional and non-functional requirements. "Connect to the database" is functional, it describes what the app does. "Don't leak the credential" is non-functional, it describes how the system should behave under conditions you didn't spell out. AI models are extremely good at satisfying the functional request in front of them. They have no built-in incentive to guess at the non-functional ones you left unsaid.

Why "private repo" doesn't mean what you think it means

Here's the part that trips people up: a key sitting in your code doesn't feel dangerous while the repo is private. It's just a string in a file only you can see. The risk isn't in that moment. It's in every moment after.

Walk through the actual lifecycle of that credential:

You flip the repo to public later, maybe to add it to your portfolio, maybe because you forgot it was ever private in the first place. The key goes with it, and it's now indexed by every bot that scans GitHub for exactly this pattern, often within minutes.

You invite a collaborator, a friend, someone helping you debug. They now have the credential too, whether or not you meant to hand it over, and you have no way to revoke access to just them later.

You fork the project as a starting point for a new one. Six months from now you don't remember the fork exists, but the original key does, sitting untouched in a repo you stopped thinking about.

You get stuck, paste a chunk of the file into a forum, a Discord, a ChatGPT conversation with a screen-share, to ask for help. The key travels with the fragment, permanently, in a place you don't control and can't delete.

Someone runs git log and finds the key in an earlier commit, even after you "removed" it in a later one. Deleting a line doesn't delete history. The credential is still there, one git blame away.

None of these require a hack. None of them require malice. They're just what normally happens to a codebase over its lifetime: it gets shared, forked, pasted, and revisited long after you've forgotten what's inside it. A secret hardcoded into a file has exactly as many places to leak as your code does to travel, and code travels more than people expect.

What actually works: keep the value out of the code

The fix isn't complicated, and it doesn't require learning a new framework. It's a pattern called environment variables, and once you see it, you'll wonder why it isn't the default.

Instead of writing the real value into your file, you write the name of a variable:

const connectionString = process.env.DATABASE_URL;

DATABASE_URL is not the value. It's a reference to a value that lives somewhere else, outside the file, outside the repo entirely.

On your machine, that value lives in a file usually named .env, sitting in your project root:

DATABASE_URL=postgresql://user:realpassword@host:5432/dbname

That file never gets committed. You add it to .gitignore the moment you create it, before you type anything sensitive into it:

# .gitignore
.env
.env.local

Git now ignores that file completely. It can sit next to your code, get read by your app when it starts up, and never once show up in a commit, a diff, or a git log.

When you deploy, the same logic applies on the hosting side. Vercel, Railway, Render, whatever you use, all of them have an environment variables panel in the project settings. You paste the same DATABASE_URL key and its real value there, once, through the dashboard. Your production app reads it from that panel instead of a local file, but the code doesn't know the difference. It just asks for process.env.DATABASE_URL and gets whatever value is available in the environment it happens to be running in.

That's the whole trick. The code is identical on your laptop and in production. It never contains the actual secret in either place, because it never needed to. The value moves with the environment, not with the file.

Where I actually learned this the hard way

I didn't start out doing this by default. Early on, working on smaller side projects, I'd get a working prototype fast, credentials hardcoded and all, and tell myself I'd "clean it up before pushing to GitHub." Sometimes I remembered. A couple of times I didn't, and only caught it scrolling through an old commit months later, credential sitting there in plain text, in a repo I'd since made public to show as part of my portfolio.

Nothing catastrophic happened. I rotated the keys, cleaned up what I could, and moved on. But it stuck with me that the failure wasn't a hack, wasn't a clever attacker, wasn't even a mistake in the code's logic. It was just a habit I hadn't built yet: treating "where does this value live" as a decision to make on purpose, every time, instead of an afterthought to fix later if I remembered.

That's the shift I'd ask you to make now, before it becomes a repo you have to go back and rotate keys in. It costs nothing upfront. It's one habit: the value goes in .env and the hosting panel, the name goes in the code.

The one extra sentence that changes the outcome

Next time you ask an AI assistant to wire up anything that needs a key, a token, or a password, add one more instruction: use an environment variable, not the raw value.

That's it. One sentence. The assistant will happily do it, because it was never refusing to on purpose, it just needed you to ask. The difference it makes is the difference between a project you can show anyone without checking what's in the files first, and one you have to quietly rewrite the day someone points out your key is sitting on Google.

#DeVibecoderABuilder