[{"data":1,"prerenderedAt":1580},["ShallowReactive",2],{"blog-how-to/fix-cursor-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":1552,"date":1553,"dateModified":1553,"description":1554,"draft":1555,"extension":1556,"faq":1557,"featured":1555,"headerVariant":1565,"image":1566,"keywords":1567,"meta":1568,"navigation":171,"ogDescription":1569,"ogTitle":1566,"path":1570,"readTime":1571,"schemaOrg":1572,"schemaType":1573,"seo":1574,"sitemap":1575,"stem":1576,"tags":1577,"twitterCard":1578,"__hash__":1579},"blog/blog/how-to/fix-cursor-api-key-exposure.md","How to Fix Cursor API Key Exposure (2026)",{"type":7,"value":8,"toc":1536},"minimark",[9,13,16,38,43,46,56,69,81,85,228,312,389,414,461,467,471,474,482,490,498,506,514,520,524,586,593,658,662,667,673,678,747,752,956,963,967,970,975,1032,1037,1220,1224,1288,1292,1298,1306,1309,1313,1317,1435,1439,1448,1454,1463,1471,1505,1524,1532],[10,11,12],"p",{},"Cursor's AI assistant can write a functioning API call in seconds. It can also bake your OpenAI key directly into that function if you are not watching closely. The model suggests code based on context it sees in your codebase, and if a key appears anywhere in that context, it may echo it right back into a new file you are about to commit.",[10,14,15],{},"This guide covers the three most common Cursor-specific exposure patterns, how to find them, and how to fix the root cause for Next.js and Vite apps.",[17,18,19],"tldr",{},[10,20,21,22,26,27,30,31,26,34,37],{},"Cursor API key exposure comes from three sources: Cursor's AI hardcoding a key it saw in your codebase context, a ",[23,24,25],"code",{},"NEXT_PUBLIC_"," or ",[23,28,29],{},"VITE_"," prefixed variable baking the secret into your client bundle, or a committed ",[23,32,33],{},".env",[23,35,36],{},".cursorrules"," file. Rotate the exposed key first, then fix the variable name or move the call to a Route Handler or Netlify Function. Scan generated code for key patterns before every commit.",[39,40,42],"h2",{"id":41},"the-three-cursor-specific-exposure-patterns","The Three Cursor-Specific Exposure Patterns",[10,44,45],{},"Unlike app builders that deploy your app, Cursor is an IDE. The exposure risks are different.",[10,47,48,52,53,55],{},[49,50,51],"strong",{},"Pattern 1: AI echoes a key from context."," Cursor reads files in your project to provide accurate suggestions. If your ",[23,54,33],{}," or a config file is open, the model may suggest that exact key value in a new file.",[10,57,58,61,62,26,65,68],{},[49,59,60],{},"Pattern 2: Wrong variable prefix."," Cursor generates ",[23,63,64],{},"NEXT_PUBLIC_OPENAI_API_KEY",[23,66,67],{},"VITE_OPENAI_API_KEY"," because it has seen those patterns frequently in training data. Both prefixes are designed to put values into the browser bundle.",[10,70,71,77,78,80],{},[49,72,73,74,76],{},"Pattern 3: Secrets in ",[23,75,36],{},"."," Developers sometimes paste API keys into ",[23,79,36],{}," to help Cursor understand their service setup. That file gets committed, and the key is in git history.",[39,82,84],{"id":83},"step-1-find-the-exposed-key","Step 1: Find the Exposed Key",[86,87,89,94,221],"step",{"number":88},"1",[10,90,91],{},[49,92,93],{},"Search your codebase for key patterns.",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-bash shiki shiki-themes github-light github-dark","# Common key prefix patterns in Cursor-generated code\ngrep -rn \"sk-proj-\\|sk-ant-api\\|sk_live_\\|rk_live_\\|AKIA\\|eyJhbGci\" . \\\n  --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\" --include=\"*.jsx\" \\\n  --exclude-dir=node_modules\n\n# Also search for generic secret assignment patterns\ngrep -rn \"apiKey.*=.*['\\\"][A-Za-z0-9_\\-]\\{20,\\}\" . \\\n  --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\" \\\n  --exclude-dir=node_modules\n","bash","",[23,102,103,112,133,160,166,173,179,199,216],{"__ignoreMap":100},[104,105,108],"span",{"class":106,"line":107},"line",1,[104,109,111],{"class":110},"sJ8bj","# Common key prefix patterns in Cursor-generated code\n",[104,113,115,119,123,127,130],{"class":106,"line":114},2,[104,116,118],{"class":117},"sScJk","grep",[104,120,122],{"class":121},"sj4cs"," -rn",[104,124,126],{"class":125},"sZZnC"," \"sk-proj-\\|sk-ant-api\\|sk_live_\\|rk_live_\\|AKIA\\|eyJhbGci\"",[104,128,129],{"class":125}," .",[104,131,132],{"class":121}," \\\n",[104,134,136,139,142,145,148,150,153,155,158],{"class":106,"line":135},3,[104,137,138],{"class":121},"  --include=",[104,140,141],{"class":125},"\"*.ts\"",[104,143,144],{"class":121}," --include=",[104,146,147],{"class":125},"\"*.tsx\"",[104,149,144],{"class":121},[104,151,152],{"class":125},"\"*.js\"",[104,154,144],{"class":121},[104,156,157],{"class":125},"\"*.jsx\"",[104,159,132],{"class":121},[104,161,163],{"class":106,"line":162},4,[104,164,165],{"class":121},"  --exclude-dir=node_modules\n",[104,167,169],{"class":106,"line":168},5,[104,170,172],{"emptyLinePlaceholder":171},true,"\n",[104,174,176],{"class":106,"line":175},6,[104,177,178],{"class":110},"# Also search for generic secret assignment patterns\n",[104,180,182,184,186,189,192,195,197],{"class":106,"line":181},7,[104,183,118],{"class":117},[104,185,122],{"class":121},[104,187,188],{"class":125}," \"apiKey.*=.*['",[104,190,191],{"class":121},"\\\"",[104,193,194],{"class":125},"][A-Za-z0-9_\\-]\\{20,\\}\"",[104,196,129],{"class":125},[104,198,132],{"class":121},[104,200,202,204,206,208,210,212,214],{"class":106,"line":201},8,[104,203,138],{"class":121},[104,205,141],{"class":125},[104,207,144],{"class":121},[104,209,147],{"class":125},[104,211,144],{"class":121},[104,213,152],{"class":125},[104,215,132],{"class":121},[104,217,219],{"class":106,"line":218},9,[104,220,165],{"class":121},[10,222,223,224,227],{},"If any match returns an actual key value (not a ",[23,225,226],{},"process.env."," reference), that key is hardcoded.",[86,229,231,236,302],{"number":230},"2",[10,232,233],{},[49,234,235],{},"Check for NEXT_PUBLIC_ and VITE_ prefixed secrets.",[95,237,239],{"className":97,"code":238,"language":99,"meta":100,"style":100},"# Next.js: any secret-like var with the client-bundle prefix\ngrep -rn \"NEXT_PUBLIC_\" . --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.env*\"\n\n# Vite / React: same prefix class\ngrep -rn \"VITE_\" . --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.env*\"\n",[23,240,241,246,270,274,279],{"__ignoreMap":100},[104,242,243],{"class":106,"line":107},[104,244,245],{"class":110},"# Next.js: any secret-like var with the client-bundle prefix\n",[104,247,248,250,252,255,257,259,261,263,265,267],{"class":106,"line":114},[104,249,118],{"class":117},[104,251,122],{"class":121},[104,253,254],{"class":125}," \"NEXT_PUBLIC_\"",[104,256,129],{"class":125},[104,258,144],{"class":121},[104,260,141],{"class":125},[104,262,144],{"class":121},[104,264,147],{"class":125},[104,266,144],{"class":121},[104,268,269],{"class":125},"\"*.env*\"\n",[104,271,272],{"class":106,"line":135},[104,273,172],{"emptyLinePlaceholder":171},[104,275,276],{"class":106,"line":162},[104,277,278],{"class":110},"# Vite / React: same prefix class\n",[104,280,281,283,285,288,290,292,294,296,298,300],{"class":106,"line":168},[104,282,118],{"class":117},[104,284,122],{"class":121},[104,286,287],{"class":125}," \"VITE_\"",[104,289,129],{"class":125},[104,291,144],{"class":121},[104,293,141],{"class":125},[104,295,144],{"class":121},[104,297,147],{"class":125},[104,299,144],{"class":121},[104,301,269],{"class":125},[10,303,304,305,26,308,311],{},"Variables like ",[23,306,307],{},"NEXT_PUBLIC_OPENAI_KEY",[23,309,310],{},"VITE_STRIPE_SECRET"," are in every visitor's browser.",[86,313,315,323,382],{"number":314},"3",[10,316,317],{},[49,318,319,320,322],{},"Inspect your ",[23,321,36],{}," file.",[95,324,326],{"className":97,"code":325,"language":99,"meta":100,"style":100},"cat .cursorrules 2>/dev/null || cat .cursor/rules 2>/dev/null\n# Also check if it was ever committed\ngit log --all --oneline -- .cursorrules\n",[23,327,328,357,362],{"__ignoreMap":100},[104,329,330,333,336,340,343,346,349,352,354],{"class":106,"line":107},[104,331,332],{"class":117},"cat",[104,334,335],{"class":125}," .cursorrules",[104,337,339],{"class":338},"szBVR"," 2>",[104,341,342],{"class":125},"/dev/null",[104,344,345],{"class":338}," ||",[104,347,348],{"class":117}," cat",[104,350,351],{"class":125}," .cursor/rules",[104,353,339],{"class":338},[104,355,356],{"class":125},"/dev/null\n",[104,358,359],{"class":106,"line":114},[104,360,361],{"class":110},"# Also check if it was ever committed\n",[104,363,364,367,370,373,376,379],{"class":106,"line":135},[104,365,366],{"class":117},"git",[104,368,369],{"class":125}," log",[104,371,372],{"class":121}," --all",[104,374,375],{"class":121}," --oneline",[104,377,378],{"class":121}," --",[104,380,381],{"class":125}," .cursorrules\n",[10,383,384,385,388],{},"If you see an actual key value (not a placeholder like ",[23,386,387],{},"your-key-here","), the key is in git history.",[86,390,392],{"number":391},"4",[10,393,394,397,398,401,402,405,406,409,410,413],{},[49,395,396],{},"Check your deployed JS bundle."," Open your deployed app in a browser, press F12, go to Sources, and search for ",[23,399,400],{},"sk-proj-",", ",[23,403,404],{},"sk_live_",", or ",[23,407,408],{},"AKIA",". If a key appears in a ",[23,411,412],{},".js"," file, it is in your bundle.",[86,415,417,422,458],{"number":416},"5",[10,418,419],{},[49,420,421],{},"Check git history for committed .env files.",[95,423,425],{"className":97,"code":424,"language":99,"meta":100,"style":100},"git log --all --full-history -- .env\ngit log --all --full-history -- .env.local\n",[23,426,427,443],{"__ignoreMap":100},[104,428,429,431,433,435,438,440],{"class":106,"line":107},[104,430,366],{"class":117},[104,432,369],{"class":125},[104,434,372],{"class":121},[104,436,437],{"class":121}," --full-history",[104,439,378],{"class":121},[104,441,442],{"class":125}," .env\n",[104,444,445,447,449,451,453,455],{"class":106,"line":114},[104,446,366],{"class":117},[104,448,369],{"class":125},[104,450,372],{"class":121},[104,452,437],{"class":121},[104,454,378],{"class":121},[104,456,457],{"class":125}," .env.local\n",[10,459,460],{},"Any result means the secret was stored in your repo.",[462,463,464],"tip-box",{},[10,465,466],{},"CheckYourVibe scans your deployed app for API keys in the JavaScript bundle, including OpenAI, Anthropic, Stripe, Supabase, and AWS credentials. It catches what a visitor's browser can read, not just what is in source.",[39,468,470],{"id":469},"step-2-rotate-the-exposed-key-immediately","Step 2: Rotate the Exposed Key Immediately",[10,472,473],{},"Rotate before fixing code. The key is live and usable right now.",[86,475,476],{"number":88},[10,477,478,481],{},[49,479,480],{},"Generate a new key"," in the affected service's dashboard (OpenAI, Stripe, Anthropic, Supabase, etc.).",[86,483,484],{"number":230},[10,485,486,489],{},[49,487,488],{},"Update your deployment environment variables."," In Vercel: Project Settings > Environment Variables. In Netlify: Site Settings > Environment Variables. In Railway: Variables tab.",[86,491,492],{"number":314},[10,493,494,497],{},[49,495,496],{},"Redeploy"," so the new variable takes effect in production.",[86,499,500],{"number":391},[10,501,502,505],{},[49,503,504],{},"Revoke the old key."," Delete or disable it in the service dashboard. Do not leave it active.",[86,507,508],{"number":416},[10,509,510,513],{},[49,511,512],{},"Check usage logs"," for spikes in the exposure window. OpenAI, Stripe, and AWS all provide per-key usage reports.",[515,516,517],"danger-box",{},[10,518,519],{},"If the key was ever in a public GitHub repo, treat it as compromised regardless of exposure time. Automated scanners index public repos within minutes of a push.",[39,521,523],{"id":522},"step-3-remove-from-git-history-if-committed","Step 3: Remove from Git History (if committed)",[95,525,527],{"className":97,"code":526,"language":99,"meta":100,"style":100},"# Install git-filter-repo\npip install git-filter-repo\n\n# Remove sensitive files from all history\ngit filter-repo --path .env --invert-paths\ngit filter-repo --path .cursorrules --invert-paths  # if it contains secrets\n",[23,528,529,534,545,549,554,570],{"__ignoreMap":100},[104,530,531],{"class":106,"line":107},[104,532,533],{"class":110},"# Install git-filter-repo\n",[104,535,536,539,542],{"class":106,"line":114},[104,537,538],{"class":117},"pip",[104,540,541],{"class":125}," install",[104,543,544],{"class":125}," git-filter-repo\n",[104,546,547],{"class":106,"line":135},[104,548,172],{"emptyLinePlaceholder":171},[104,550,551],{"class":106,"line":162},[104,552,553],{"class":110},"# Remove sensitive files from all history\n",[104,555,556,558,561,564,567],{"class":106,"line":168},[104,557,366],{"class":117},[104,559,560],{"class":125}," filter-repo",[104,562,563],{"class":121}," --path",[104,565,566],{"class":125}," .env",[104,568,569],{"class":121}," --invert-paths\n",[104,571,572,574,576,578,580,583],{"class":106,"line":175},[104,573,366],{"class":117},[104,575,560],{"class":125},[104,577,563],{"class":121},[104,579,335],{"class":125},[104,581,582],{"class":121}," --invert-paths",[104,584,585],{"class":110},"  # if it contains secrets\n",[10,587,588,589,592],{},"After scrubbing, force-push to all remotes and notify collaborators to re-clone. Add the files to ",[23,590,591],{},".gitignore",":",[95,594,596],{"className":97,"code":595,"language":99,"meta":100,"style":100},"echo \".env\" >> .gitignore\necho \".env.local\" >> .gitignore\n# Only add .cursorrules if it contains secrets\n# Otherwise keep it for team context\ngit add .gitignore && git commit -m \"chore: gitignore sensitive files\"\n",[23,597,598,612,623,628,633],{"__ignoreMap":100},[104,599,600,603,606,609],{"class":106,"line":107},[104,601,602],{"class":121},"echo",[104,604,605],{"class":125}," \".env\"",[104,607,608],{"class":338}," >>",[104,610,611],{"class":125}," .gitignore\n",[104,613,614,616,619,621],{"class":106,"line":114},[104,615,602],{"class":121},[104,617,618],{"class":125}," \".env.local\"",[104,620,608],{"class":338},[104,622,611],{"class":125},[104,624,625],{"class":106,"line":135},[104,626,627],{"class":110},"# Only add .cursorrules if it contains secrets\n",[104,629,630],{"class":106,"line":162},[104,631,632],{"class":110},"# Otherwise keep it for team context\n",[104,634,635,637,640,643,647,649,652,655],{"class":106,"line":168},[104,636,366],{"class":117},[104,638,639],{"class":125}," add",[104,641,642],{"class":125}," .gitignore",[104,644,646],{"class":645},"sVt8B"," && ",[104,648,366],{"class":117},[104,650,651],{"class":125}," commit",[104,653,654],{"class":121}," -m",[104,656,657],{"class":125}," \"chore: gitignore sensitive files\"\n",[39,659,661],{"id":660},"step-4-fix-the-root-cause","Step 4: Fix the Root Cause",[663,664,666],"h3",{"id":665},"nextjs-apps-remove-next_public_","Next.js Apps (remove NEXT_PUBLIC_)",[10,668,669,670,672],{},"Cursor frequently generates ",[23,671,64],{}," because it has seen that pattern in training data. Remove the prefix and move the API call server-side.",[10,674,675],{},[49,676,677],{},"Wrong (key in browser bundle):",[95,679,683],{"className":680,"code":681,"language":682,"meta":100,"style":100},"language-typescript shiki shiki-themes github-light github-dark","// app/components/Chat.tsx - runs in browser, key visible in bundle\nconst response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n  headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}` },\n});\n","typescript",[23,684,685,690,716,742],{"__ignoreMap":100},[104,686,687],{"class":106,"line":107},[104,688,689],{"class":110},"// app/components/Chat.tsx - runs in browser, key visible in bundle\n",[104,691,692,695,698,701,704,707,710,713],{"class":106,"line":114},[104,693,694],{"class":338},"const",[104,696,697],{"class":121}," response",[104,699,700],{"class":338}," =",[104,702,703],{"class":338}," await",[104,705,706],{"class":117}," fetch",[104,708,709],{"class":645},"(",[104,711,712],{"class":125},"\"https://api.openai.com/v1/chat/completions\"",[104,714,715],{"class":645},", {\n",[104,717,718,721,724,727,729,732,734,736,739],{"class":106,"line":135},[104,719,720],{"class":645},"  headers: { Authorization: ",[104,722,723],{"class":125},"`Bearer ${",[104,725,726],{"class":645},"process",[104,728,76],{"class":125},[104,730,731],{"class":645},"env",[104,733,76],{"class":125},[104,735,64],{"class":121},[104,737,738],{"class":125},"}`",[104,740,741],{"class":645}," },\n",[104,743,744],{"class":106,"line":162},[104,745,746],{"class":645},"});\n",[10,748,749],{},[49,750,751],{},"Right (Route Handler keeps key server-side):",[95,753,755],{"className":680,"code":754,"language":682,"meta":100,"style":100},"// app/api/chat/route.ts - runs on server only\nimport { NextRequest, NextResponse } from \"next/server\";\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport async function POST(req: NextRequest) {\n  const { message } = await req.json();\n  const response = await client.chat.completions.create({\n    model: \"gpt-4o\",\n    messages: [{ role: \"user\", content: message }],\n  });\n  return NextResponse.json({ reply: response.choices[0].message.content });\n}\n",[23,756,757,762,779,793,797,821,825,853,881,900,912,924,930,950],{"__ignoreMap":100},[104,758,759],{"class":106,"line":107},[104,760,761],{"class":110},"// app/api/chat/route.ts - runs on server only\n",[104,763,764,767,770,773,776],{"class":106,"line":114},[104,765,766],{"class":338},"import",[104,768,769],{"class":645}," { NextRequest, NextResponse } ",[104,771,772],{"class":338},"from",[104,774,775],{"class":125}," \"next/server\"",[104,777,778],{"class":645},";\n",[104,780,781,783,786,788,791],{"class":106,"line":135},[104,782,766],{"class":338},[104,784,785],{"class":645}," OpenAI ",[104,787,772],{"class":338},[104,789,790],{"class":125}," \"openai\"",[104,792,778],{"class":645},[104,794,795],{"class":106,"line":162},[104,796,172],{"emptyLinePlaceholder":171},[104,798,799,801,804,806,809,812,815,818],{"class":106,"line":168},[104,800,694],{"class":338},[104,802,803],{"class":121}," client",[104,805,700],{"class":338},[104,807,808],{"class":338}," new",[104,810,811],{"class":117}," OpenAI",[104,813,814],{"class":645},"({ apiKey: process.env.",[104,816,817],{"class":121},"OPENAI_API_KEY",[104,819,820],{"class":645}," });\n",[104,822,823],{"class":106,"line":175},[104,824,172],{"emptyLinePlaceholder":171},[104,826,827,830,833,836,839,841,845,847,850],{"class":106,"line":181},[104,828,829],{"class":338},"export",[104,831,832],{"class":338}," async",[104,834,835],{"class":338}," function",[104,837,838],{"class":117}," POST",[104,840,709],{"class":645},[104,842,844],{"class":843},"s4XuR","req",[104,846,592],{"class":338},[104,848,849],{"class":117}," NextRequest",[104,851,852],{"class":645},") {\n",[104,854,855,858,861,864,867,870,872,875,878],{"class":106,"line":201},[104,856,857],{"class":338},"  const",[104,859,860],{"class":645}," { ",[104,862,863],{"class":121},"message",[104,865,866],{"class":645}," } ",[104,868,869],{"class":338},"=",[104,871,703],{"class":338},[104,873,874],{"class":645}," req.",[104,876,877],{"class":117},"json",[104,879,880],{"class":645},"();\n",[104,882,883,885,887,889,891,894,897],{"class":106,"line":218},[104,884,857],{"class":338},[104,886,697],{"class":121},[104,888,700],{"class":338},[104,890,703],{"class":338},[104,892,893],{"class":645}," client.chat.completions.",[104,895,896],{"class":117},"create",[104,898,899],{"class":645},"({\n",[104,901,903,906,909],{"class":106,"line":902},10,[104,904,905],{"class":645},"    model: ",[104,907,908],{"class":125},"\"gpt-4o\"",[104,910,911],{"class":645},",\n",[104,913,915,918,921],{"class":106,"line":914},11,[104,916,917],{"class":645},"    messages: [{ role: ",[104,919,920],{"class":125},"\"user\"",[104,922,923],{"class":645},", content: message }],\n",[104,925,927],{"class":106,"line":926},12,[104,928,929],{"class":645},"  });\n",[104,931,933,936,939,941,944,947],{"class":106,"line":932},13,[104,934,935],{"class":338},"  return",[104,937,938],{"class":645}," NextResponse.",[104,940,877],{"class":117},[104,942,943],{"class":645},"({ reply: response.choices[",[104,945,946],{"class":121},"0",[104,948,949],{"class":645},"].message.content });\n",[104,951,953],{"class":106,"line":952},14,[104,954,955],{"class":645},"}\n",[10,957,958,959,962],{},"Your React component calls ",[23,960,961],{},"/api/chat",". Your Route Handler calls OpenAI. The key stays on the server.",[663,964,966],{"id":965},"vite-react-apps-remove-vite_","Vite / React Apps (remove VITE_)",[10,968,969],{},"Same pattern, different platform:",[10,971,972],{},[49,973,974],{},"Wrong (key in bundle):",[95,976,978],{"className":680,"code":977,"language":682,"meta":100,"style":100},"// src/components/Chat.tsx\nconst client = new OpenAI({\n  apiKey: import.meta.env.VITE_OPENAI_API_KEY,\n  dangerouslyAllowBrowser: true,\n});\n",[23,979,980,985,999,1018,1028],{"__ignoreMap":100},[104,981,982],{"class":106,"line":107},[104,983,984],{"class":110},"// src/components/Chat.tsx\n",[104,986,987,989,991,993,995,997],{"class":106,"line":114},[104,988,694],{"class":338},[104,990,803],{"class":121},[104,992,700],{"class":338},[104,994,808],{"class":338},[104,996,811],{"class":117},[104,998,899],{"class":645},[104,1000,1001,1004,1006,1008,1011,1014,1016],{"class":106,"line":135},[104,1002,1003],{"class":645},"  apiKey: ",[104,1005,766],{"class":338},[104,1007,76],{"class":645},[104,1009,1010],{"class":121},"meta",[104,1012,1013],{"class":645},".env.",[104,1015,67],{"class":121},[104,1017,911],{"class":645},[104,1019,1020,1023,1026],{"class":106,"line":162},[104,1021,1022],{"class":645},"  dangerouslyAllowBrowser: ",[104,1024,1025],{"class":121},"true",[104,1027,911],{"class":645},[104,1029,1030],{"class":106,"line":168},[104,1031,746],{"class":645},[10,1033,1034],{},[49,1035,1036],{},"Right (Netlify Function or Express route):",[95,1038,1040],{"className":680,"code":1039,"language":682,"meta":100,"style":100},"// netlify/functions/chat.ts\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport const handler = async (event: any) => {\n  const { message } = JSON.parse(event.body || \"{}\");\n  const response = await client.chat.completions.create({\n    model: \"gpt-4o\",\n    messages: [{ role: \"user\", content: message }],\n  });\n  return { statusCode: 200, body: JSON.stringify({ reply: response.choices[0].message.content }) };\n};\n",[23,1041,1042,1047,1059,1063,1081,1085,1119,1151,1167,1175,1183,1187,1215],{"__ignoreMap":100},[104,1043,1044],{"class":106,"line":107},[104,1045,1046],{"class":110},"// netlify/functions/chat.ts\n",[104,1048,1049,1051,1053,1055,1057],{"class":106,"line":114},[104,1050,766],{"class":338},[104,1052,785],{"class":645},[104,1054,772],{"class":338},[104,1056,790],{"class":125},[104,1058,778],{"class":645},[104,1060,1061],{"class":106,"line":135},[104,1062,172],{"emptyLinePlaceholder":171},[104,1064,1065,1067,1069,1071,1073,1075,1077,1079],{"class":106,"line":162},[104,1066,694],{"class":338},[104,1068,803],{"class":121},[104,1070,700],{"class":338},[104,1072,808],{"class":338},[104,1074,811],{"class":117},[104,1076,814],{"class":645},[104,1078,817],{"class":121},[104,1080,820],{"class":645},[104,1082,1083],{"class":106,"line":168},[104,1084,172],{"emptyLinePlaceholder":171},[104,1086,1087,1089,1092,1095,1097,1099,1102,1105,1107,1110,1113,1116],{"class":106,"line":175},[104,1088,829],{"class":338},[104,1090,1091],{"class":338}," const",[104,1093,1094],{"class":117}," handler",[104,1096,700],{"class":338},[104,1098,832],{"class":338},[104,1100,1101],{"class":645}," (",[104,1103,1104],{"class":843},"event",[104,1106,592],{"class":338},[104,1108,1109],{"class":121}," any",[104,1111,1112],{"class":645},") ",[104,1114,1115],{"class":338},"=>",[104,1117,1118],{"class":645}," {\n",[104,1120,1121,1123,1125,1127,1129,1131,1134,1136,1139,1142,1145,1148],{"class":106,"line":181},[104,1122,857],{"class":338},[104,1124,860],{"class":645},[104,1126,863],{"class":121},[104,1128,866],{"class":645},[104,1130,869],{"class":338},[104,1132,1133],{"class":121}," JSON",[104,1135,76],{"class":645},[104,1137,1138],{"class":117},"parse",[104,1140,1141],{"class":645},"(event.body ",[104,1143,1144],{"class":338},"||",[104,1146,1147],{"class":125}," \"{}\"",[104,1149,1150],{"class":645},");\n",[104,1152,1153,1155,1157,1159,1161,1163,1165],{"class":106,"line":201},[104,1154,857],{"class":338},[104,1156,697],{"class":121},[104,1158,700],{"class":338},[104,1160,703],{"class":338},[104,1162,893],{"class":645},[104,1164,896],{"class":117},[104,1166,899],{"class":645},[104,1168,1169,1171,1173],{"class":106,"line":218},[104,1170,905],{"class":645},[104,1172,908],{"class":125},[104,1174,911],{"class":645},[104,1176,1177,1179,1181],{"class":106,"line":902},[104,1178,917],{"class":645},[104,1180,920],{"class":125},[104,1182,923],{"class":645},[104,1184,1185],{"class":106,"line":914},[104,1186,929],{"class":645},[104,1188,1189,1191,1194,1197,1200,1203,1205,1208,1210,1212],{"class":106,"line":926},[104,1190,935],{"class":338},[104,1192,1193],{"class":645}," { statusCode: ",[104,1195,1196],{"class":121},"200",[104,1198,1199],{"class":645},", body: ",[104,1201,1202],{"class":121},"JSON",[104,1204,76],{"class":645},[104,1206,1207],{"class":117},"stringify",[104,1209,943],{"class":645},[104,1211,946],{"class":121},[104,1213,1214],{"class":645},"].message.content }) };\n",[104,1216,1217],{"class":106,"line":932},[104,1218,1219],{"class":645},"};\n",[663,1221,1223],{"id":1222},"environment-variable-naming","Environment Variable Naming",[1225,1226,1227,1240],"table",{},[1228,1229,1230],"thead",{},[1231,1232,1233,1237],"tr",{},[1234,1235,1236],"th",{},"Wrong (client-exposed)",[1234,1238,1239],{},"Right (server-only)",[1241,1242,1243,1254,1266,1278],"tbody",{},[1231,1244,1245,1250],{},[1246,1247,1248],"td",{},[23,1249,64],{},[1246,1251,1252],{},[23,1253,817],{},[1231,1255,1256,1261],{},[1246,1257,1258],{},[23,1259,1260],{},"NEXT_PUBLIC_STRIPE_SECRET_KEY",[1246,1262,1263],{},[23,1264,1265],{},"STRIPE_SECRET_KEY",[1231,1267,1268,1273],{},[1246,1269,1270],{},[23,1271,1272],{},"VITE_ANTHROPIC_API_KEY",[1246,1274,1275],{},[23,1276,1277],{},"ANTHROPIC_API_KEY",[1231,1279,1280,1284],{},[1246,1281,1282],{},[23,1283,310],{},[1246,1285,1286],{},[23,1287,1265],{},[39,1289,1291],{"id":1290},"step-5-fix-your-cursorrules-file","Step 5: Fix Your .cursorrules File",[10,1293,1294,1295,1297],{},"If your ",[23,1296,36],{}," file contains real API keys, clean it and add a rule to prevent Cursor from generating the pattern again:",[95,1299,1304],{"className":1300,"code":1302,"language":1303},[1301],"language-text","# .cursorrules\n# Rules for this project\n\n## API Keys and Secrets\n- Never hardcode API keys, secrets, or passwords in source files.\n- Always use environment variables accessed via process.env on the server.\n- Client components must never call third-party APIs directly. Use Route Handlers or server actions.\n- NEXT_PUBLIC_ prefix is for non-secret config only (feature flags, public URLs). Never for API keys.\n- Variable naming: OPENAI_API_KEY (not NEXT_PUBLIC_OPENAI_API_KEY), STRIPE_SECRET_KEY (not NEXT_PUBLIC_STRIPE_SECRET_KEY).\n","text",[23,1305,1302],{"__ignoreMap":100},[10,1307,1308],{},"This gives Cursor explicit guidance so it generates correct patterns in future sessions.",[39,1310,1312],{"id":1311},"step-6-prevent-future-leaks","Step 6: Prevent Future Leaks",[663,1314,1316],{"id":1315},"pre-commit-hook-with-gitleaks","Pre-commit Hook with Gitleaks",[95,1318,1320],{"className":97,"code":1319,"language":99,"meta":100,"style":100},"# Install gitleaks (macOS)\nbrew install gitleaks\n\n# Test your current repo\ngitleaks detect --source . --verbose\n\n# Add pre-commit hook\ncat > .git/hooks/pre-commit \u003C\u003C 'EOF'\n#!/bin/sh\ngitleaks protect --staged --verbose\nif [ $? -ne 0 ]; then\n  echo \"Gitleaks found potential secrets. Commit blocked.\"\n  exit 1\nfi\nEOF\nchmod +x .git/hooks/pre-commit\n",[23,1321,1322,1327,1337,1341,1346,1362,1366,1371,1387,1392,1397,1402,1407,1412,1417,1423],{"__ignoreMap":100},[104,1323,1324],{"class":106,"line":107},[104,1325,1326],{"class":110},"# Install gitleaks (macOS)\n",[104,1328,1329,1332,1334],{"class":106,"line":114},[104,1330,1331],{"class":117},"brew",[104,1333,541],{"class":125},[104,1335,1336],{"class":125}," gitleaks\n",[104,1338,1339],{"class":106,"line":135},[104,1340,172],{"emptyLinePlaceholder":171},[104,1342,1343],{"class":106,"line":162},[104,1344,1345],{"class":110},"# Test your current repo\n",[104,1347,1348,1351,1354,1357,1359],{"class":106,"line":168},[104,1349,1350],{"class":117},"gitleaks",[104,1352,1353],{"class":125}," detect",[104,1355,1356],{"class":121}," --source",[104,1358,129],{"class":125},[104,1360,1361],{"class":121}," --verbose\n",[104,1363,1364],{"class":106,"line":175},[104,1365,172],{"emptyLinePlaceholder":171},[104,1367,1368],{"class":106,"line":181},[104,1369,1370],{"class":110},"# Add pre-commit hook\n",[104,1372,1373,1375,1378,1381,1384],{"class":106,"line":201},[104,1374,332],{"class":117},[104,1376,1377],{"class":338}," >",[104,1379,1380],{"class":125}," .git/hooks/pre-commit",[104,1382,1383],{"class":338}," \u003C\u003C",[104,1385,1386],{"class":125}," 'EOF'\n",[104,1388,1389],{"class":106,"line":218},[104,1390,1391],{"class":125},"#!/bin/sh\n",[104,1393,1394],{"class":106,"line":902},[104,1395,1396],{"class":125},"gitleaks protect --staged --verbose\n",[104,1398,1399],{"class":106,"line":914},[104,1400,1401],{"class":125},"if [ $? -ne 0 ]; then\n",[104,1403,1404],{"class":106,"line":926},[104,1405,1406],{"class":125},"  echo \"Gitleaks found potential secrets. Commit blocked.\"\n",[104,1408,1409],{"class":106,"line":932},[104,1410,1411],{"class":125},"  exit 1\n",[104,1413,1414],{"class":106,"line":952},[104,1415,1416],{"class":125},"fi\n",[104,1418,1420],{"class":106,"line":1419},15,[104,1421,1422],{"class":125},"EOF\n",[104,1424,1426,1429,1432],{"class":106,"line":1425},16,[104,1427,1428],{"class":117},"chmod",[104,1430,1431],{"class":125}," +x",[104,1433,1434],{"class":125}," .git/hooks/pre-commit\n",[663,1436,1438],{"id":1437},"add-a-cursorignore","Add a .cursorignore",[10,1440,1441,1442,401,1444,1447],{},"Like ",[23,1443,591],{},[23,1445,1446],{},".cursorignore"," tells Cursor not to read certain files as context. Use it to keep secrets out of Cursor's context window entirely:",[95,1449,1452],{"className":1450,"code":1451,"language":1303},[1301],"# .cursorignore\n.env\n.env.local\n.env.production\n.env.*.local\n*.pem\n*.key\nsecrets/\n",[23,1453,1451],{"__ignoreMap":100},[10,1455,1456,1457,1459,1460,1462],{},"When ",[23,1458,33],{}," is in ",[23,1461,1446],{},", Cursor cannot read it and cannot echo your keys into generated code.",[462,1464,1465],{},[10,1466,1467,1468,1470],{},"After adding ",[23,1469,1446],{},", restart Cursor so the index rebuilds without the excluded files.",[1472,1473,1474,1481,1487,1493,1499],"faq-section",{},[1475,1476,1478],"faq-item",{"question":1477},"Why does Cursor generate code with hardcoded API keys?",[10,1479,1480],{},"Cursor's AI generates code based on patterns it has seen and on context from your current codebase. If your codebase contains example keys, or if you pasted a key into a prompt or rules file, the model may echo it back in suggestions. Cursor does not intentionally insert keys, but it can reproduce ones it sees in context. Always scan generated code before committing.",[1475,1482,1484],{"question":1483},"What is a .cursorrules file and can it expose secrets?",[10,1485,1486],{},"A .cursorrules file tells Cursor's AI how to write code for your project. If you paste an API key into it so the AI can reference your exact service, and you commit the file, that key is now in git history. Add .cursorrules to .gitignore if it contains any credentials, or strip the keys and reference them by name only.",[1475,1488,1490],{"question":1489},"Why does NEXT_PUBLIC_OPENAI_API_KEY expose my key?",[10,1491,1492],{},"Next.js bundles any variable prefixed with NEXT_PUBLIC_ into the client-side JavaScript at build time. The prefix is designed to share values with browser code, not to hide them. If Cursor creates a NEXT_PUBLIC_OPENAI_API_KEY variable, that key is in your compiled JS files, readable by any visitor. Move the call to a Route Handler and use a variable without the NEXT_PUBLIC_ prefix.",[1475,1494,1496],{"question":1495},"Do I need to rotate my key if I only exposed it briefly?",[10,1497,1498],{},"Yes. Automated scanners index public GitHub repos within minutes of a push. If the key was in a public repo at any point, assume it was found. Rotate it even if you deleted the commit immediately.",[1475,1500,1502],{"question":1501},"How do I tell Cursor not to hardcode API keys in future?",[10,1503,1504],{},"Add a rule to your .cursorrules file: \"Never hardcode API keys, passwords, or secrets in source code. Always use environment variables and access them via process.env on the server side only.\" You can also use Cursor's @Rules feature to set project-wide guidelines.",[1506,1507,1508,1514,1519],"related-articles",{},[1509,1510],"related-card",{"description":1511,"href":1512,"title":1513},"Security analysis of Cursor AI editor: data privacy, code quality risks, and what to check before using it for production work.","/blog/is-safe/cursor","Is Cursor Safe?",[1509,1515],{"description":1516,"href":1517,"title":1518},"Emergency guide for rotating compromised API keys without downtime. Step-by-step for OpenAI, Stripe, Supabase, and others.","/blog/how-to/rotate-api-keys","How to Rotate API Keys",[1509,1520],{"description":1521,"href":1522,"title":1523},"Clean secrets from your git history after accidental commits using git filter-repo and BFG Repo Cleaner.","/blog/how-to/remove-secrets-git-history","How to Remove Secrets from Git History",[1525,1526,1529],"cta-box",{"href":1527,"label":1528},"/","Scan Your Cursor App",[10,1530,1531],{},"Check your Cursor-built deployment for API keys in your JavaScript bundle, exposed environment variables, and security header gaps. Free scan, no signup required.",[1533,1534,1535],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":100,"searchDepth":114,"depth":114,"links":1537},[1538,1539,1540,1541,1542,1547,1548],{"id":41,"depth":114,"text":42},{"id":83,"depth":114,"text":84},{"id":469,"depth":114,"text":470},{"id":522,"depth":114,"text":523},{"id":660,"depth":114,"text":661,"children":1543},[1544,1545,1546],{"id":665,"depth":135,"text":666},{"id":965,"depth":135,"text":966},{"id":1222,"depth":135,"text":1223},{"id":1290,"depth":114,"text":1291},{"id":1311,"depth":114,"text":1312,"children":1549},[1550,1551],{"id":1315,"depth":135,"text":1316},{"id":1437,"depth":135,"text":1438},"how-to","2026-05-25","Cursor AI-generated code often exposes API keys in three ways: hardcoded in source files, sent to the client via NEXT_PUBLIC_ or VITE_ prefixes, or committed to git in .env files. Step-by-step fix.",false,"md",[1558,1559,1561,1562,1563],{"question":1477,"answer":1480},{"question":1483,"answer":1560},"A .cursorrules file (or .cursor/rules/) tells Cursor's AI how to write code for your project. If you paste an API key into it so the AI can reference your exact service, and you commit the file, that key is now in git history. Add .cursorrules to .gitignore if it contains any credentials, or strip the keys and reference them by name only.",{"question":1489,"answer":1492},{"question":1495,"answer":1498},{"question":1501,"answer":1564},"Add a rule to your .cursorrules file: 'Never hardcode API keys, passwords, or secrets in source code. Always use environment variables and access them via process.env on the server side only.' You can also use Cursor's @Rules feature to set project-wide guidelines.","yellow",null,"cursor api key exposure, fix cursor api key, cursor generated code secrets, cursor ai key leaked, nextjs api key exposed, vite api key exposed",{},"Cursor API key exposure fix: audit your AI-generated code for hardcoded secrets, NEXT_PUBLIC_ and VITE_ leaks, and committed .env files. Rotate and move server-side.","/blog/how-to/fix-cursor-api-key-exposure","8 min read","[object Object]","HowTo",{"title":5,"description":1554},{"loc":1570},"blog/how-to/fix-cursor-api-key-exposure",[],"summary_large_image","XgyN0uy6AN_dInqRoZU2Ti7tRqnY1u9ltCPZZYB9SO0",1779752890345]