SECURE AI CODING PRACTICES: PROMPTS AND WORKFLOW THAT ACTUALLY WORK
AI coding tools optimize for “runs on first try,” not “survives an attacker.” Telling yourself you’ll prompt more carefully doesn’t scale — you will forget, and the model will happily ship a working, insecure handler. What scales is moving security out of your working memory and into artifacts: instruction files the model reads on every request, prompts you paste rather than compose, scaffolding that makes the insecure path harder than the secure one, and a scan that catches what everything else missed. Six practices, each concrete enough to adopt today.
1. Put security context in the instruction file, not the prompt
Every serious tool reads a persistent instruction file — CLAUDE.md for Claude Code, .cursorrules (or .cursor/rules/) for Cursor, .github/copilot-instructions.md for Copilot. Rules placed there apply to every generation without you remembering to ask. This is the highest-leverage five minutes in this article. A block that works as-is:
## Security rules (non-negotiable)
- Parameterized queries only. Never interpolate variables into SQL,
shell commands, or HTML.
- Every route handler: validate body/query/params with zod at the top,
reject with 400 before any logic runs.
- Every route that reads or writes a resource by id: check the
authenticated user owns that resource. 403 otherwise.
- Secrets come from process.env only. Never write a literal API key,
token, or password into code, tests, or examples.
- Passwords: bcrypt or argon2. Tokens: crypto.randomBytes. Never
MD5/SHA-1 for anything security-related, never Math.random for tokens.
- Error responses: generic message to the client, full detail to the
server log. Never return err.message, stack traces, or DB errors.
- Do not add new dependencies without flagging them in your summary.
Keep it short and imperative — long instruction files get diluted. Adapt the validator and hashing library names to your stack, then leave it alone.
2. The re-review prompt
The model that wrote the code will also critique it competently — but only if you ask in a separate turn, because generation and review are different tasks. After any change touching auth, data access, file handling, or payments, paste this before you read the diff yourself:
Review the code you just wrote as a hostile security reviewer. For each
function, answer specifically:
1. Can any input reach a query, command, file path, or HTML output
without validation? Show the line.
2. Can an authenticated user reach another user's data by changing an
id, email, or filename in the request?
3. What appears in the response and in logs when this code throws?
4. Are any secrets, tokens, or connection strings in the code itself?
Fix every issue you find and list what you changed. If something is
fine, say why in one line - do not pad the list.
This routinely catches missing ownership checks and leaky error handlers the first pass skipped. It is not a substitute for human review or scanning — it is a cheap filter that makes the human review shorter. The full framework for the human pass is in the AI Code Review Guide.
3. Validation-first scaffolding
Order of operations matters. If you ask for “an endpoint to create projects,” validation is an afterthought the model may skip. If the schema and guard exist first, the model completes inside them — AI tools are continuation engines, and they continue whatever structure is already on screen. Write (or generate) this shell first:
const CreateProject = z.object({
name: z.string().min(1).max(120),
visibility: z.enum(["private", "public"]).default("private"),
});
app.post("/api/projects", requireAuth, (req, res) => {
const parsed = CreateProject.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: "invalid input" });
// TODO: implement using parsed.data and req.user.id only
});
Then prompt: “Implement the TODO. Use only parsed.data and req.user.id — never req.body directly.” The suggestion inherits auth, validation, and the ownership scope because they were already there. The inverse — generating the handler first and bolting validation on later — is exactly how BOLA in AI-generated CRUD ships.
4. Vet every dependency the model suggests
AI tools recommend packages by name, and some of those names don’t exist upstream — attackers pre-register the recurring hallucinations on npm and PyPI (slopsquatting). Others are real but abandoned or compromised. Make this 30-second check a reflex before any install the model proposed:
npm view express-schema-validator # does it exist, when was it published?
npm view <pkg> maintainers repository.url
Reject anything that fails the sniff test: published recently with trivial downloads, no repository link, or a name suspiciously close to a package you actually meant. Then ask the model: “Can this be done with packages already in package.json, or with the standard library?” — the answer is yes more often than not, and the best dependency is the one you didn’t add. Check your existing tree with the Package Hallucination Scanner and keep npm audit in CI.
5. Secrets discipline in prompts
Prompts are outbound data. Whatever you paste — connection strings, live API keys, customer records used to “reproduce the bug” — leaves your machine and lands in provider logs and chat history, and may be echoed back into generated code or example files. Three rules:
- Paste shapes, not values.
postgres://user:REDACTED@host/dbreproduces a connection bug exactly as well as the real string. - Keep secret files out of the tool’s context.
.env, key material, and customer fixtures belong in.cursorignore/ Copilot content exclusions, and.envbelongs in.gitignoreregardless. - When the model writes a literal key-shaped string, replace it with
process.env.Xbefore accepting — placeholder keys become real keys “just to test,” and then they’re in git history. Anything that ever touched a commit gets rotated.
Back the habit with a gitleaks pre-commit hook, and verify nothing already leaked into your shipped frontend with the Token Leak Checker.
6. Close the loop: scan after every deploy
Everything above reduces the defect rate; nothing above gets it to zero. Instruction files get ignored under long context, re-review prompts miss cross-file issues, and the one PR merged in a hurry is the one with the bug. The backstop is a dynamic scan of the deployed app — not the source — on every release, because the classes AI tools ship most (missing RLS, BOLA, permissive CORS, leaked keys, verbose errors) are exactly the ones visible from outside.
The loop: deploy, scan with the Vibe Code Scanner, then feed each finding back to the tool as a fix prompt — “The deployed app returns other users’ invoices at GET /api/invoices/:id with any valid session. Add an ownership check and show me the diff.” — and rescan to confirm. Findings become fix prompts; fix prompts become instruction-file rules (practice 1) so the same class doesn’t recur. That feedback cycle, not any single prompt, is what makes AI-assisted teams ship secure code. Where scanning sits relative to SAST and pentests is covered in Between SAST and Pentest.
Where this fits
These practices are tool-agnostic. For what each tool gets wrong specifically, read the Cursor and Copilot risk profiles; for what you’re defending against, the vibe coding vulnerability taxonomy and the OWASP Top 10 for AI code.
Related resources
- AI Code Review Guide — the human review pass these practices feed into
- Vibe Coding Vulnerabilities — what ships when the practices are skipped
- Copilot Security Risks — pattern deep-dive for Copilot
- Cursor Security Risks — pattern deep-dive for Cursor
- SAST Tools for AI Code — static gates for CI
- Free Security Self-Audit — a one-hour manual pass over a deployed app
SCAN YOUR DEPLOYED APP
Paste your live URL. We probe exposed keys, missing auth, open databases, and broken access control — results in under 60 seconds. 14-day trial, no card.
14-day free trial · No credit card · Cancel anytime