[{"data":1,"prerenderedAt":1222},["ShallowReactive",2],{"blog-best-practices/ai-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":1194,"date":1195,"dateModified":1195,"description":1196,"draft":1197,"extension":1198,"faq":1199,"featured":1197,"headerVariant":1207,"image":1208,"keywords":1209,"meta":1210,"navigation":94,"ogDescription":1211,"ogTitle":1208,"path":1212,"readTime":1213,"schemaOrg":1214,"schemaType":1215,"seo":1216,"sitemap":1217,"stem":1218,"tags":1219,"twitterCard":1220,"__hash__":1221},"blog/blog/best-practices/ai-api-key-exposure.md","Why AI Code Generators Keep Exposing Your API Keys (and How to Stop It)",{"type":7,"value":8,"toc":1173},"minimark",[9,18,34,47,52,59,62,126,133,137,142,145,163,169,173,176,209,219,223,233,283,296,300,303,316,320,323,355,359,362,374,392,410,422,437,449,453,456,470,483,491,504,512,524,533,537,624,724,877,1028,1044,1048,1085,1138,1157,1169],[10,11,12,13,17],"p",{},"You asked Cursor to add Stripe payments. It generated beautiful checkout code in under a minute. But buried in that code is your live secret key, hardcoded right in the client-side JavaScript. One ",[14,15,16],"code",{},"git push"," later, it's public.",[10,19,20,21,33],{},"This isn't a rare edge case. ",[22,23,24,25,32],"strong",{},"Exposed API keys are the ",[26,27,31],"a",{"href":28,"rel":29},"https://evilmartians.com/chronicles/four-most-common-security-risks-when-vibe-coding-your-app",[30],"nofollow","#1 security finding in vibe-coded apps","."," AI code generators are optimized to produce code that works, not code that's secure. And the gap between \"it works\" and \"it's safe\" is where your keys get leaked.",[35,36,37],"tldr",{},[10,38,39,40,43,44,46],{},"AI code generators frequently hardcode API keys in client-side code, skip ",[14,41,42],{},".gitignore"," patterns, and blur the line between server and browser. Five strategies stop this: use environment variables correctly, keep secrets server-side, add ",[14,45,42],{}," rules before writing code, run secret scanning in CI, and scan your deployed app with CheckYourVibe.",[48,49,51],"h2",{"id":50},"why-ai-tools-get-this-wrong","Why AI Tools Get This Wrong",[10,53,54,55,58],{},"AI models are trained on millions of code samples — tutorials, Stack Overflow answers, README examples, and open-source projects. Most of those samples use ",[22,56,57],{},"hardcoded keys for simplicity",". The AI learns this pattern and reproduces it.",[10,60,61],{},"There's no malicious intent. The model simply doesn't distinguish between \"demo code\" and \"production code.\" When you say \"add Stripe payments,\" it generates the most common pattern it's seen:",[63,64,66],"code-block",{"label":65},"What AI often generates (dangerous)",[67,68,73],"pre",{"className":69,"code":70,"language":71,"meta":72,"style":72},"language-javascript shiki shiki-themes github-light github-dark","// AI-generated code — looks correct, works immediately\nconst stripe = new Stripe(\"sk_live_51abc123...\");\n\nconst session = await stripe.checkout.sessions.create({\n  line_items: [{ price: \"price_1234\", quantity: 1 }],\n  mode: \"payment\",\n  success_url: \"https://yourapp.com/success\",\n});\n","javascript","",[14,74,75,83,89,96,102,108,114,120],{"__ignoreMap":72},[76,77,80],"span",{"class":78,"line":79},"line",1,[76,81,82],{},"// AI-generated code — looks correct, works immediately\n",[76,84,86],{"class":78,"line":85},2,[76,87,88],{},"const stripe = new Stripe(\"sk_live_51abc123...\");\n",[76,90,92],{"class":78,"line":91},3,[76,93,95],{"emptyLinePlaceholder":94},true,"\n",[76,97,99],{"class":78,"line":98},4,[76,100,101],{},"const session = await stripe.checkout.sessions.create({\n",[76,103,105],{"class":78,"line":104},5,[76,106,107],{},"  line_items: [{ price: \"price_1234\", quantity: 1 }],\n",[76,109,111],{"class":78,"line":110},6,[76,112,113],{},"  mode: \"payment\",\n",[76,115,117],{"class":78,"line":116},7,[76,118,119],{},"  success_url: \"https://yourapp.com/success\",\n",[76,121,123],{"class":78,"line":122},8,[76,124,125],{},"});\n",[10,127,128,129,132],{},"That ",[14,130,131],{},"sk_live_"," key is your Stripe secret key. If this code ends up in a client-side bundle or a public repository, anyone can charge your customers, issue refunds, or access your financial data.",[48,134,136],{"id":135},"_5-common-exposure-patterns","5 Common Exposure Patterns",[138,139,141],"h3",{"id":140},"pattern-1-hardcoded-keys-in-source-files","Pattern 1: Hardcoded Keys in Source Files",[10,143,144],{},"The most straightforward mistake. The AI writes a key directly in the code:",[63,146,148],{"label":147},"Hardcoded key in source",[67,149,151],{"className":69,"code":150,"language":71,"meta":72,"style":72},"// AI generates this — key is now in your git history forever\nconst openai = new OpenAI({ apiKey: \"sk-proj-abc123...\" });\n",[14,152,153,158],{"__ignoreMap":72},[76,154,155],{"class":78,"line":79},[76,156,157],{},"// AI generates this — key is now in your git history forever\n",[76,159,160],{"class":78,"line":85},[76,161,162],{},"const openai = new OpenAI({ apiKey: \"sk-proj-abc123...\" });\n",[10,164,165,166,32],{},"Even if you replace it later, the key lives in your git history. Anyone with repo access can find it with ",[14,167,168],{},"git log -p",[138,170,172],{"id":171},"pattern-2-secret-keys-in-client-side-bundles","Pattern 2: Secret Keys in Client-Side Bundles",[10,174,175],{},"AI tools often don't distinguish between server and client code. In Next.js, for example:",[63,177,179],{"label":178},"Secret key accidentally exposed to the browser",[67,180,182],{"className":69,"code":181,"language":71,"meta":72,"style":72},"// AI puts this in a client component — key ships to every browser\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL,\n  process.env.SUPABASE_SERVICE_ROLE_KEY, // This should NEVER be public\n);\n",[14,183,184,189,194,199,204],{"__ignoreMap":72},[76,185,186],{"class":78,"line":79},[76,187,188],{},"// AI puts this in a client component — key ships to every browser\n",[76,190,191],{"class":78,"line":85},[76,192,193],{},"const supabase = createClient(\n",[76,195,196],{"class":78,"line":91},[76,197,198],{},"  process.env.NEXT_PUBLIC_SUPABASE_URL,\n",[76,200,201],{"class":78,"line":98},[76,202,203],{},"  process.env.SUPABASE_SERVICE_ROLE_KEY, // This should NEVER be public\n",[76,205,206],{"class":78,"line":104},[76,207,208],{},");\n",[210,211,212],"danger-box",{},[10,213,214,215,218],{},"Any environment variable accessed in client-side code gets bundled into JavaScript that's ",[22,216,217],{},"sent to every user's browser",". View Source or browser DevTools is all it takes to extract it.",[138,220,222],{"id":221},"pattern-3-env-files-committed-to-git","Pattern 3: .env Files Committed to Git",[10,224,225,226,229,230,232],{},"AI tools generate ",[14,227,228],{},".env"," files with real-looking keys but don't always create or update ",[14,231,42],{},":",[63,234,236],{"label":235},"Generated .env with no .gitignore entry",[67,237,241],{"className":238,"code":239,"language":240,"meta":72,"style":72},"language-bash shiki shiki-themes github-light github-dark","# AI creates this file\nDATABASE_URL=postgresql://user:password@db.example.com:5432/prod\nSTRIPE_SECRET_KEY=sk_live_51abc...\nOPENAI_API_KEY=sk-proj-abc...\n","bash",[14,242,243,249,263,273],{"__ignoreMap":72},[76,244,245],{"class":78,"line":79},[76,246,248],{"class":247},"sJ8bj","# AI creates this file\n",[76,250,251,255,259],{"class":78,"line":85},[76,252,254],{"class":253},"sVt8B","DATABASE_URL",[76,256,258],{"class":257},"szBVR","=",[76,260,262],{"class":261},"sZZnC","postgresql://user:password@db.example.com:5432/prod\n",[76,264,265,268,270],{"class":78,"line":91},[76,266,267],{"class":253},"STRIPE_SECRET_KEY",[76,269,258],{"class":257},[76,271,272],{"class":261},"sk_live_51abc...\n",[76,274,275,278,280],{"class":78,"line":98},[76,276,277],{"class":253},"OPENAI_API_KEY",[76,279,258],{"class":257},[76,281,282],{"class":261},"sk-proj-abc...\n",[10,284,285,286,288,289,291,292,295],{},"If ",[14,287,228],{}," isn't in ",[14,290,42],{},", your next ",[14,293,294],{},"git add ."," commits every secret to your repository.",[138,297,299],{"id":298},"pattern-4-keys-in-ai-chat-context","Pattern 4: Keys in AI Chat Context",[10,301,302],{},"When you paste error messages or code snippets into AI tools, you may include keys in the prompt. These can end up in:",[304,305,306,310,313],"ul",{},[307,308,309],"li",{},"The AI provider's training data (depending on their data policy)",[307,311,312],{},"Chat history logs that other team members can access",[307,314,315],{},"Screenshot-based bug reports",[138,317,319],{"id":318},"pattern-5-overly-permissive-public-keys","Pattern 5: Overly Permissive Public Keys",[10,321,322],{},"AI tools often use full-access keys when scoped, limited keys would suffice:",[63,324,326],{"label":325},"Over-privileged key usage",[67,327,329],{"className":69,"code":328,"language":71,"meta":72,"style":72},"// AI uses the admin key when a read-only key would work\nconst firebase = initializeApp({\n  apiKey: \"AIza...\",\n  // AI doesn't set up Security Rules or use restricted keys\n});\n",[14,330,331,336,341,346,351],{"__ignoreMap":72},[76,332,333],{"class":78,"line":79},[76,334,335],{},"// AI uses the admin key when a read-only key would work\n",[76,337,338],{"class":78,"line":85},[76,339,340],{},"const firebase = initializeApp({\n",[76,342,343],{"class":78,"line":91},[76,344,345],{},"  apiKey: \"AIza...\",\n",[76,347,348],{"class":78,"line":98},[76,349,350],{},"  // AI doesn't set up Security Rules or use restricted keys\n",[76,352,353],{"class":78,"line":104},[76,354,125],{},[48,356,358],{"id":357},"real-world-consequences","Real-World Consequences",[10,360,361],{},"Exposed keys aren't theoretical risks. Here are real incidents that cost companies millions:",[10,363,364,367,368,373],{},[22,365,366],{},"Moltbook: 1.5 million API keys exposed from a vibe-coded app."," Moltbook, an AI social network ",[26,369,372],{"href":370,"rel":371},"https://www.wiz.io/blog/exposed-moltbook-database-reveals-millions-of-api-keys",[30],"built entirely through vibe coding",", launched in January 2026. Within five days, Wiz researchers found a Supabase API key in client-side JavaScript that granted full unauthenticated access to the production database — no Row Level Security configured. The exposed data included 1.5 million API auth tokens, 35,000 email addresses, and private messages containing plaintext OpenAI, Anthropic, and AWS credentials.",[10,375,376,379,380,385,386,391],{},[22,377,378],{},"AWS crypto mining: $45K-$89K overnight bills."," This happens constantly. One founder ",[26,381,384],{"href":382,"rel":383},"https://www.tomshardware.com/news/aws-45000-usd-bill-for-crypto-mining-hack",[30],"woke up to a $45,000 AWS bill"," after attackers found his keys on GitHub and installed crypto miners on Lambda. Palo Alto Networks documented an ",[26,387,390],{"href":388,"rel":389},"https://thehackernews.com/2023/10/elektra-leak-cryptojacking-attacks.html",[30],"organized campaign called EleKtra-Leak"," that automatically harvests AWS keys from GitHub within 5 minutes of being pushed.",[10,393,394,397,398,403,404,409],{},[22,395,396],{},"Stripe key exposures enable direct fraud."," Truffle Security identified ",[26,399,402],{"href":400,"rel":401},"https://trufflesecurity.com/blog/the-risks-of-a-leaked-stripe-api-key",[30],"5 distinct attack paths"," from a single leaked Stripe secret key — including querying all customer PII, issuing unauthorized refunds, and manipulating pricing. A separate ",[26,405,408],{"href":406,"rel":407},"https://jscrambler.com/blog/stripe-api-skimming-campaign",[30],"web skimming campaign"," leveraged Stripe's own API to validate stolen payment cards across 49 merchants.",[10,411,412,415,416,421],{},[22,413,414],{},"OpenAI keys stolen via Chrome extensions."," A fake Chrome extension called \"H-Chat Assistant\" ",[26,417,420],{"href":418,"rel":419},"https://www.obsidiansecurity.com/blog/small-tools-big-risk-when-browser-extensions-start-stealing-api-keys",[30],"harvested over 459 API keys"," from 10,000+ users. Stolen keys gave attackers access to billing accounts — one case involved a key with a $150,000 usage limit.",[10,423,424,427,428,433,434,436],{},[22,425,426],{},"The scale: 23.8 million secrets leaked on GitHub in 2024."," GitGuardian's annual ",[26,429,432],{"href":430,"rel":431},"https://blog.gitguardian.com/the-state-of-secrets-sprawl-2025/",[30],"State of Secrets Sprawl report"," found the number has quadrupled since 2021. Over 70% of secrets leaked in 2022 were still active two years later, and ",[14,435,228],{}," files have a 54% chance of containing at least one secret.",[10,438,439,442,443,448],{},[22,440,441],{},"Even the big guys mess up."," In 2016, attackers found hardcoded AWS credentials in Uber's private GitHub repos and used them to download personal data of 57 million users. The result: a ",[26,444,447],{"href":445,"rel":446},"https://www.breaches.cloud/incidents/uber/",[30],"$148 million settlement"," with all 50 US states and a criminal conviction for Uber's former CSO. If Uber's engineers can leak keys, so can anyone.",[48,450,452],{"id":451},"how-each-ai-tool-handles-secrets","How Each AI Tool Handles Secrets",[10,454,455],{},"Not all AI code generators handle secrets equally. Here's how the major tools compare:",[457,458,460,464],"finding-box",{"title":459},"Cursor",[461,462],"verdict-badge",{"verdict":463},"caution",[10,465,466,467,469],{},"Uses your local codebase and .env files. Respects ",[14,468,42],{}," and won't send ignored files to the AI — but it can still generate code that hardcodes keys if your prompt doesn't specify otherwise.",[457,471,473,476],{"title":472},"Bolt",[461,474],{"verdict":475},"risky",[10,477,478,479,482],{},"Generates full-stack apps entirely in-browser. ",[22,480,481],{},"No built-in secret management"," — keys frequently end up in client-side code. You must manually move secrets to environment variables after generation.",[457,484,486,488],{"title":485},"Lovable",[461,487],{"verdict":475},[10,489,490],{},"Generates and deploys full applications. Has basic environment variable support, but the line between server and client code is blurred. Secrets can easily leak to the client bundle.",[457,492,494,497],{"title":493},"GitHub Copilot",[461,495],{"verdict":496},"safe",[10,498,499,500,503],{},"Inline code completion only. Won't autocomplete recognized API key patterns. GitHub's ",[22,501,502],{},"push protection"," can block commits containing detected secrets.",[457,505,507,509],{"title":506},"V0",[461,508],{"verdict":496},[10,510,511],{},"Focused on UI component generation. Rarely generates backend or secret-handling code, so exposure risk is minimal.",[457,513,515,517],{"title":514},"Replit",[461,516],{"verdict":463},[10,518,519,520,523],{},"Full IDE with built-in deployment. Has a dedicated ",[22,521,522],{},"Secrets tab"," for managing environment variables — but the AI assistant may still hardcode keys directly in source files.",[525,526,527],"warning-box",{},[10,528,529,532],{},[22,530,531],{},"Bolt and Lovable deserve extra caution."," Because they generate entire applications including backend and frontend together, the boundary between server-side and client-side code is easily blurred. Always review generated code for secret placement before deploying.",[48,534,536],{"id":535},"_5-strategies-to-stop-key-exposure","5 Strategies to Stop Key Exposure",[538,539,541,545,559],"step",{"number":540},"1",[138,542,544],{"id":543},"set-up-environment-variables-before-writing-code","Set Up Environment Variables Before Writing Code",[10,546,547,548,551,552,554,555,558],{},"Create your ",[14,549,550],{},".env.local"," and ",[14,553,42],{}," files ",[22,556,557],{},"before"," you start prompting the AI. This establishes the pattern the AI will follow:",[67,560,562],{"className":238,"code":561,"language":240,"meta":72,"style":72},"# Create .gitignore first\necho \".env\\n.env.local\\n.env.production\" >> .gitignore\n\n# Create .env.local with your keys\ntouch .env.local\n\n# Create .env.example with placeholders (safe to commit)\necho \"STRIPE_SECRET_KEY=sk_test_your_key_here\\nOPENAI_API_KEY=sk-your_key_here\" > .env.example\n",[14,563,564,569,584,588,593,602,606,611],{"__ignoreMap":72},[76,565,566],{"class":78,"line":79},[76,567,568],{"class":247},"# Create .gitignore first\n",[76,570,571,575,578,581],{"class":78,"line":85},[76,572,574],{"class":573},"sj4cs","echo",[76,576,577],{"class":261}," \".env\\n.env.local\\n.env.production\"",[76,579,580],{"class":257}," >>",[76,582,583],{"class":261}," .gitignore\n",[76,585,586],{"class":78,"line":91},[76,587,95],{"emptyLinePlaceholder":94},[76,589,590],{"class":78,"line":98},[76,591,592],{"class":247},"# Create .env.local with your keys\n",[76,594,595,599],{"class":78,"line":104},[76,596,598],{"class":597},"sScJk","touch",[76,600,601],{"class":261}," .env.local\n",[76,603,604],{"class":78,"line":110},[76,605,95],{"emptyLinePlaceholder":94},[76,607,608],{"class":78,"line":116},[76,609,610],{"class":247},"# Create .env.example with placeholders (safe to commit)\n",[76,612,613,615,618,621],{"class":78,"line":122},[76,614,574],{"class":573},[76,616,617],{"class":261}," \"STRIPE_SECRET_KEY=sk_test_your_key_here\\nOPENAI_API_KEY=sk-your_key_here\"",[76,619,620],{"class":257}," >",[76,622,623],{"class":261}," .env.example\n",[538,625,627,631,634,715],{"number":626},"2",[138,628,630],{"id":629},"keep-secret-keys-server-side-only","Keep Secret Keys Server-Side Only",[10,632,633],{},"Every framework has a convention for what reaches the browser. Know yours:",[635,636,637,653],"table",{},[638,639,640],"thead",{},[641,642,643,647,650],"tr",{},[644,645,646],"th",{},"Framework",[644,648,649],{},"Server-only",[644,651,652],{},"Client-safe (public)",[654,655,656,672,687,700],"tbody",{},[641,657,658,662,667],{},[659,660,661],"td",{},"Next.js",[659,663,664],{},[14,665,666],{},"process.env.SECRET",[659,668,669],{},[14,670,671],{},"process.env.NEXT_PUBLIC_*",[641,673,674,677,682],{},[659,675,676],{},"Nuxt 3",[659,678,679],{},[14,680,681],{},"runtimeConfig.secret",[659,683,684],{},[14,685,686],{},"runtimeConfig.public.*",[641,688,689,692,695],{},[659,690,691],{},"Vite/Vue",[659,693,694],{},"Not accessible by default",[659,696,697],{},[14,698,699],{},"import.meta.env.VITE_*",[641,701,702,705,710],{},[659,703,704],{},"SvelteKit",[659,706,707],{},[14,708,709],{},"$env/static/private",[659,711,712],{},[14,713,714],{},"$env/static/public",[716,717,718],"tip-box",{},[10,719,720,723],{},[22,721,722],{},"Tell the AI explicitly."," Include \"use server-side API route\" or \"keep this key server-side only\" in your prompt. AI tools follow instructions better when security requirements are stated upfront.",[538,725,727,731,734,831],{"number":726},"3",[138,728,730],{"id":729},"use-server-side-api-proxy-routes","Use Server-Side API Proxy Routes",[10,732,733],{},"Instead of calling third-party APIs directly from the browser, create a server-side route:",[63,735,737],{"label":736},"Server-side API route (Next.js)",[67,738,740],{"className":69,"code":739,"language":71,"meta":72,"style":72},"// app/api/generate/route.js — runs on the server, key never reaches browser\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY, // Server-only, no NEXT_PUBLIC_ prefix\n});\n\nexport async function POST(request) {\n  const { prompt } = await request.json();\n\n  const completion = await openai.chat.completions.create({\n    model: \"gpt-4\",\n    messages: [{ role: \"user\", content: prompt }],\n  });\n\n  return Response.json({ result: completion.choices[0].message.content });\n}\n",[14,741,742,747,752,756,761,766,770,774,779,785,790,796,802,808,814,819,825],{"__ignoreMap":72},[76,743,744],{"class":78,"line":79},[76,745,746],{},"// app/api/generate/route.js — runs on the server, key never reaches browser\n",[76,748,749],{"class":78,"line":85},[76,750,751],{},"import OpenAI from \"openai\";\n",[76,753,754],{"class":78,"line":91},[76,755,95],{"emptyLinePlaceholder":94},[76,757,758],{"class":78,"line":98},[76,759,760],{},"const openai = new OpenAI({\n",[76,762,763],{"class":78,"line":104},[76,764,765],{},"  apiKey: process.env.OPENAI_API_KEY, // Server-only, no NEXT_PUBLIC_ prefix\n",[76,767,768],{"class":78,"line":110},[76,769,125],{},[76,771,772],{"class":78,"line":116},[76,773,95],{"emptyLinePlaceholder":94},[76,775,776],{"class":78,"line":122},[76,777,778],{},"export async function POST(request) {\n",[76,780,782],{"class":78,"line":781},9,[76,783,784],{},"  const { prompt } = await request.json();\n",[76,786,788],{"class":78,"line":787},10,[76,789,95],{"emptyLinePlaceholder":94},[76,791,793],{"class":78,"line":792},11,[76,794,795],{},"  const completion = await openai.chat.completions.create({\n",[76,797,799],{"class":78,"line":798},12,[76,800,801],{},"    model: \"gpt-4\",\n",[76,803,805],{"class":78,"line":804},13,[76,806,807],{},"    messages: [{ role: \"user\", content: prompt }],\n",[76,809,811],{"class":78,"line":810},14,[76,812,813],{},"  });\n",[76,815,817],{"class":78,"line":816},15,[76,818,95],{"emptyLinePlaceholder":94},[76,820,822],{"class":78,"line":821},16,[76,823,824],{},"  return Response.json({ result: completion.choices[0].message.content });\n",[76,826,828],{"class":78,"line":827},17,[76,829,830],{},"}\n",[63,832,834],{"label":833},"Client code calls your API route, not OpenAI directly",[67,835,837],{"className":69,"code":836,"language":71,"meta":72,"style":72},"// components/Generator.jsx — browser code, no secrets here\nasync function generate(prompt) {\n  const res = await fetch(\"/api/generate\", {\n    method: \"POST\",\n    body: JSON.stringify({ prompt }),\n  });\n  return res.json();\n}\n",[14,838,839,844,849,854,859,864,868,873],{"__ignoreMap":72},[76,840,841],{"class":78,"line":79},[76,842,843],{},"// components/Generator.jsx — browser code, no secrets here\n",[76,845,846],{"class":78,"line":85},[76,847,848],{},"async function generate(prompt) {\n",[76,850,851],{"class":78,"line":91},[76,852,853],{},"  const res = await fetch(\"/api/generate\", {\n",[76,855,856],{"class":78,"line":98},[76,857,858],{},"    method: \"POST\",\n",[76,860,861],{"class":78,"line":104},[76,862,863],{},"    body: JSON.stringify({ prompt }),\n",[76,865,866],{"class":78,"line":110},[76,867,813],{},[76,869,870],{"class":78,"line":116},[76,871,872],{},"  return res.json();\n",[76,874,875],{"class":78,"line":122},[76,876,830],{},[538,878,880,884,887,1021],{"number":879},"4",[138,881,883],{"id":882},"add-secret-scanning-to-your-workflow","Add Secret Scanning to Your Workflow",[10,885,886],{},"Catch leaked keys before they reach production:",[63,888,890],{"label":889},"GitHub secret scanning setup",[67,891,895],{"className":892,"code":893,"language":894,"meta":72,"style":72},"language-yaml shiki shiki-themes github-light github-dark","# .github/workflows/secret-scan.yml\nname: Secret Scanning\non: [push, pull_request]\n\njobs:\n  scan:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run Gitleaks\n        uses: gitleaks/gitleaks-action@v2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n","yaml",[14,896,897,902,914,934,938,946,953,963,970,983,994,1004,1011],{"__ignoreMap":72},[76,898,899],{"class":78,"line":79},[76,900,901],{"class":247},"# .github/workflows/secret-scan.yml\n",[76,903,904,908,911],{"class":78,"line":85},[76,905,907],{"class":906},"s9eBZ","name",[76,909,910],{"class":253},": ",[76,912,913],{"class":261},"Secret Scanning\n",[76,915,916,919,922,925,928,931],{"class":78,"line":91},[76,917,918],{"class":573},"on",[76,920,921],{"class":253},": [",[76,923,924],{"class":261},"push",[76,926,927],{"class":253},", ",[76,929,930],{"class":261},"pull_request",[76,932,933],{"class":253},"]\n",[76,935,936],{"class":78,"line":98},[76,937,95],{"emptyLinePlaceholder":94},[76,939,940,943],{"class":78,"line":104},[76,941,942],{"class":906},"jobs",[76,944,945],{"class":253},":\n",[76,947,948,951],{"class":78,"line":110},[76,949,950],{"class":906},"  scan",[76,952,945],{"class":253},[76,954,955,958,960],{"class":78,"line":116},[76,956,957],{"class":906},"    runs-on",[76,959,910],{"class":253},[76,961,962],{"class":261},"ubuntu-latest\n",[76,964,965,968],{"class":78,"line":122},[76,966,967],{"class":906},"    steps",[76,969,945],{"class":253},[76,971,972,975,978,980],{"class":78,"line":781},[76,973,974],{"class":253},"      - ",[76,976,977],{"class":906},"uses",[76,979,910],{"class":253},[76,981,982],{"class":261},"actions/checkout@v4\n",[76,984,985,987,989,991],{"class":78,"line":787},[76,986,974],{"class":253},[76,988,907],{"class":906},[76,990,910],{"class":253},[76,992,993],{"class":261},"Run Gitleaks\n",[76,995,996,999,1001],{"class":78,"line":792},[76,997,998],{"class":906},"        uses",[76,1000,910],{"class":253},[76,1002,1003],{"class":261},"gitleaks/gitleaks-action@v2\n",[76,1005,1006,1009],{"class":78,"line":798},[76,1007,1008],{"class":906},"        env",[76,1010,945],{"class":253},[76,1012,1013,1016,1018],{"class":78,"line":804},[76,1014,1015],{"class":906},"          GITHUB_TOKEN",[76,1017,910],{"class":253},[76,1019,1020],{"class":261},"${{ secrets.GITHUB_TOKEN }}\n",[10,1022,1023,1024,1027],{},"Also enable ",[22,1025,1026],{},"GitHub Push Protection"," in your repository settings. It blocks pushes that contain detected secrets.",[538,1029,1031,1035,1038],{"number":1030},"5",[138,1032,1034],{"id":1033},"scan-your-deployed-application","Scan Your Deployed Application",[10,1036,1037],{},"Secret scanning catches keys in your source code, but some keys only appear in the built, deployed application — embedded in JavaScript bundles, visible in network requests, or leaked through error pages.",[10,1039,1040,1043],{},[22,1041,1042],{},"Run a CheckYourVibe scan"," to detect secrets in your live application. The free tier checks for exposed API keys in client-side bundles, misconfigured environment variables, and publicly accessible configuration endpoints.",[48,1045,1047],{"id":1046},"api-key-security-checklist","API Key Security Checklist",[1049,1050,1052,1057,1061,1065,1069,1073,1077,1081],"checklist-section",{"title":1051},"Before Every Deploy",[1053,1054],"checklist-item",{"description":1055,"label":1056},"Search your codebase: grep -r 'sk*live\\\\|sk_test\\\\|sk-proj' --include='*.ts' --include='_.js'","No hardcoded API keys in source files",[1053,1058],{"description":1059,"label":1060},"Run: git ls-files | grep -E '^\\\\.env' — should return nothing",".env and .env.local are in .gitignore",[1053,1062],{"description":1063,"label":1064},"No secret keys use NEXT_PUBLIC_, VITE*, or NUXT_PUBLIC* prefixes","Secret keys are server-side only",[1053,1066],{"description":1067,"label":1068},"Browser code calls your API, not third-party services directly","Third-party API calls go through server routes",[1053,1070],{"description":1071,"label":1072},"Gitleaks, TruffleHog, or GitHub Push Protection is enabled","Secret scanning runs in CI",[1053,1074],{"description":1075,"label":1076},"Use restricted/read-only keys where possible, not admin keys","API keys are scoped with minimum permissions",[1053,1078],{"description":1079,"label":1080},"Documents required variables without exposing real keys",".env.example exists with placeholder values",[1053,1082],{"description":1083,"label":1084},"Run a CheckYourVibe scan to catch keys in production bundles","Deployed app scanned for client-side key leakage",[1086,1087,1088,1095,1108,1114,1132],"faq-section",{},[1089,1090,1092],"faq-item",{"question":1091},"Do AI code generators intentionally expose API keys?",[10,1093,1094],{},"No. AI models optimize for functional code, not secure code. They generate patterns that work in development without considering production security implications like secret management.",[1089,1096,1098],{"question":1097},"Are public keys like Supabase anon keys safe to expose?",[10,1099,1100,1101,927,1104,1107],{},"Supabase anon keys and Firebase public config are designed to be client-facing when used with Row Level Security (RLS). But secret keys like ",[14,1102,1103],{},"service_role",[14,1105,1106],{},"sk_live",", or any key prefixed with \"secret\" must never appear in client code.",[1089,1109,1111],{"question":1110},"What should I do if my API key is already exposed?",[10,1112,1113],{},"Rotate the key immediately in the service's dashboard. Don't try to remove it from git history — assume it's compromised. Check service logs for unauthorized usage, update production environment variables with the new key, and set up monitoring for anomalies.",[1089,1115,1117],{"question":1116},"Can I use environment variables in frontend code?",[10,1118,1119,1120,1123,1124,1127,1128,1131],{},"Only for public keys. In Next.js, only ",[14,1121,1122],{},"NEXT_PUBLIC_"," prefixed vars reach the browser. In Vite/Nuxt, only ",[14,1125,1126],{},"VITE_"," or ",[14,1129,1130],{},"NUXT_PUBLIC_"," prefixed vars are exposed. Secret keys must stay server-side and be accessed through API routes.",[1089,1133,1135],{"question":1134},"How do I know if my deployed app has exposed keys?",[10,1136,1137],{},"Run a free CheckYourVibe scan. It checks your deployed application for API keys in client-side JavaScript bundles, exposed environment variables, and other secret leakage patterns.",[1139,1140,1141,1147,1152],"related-articles",{},[1142,1143],"related-card",{"description":1144,"href":1145,"title":1146},"Exposed keys are just one of 8 attack vectors in AI-built apps","/blog/vulnerabilities/how-ai-apps-are-vulnerable","How AI-Generated Apps Are Vulnerable to Attacks",[1142,1148],{"description":1149,"href":1150,"title":1151},"Lovable app with backwards auth logic leaked 18,697 user records including students.","/blog/stories/lovable-app-exposed-18000-users","How a Lovable App Exposed 18,000 Users",[1142,1153],{"description":1154,"href":1155,"title":1156},"Step-by-step guide to securing API keys in your web app","/blog/how-to/secure-api-keys","How to Secure API Keys",[1158,1159,1162,1166],"cta-box",{"href":1160,"label":1161},"/","Start Free Scan",[48,1163,1165],{"id":1164},"is-your-app-leaking-api-keys","Is Your App Leaking API Keys?",[10,1167,1168],{},"AI-generated code may have exposed your secrets. Run a free CheckYourVibe scan to find API keys in your client-side bundles, misconfigured environment variables, and more.",[1170,1171,1172],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":72,"searchDepth":85,"depth":85,"links":1174},[1175,1176,1183,1184,1185,1192,1193],{"id":50,"depth":85,"text":51},{"id":135,"depth":85,"text":136,"children":1177},[1178,1179,1180,1181,1182],{"id":140,"depth":91,"text":141},{"id":171,"depth":91,"text":172},{"id":221,"depth":91,"text":222},{"id":298,"depth":91,"text":299},{"id":318,"depth":91,"text":319},{"id":357,"depth":85,"text":358},{"id":451,"depth":85,"text":452},{"id":535,"depth":85,"text":536,"children":1186},[1187,1188,1189,1190,1191],{"id":543,"depth":91,"text":544},{"id":629,"depth":91,"text":630},{"id":729,"depth":91,"text":730},{"id":882,"depth":91,"text":883},{"id":1033,"depth":91,"text":1034},{"id":1046,"depth":85,"text":1047},{"id":1164,"depth":85,"text":1165},"best-practices","2026-02-19","AI code generators like Cursor, Bolt, and Lovable frequently hardcode API keys in client-side code. Learn why this happens and 5 proven strategies to prevent it.",false,"md",[1200,1201,1203,1204,1206],{"question":1091,"answer":1094},{"question":1097,"answer":1202},"Supabase anon keys and Firebase public config are designed to be client-facing when used with Row Level Security (RLS). But secret keys like service_role, sk_live, or any key prefixed with 'secret' must never appear in client code.",{"question":1110,"answer":1113},{"question":1116,"answer":1205},"Only for public keys. In Next.js, only NEXT_PUBLIC_ prefixed vars reach the browser. In Vite/Nuxt, only VITE_ or NUXT_PUBLIC_ prefixed vars are exposed. Secret keys must stay server-side and be accessed through API routes.",{"question":1134,"answer":1137},"vibe-green",null,"AI code generator security, API key exposure, vibe coding security, exposed API keys, secret scanning, Cursor security, Bolt security, Lovable security",{},"AI code generators frequently expose API keys. Learn 5 strategies to stop it.","/blog/best-practices/ai-api-key-exposure","12 min read","[object Object]","Article",{"title":5,"description":1196},{"loc":1212},"blog/best-practices/ai-api-key-exposure",[],"summary_large_image","jcJV6aQrEjhcwZX8dnP6B5GEDDhSRMpYXNLriTLTXSg",1775843925085]