HomeGuidesCloudflare Pages and Workers secrets without leaking .env
Guide
Cloudflare Pages and Workers secrets without leaking .env
Wrangler vars vs secrets, why VITE_ and NEXT_PUBLIC_ values reach the browser even when encrypted, and how a .env ends up live on Cloudflare Pages.
Independent notes from VibeLocked. Not a pentest, and not affiliated with the tools named below.
On October 15, 2026, a developer posted that they had finally found how their Google key leaked and produced a $90,000 bill: one AI coding agent said another had published their .env to Cloudflare Pages (the post). ThePrimeagen quoted it the next day with a one line verdict, "You published your .env to cloudflare," and that reply passed 287,000 views (his post). It is one person's account, not an audit. The mechanics behind it are real and documented, and they are easy to repeat when an agent runs your deploy.
This guide is Cloudflare specific: where each kind of value lives (.dev.vars, [vars], encrypted secrets, build variables), the four ways one of them goes public, how to check your own project, and the cleanup that a redeploy alone does not do. For the general file check on any host (Vercel, Netlify, a VPS) plus .git folders and source maps, see is your .env, .git, or source code public?
Checked on October 23, 2026 against Cloudflare's Workers and Pages docs, the Wrangler source, and the Vite, Next.js, and Astro docs, all linked below. VibeLocked is independent and not affiliated with Cloudflare or any tool named here.
The short version
- Deploy the build folder, never the project root.
wrangler pages deploy dist, notwrangler pages deploy .. For Workers static assets,assets.directoryshould be./distor similar, never./. - Secrets go in
wrangler secret put(Workers) orwrangler pages secret put(Pages), or the dashboard with the Secret type or Encrypt. Never in[vars]in a committedwrangler.tomlorwrangler.jsonc. - Encryption does not hide a
VITE_,NEXT_PUBLIC_, orPUBLIC_value. The build reads it and writes it into the JavaScript every visitor downloads. - Keep
.dev.vars*and.env*in.gitignore. They sit next to your Wrangler config, usually the project root. - If something leaked: rotate first, then redeploy the right folder, then delete old Pages deployments and check Workers Version URLs, because old URLs can keep serving the file.
Where values live on Cloudflare
| Where | Encrypted? | Who can read it | Use it for |
|---|---|---|---|
.dev.vars or .env next to your Wrangler config |
No, it is a local file | Anyone with the file, and anyone who can load it if you deploy or commit it | Local development only |
[vars] in wrangler.toml or wrangler.jsonc |
No, plain text | Anyone who can read your repo, and anyone with dashboard access | Non-secret config such as an API host |
Workers: npx wrangler secret put <KEY>, or dashboard type Secret |
Yes, hidden after saving | Your Worker at runtime, on env |
API keys, database URLs, signing secrets |
Pages: npx wrangler pages secret put <KEY>, or dashboard Encrypt |
Yes, hidden after saving | Your build and your Pages Functions | Same, for Pages projects |
| Workers Builds: Build variables and secrets | Depends on the type you pick | Only the build | Tokens the build needs, such as a private package registry |
What Cloudflare's docs say about each:
- Secrets and vars are the same thing to your code. Cloudflare's secrets page says "to your Worker, there is no difference between an environment variable and a secret." The difference is that secret values "are not visible within Wrangler or Cloudflare dashboard after you define them." The environment variables page says text and JSON vars "are not encrypted," and both pages say: do not use
varsto store sensitive information, use secrets instead. wrangler secret putdeploys right away. It creates a new version of the Worker and deploys it immediately. With gradual deployments, usewrangler versions secret putinstead.- Pages secrets must exist before the deploy that uses them. The Pages bindings page adds secrets under Settings, Variables and Secrets, with Encrypt, and notes this "needs to be done before a deployment that uses those secrets."
- Pages builds can read secrets. Cloudflare's Pages API reference lists a project's variables as
plain_textorsecret_textand describes both as "environment variables used for builds and Pages Functions." The build configuration page covers variables your build uses. - Workers Builds keeps build values apart. Its configuration page describes Build variables and secrets as "accessible only to your build" and says they "will not be accessible at runtime." Runtime values go under Settings, Variables and Secrets.
- A deploy overwrites dashboard vars, not secrets. The Wrangler configuration reference says Wrangler overrides variables you changed in the dashboard on the next deploy unless you set
keep_vars = true, and that it will not delete your secrets unless you runwrangler secret delete.
Four ways a value goes public
1. The upload includes the file
Cloudflare publishes the folder you point it at. The Direct Upload guide says to run npx wrangler pages deploy <BUILD_OUTPUT_DIRECTORY>. Wrangler's Pages uploader skips only _worker.js, _redirects, _headers, _routes.json, functions, .DS_Store, node_modules, .git, and .wrangler. A .env, .env.local, or .dev.vars is not on that list.
Cloudflare tells you to keep local secrets in .dev.vars or .env "in the same directory as the Wrangler configuration file," which is usually the project root. So when an agent "fixes" a failed deploy by running wrangler pages deploy ., your local secrets file goes up with the site. The same happens on Workers if the static assets directory is set to ./ instead of your build folder.
2. The file is in public/
Frameworks copy public/ into the build untouched: Vite, Next.js (served from the site root), and Astro ("copied into the build folder untouched"). A .env or a config.json with keys that an AI tool dropped into public/ ends up in dist/ and goes live even when you deploy the right folder.
3. [vars] in a committed Wrangler config
wrangler.toml and wrangler.jsonc are meant to be committed, and Pages treats the file as the source of truth once you use one. An API key written under [vars] is plain text in your git history, on GitHub if the repo is public, and in the dashboard for anyone on your account. Cloudflare's own example shows API_KEY under [vars] only to explain the format; its docs say secrets do not belong there.
4. A public prefix puts it in the bundle
This one has nothing to do with Cloudflare, and encrypting the variable does not help:
- Vite: "
VITE_*variables should not contain sensitive information such as API keys. The values of these variables are bundled into your source code at build time." - Next.js: a
NEXT_PUBLIC_value is inlined at build time and "will be inlined into any JavaScript sent to the browser." - Astro: only
PUBLIC_variables are available in client code, values are "statically replaced at build time," and itsastro:envschema refuses a secret client variable "because there is no safe way to send this data to the client."
So VITE_OPENAI_API_KEY stored as an encrypted Pages secret is still readable by anyone who opens your JavaScript. The fix is to move the call to a server: a Pages Function or Worker that reads an unprefixed secret from env. Keep secret API keys out of frontend code shows how per framework.
Check your own project
Run these against your own project and domain only.
1. The live site. Request the files that should never be there. This prints only the HTTP status for each path:
SITE=https://yourapp.pages.dev
for p in /.env /.env.local /.env.production /.dev.vars /wrangler.toml /wrangler.jsonc /package.json; do
printf "%s %s\n" "$(curl -s -o /dev/null -w '%{http_code}' "$SITE$p")" "$p"
done
A 404 on every line is good. Some single page apps answer every path with the home page and a 200, so if you get a 200, open that URL: a page of HTML is harmless, KEY=value lines are a leak. Check your custom domain and your *.pages.dev or *.workers.dev address.
2. The build output, before you deploy. These list file names only:
npm run build
find dist -name '.env*' -o -name '.dev.vars*' -o -name 'wrangler.*'
grep -rlE 'sk_live_|sk-|service_role|AIza' dist
Use your real output folder (dist, out, or build). The grep -l prints only the files that match, not the values. Any hit in a JavaScript file means a secret was bundled.
3. Git. List tracked secret files and confirm the ignore rules work:
git ls-files | grep -E '(^|/)(\.env|\.dev\.vars)'
git check-ignore -v .env .dev.vars
The first command should print nothing. The second should name the .gitignore line that ignores each file.
4. The Wrangler config. Open wrangler.toml or wrangler.jsonc and read the names under vars. Anything that is a key, token, password, or connection string has to move.
5. What Cloudflare holds. For Pages, npx wrangler pages secret list shows secret names. In the dashboard, open your Worker or Pages project, Settings, Variables and Secrets, and check that every credential shows as a Secret or encrypted, not as plain text.
Fix it
- Point every deploy at the build output. In scripts and CI,
wrangler pages deploy dist(or your folder). For Pages with a Wrangler file, setpages_build_output_dir = "./dist". For Workers, setassets.directoryto the build folder. - Add
.assetsignoreas a backstop for Workers assets. Cloudflare's static assets page says the file goes "in the root of the assets directory," takes the same format as.gitignore, and Wrangler will not upload files that match. It only covers Workers assets, notwrangler pages deploy:
# .assetsignore, in the root of your Workers assets directory
.env*
.dev.vars*
wrangler.*
- Move credentials out of
[vars]. For each one, runnpx wrangler secret put <KEY>(Workers) ornpx wrangler pages secret put <KEY>(Pages) and type the new value at the prompt, then delete the line from the config file. Rotate the value first if the file was ever committed or public. - Declare required secrets. The Wrangler
secretsproperty lists the names your Worker needs, andwrangler deployfails with a clear error if one is missing, which stops "it broke, so the agent pasted the key into vars." - Keep local files local. Add
.dev.vars*and.env*to.gitignore, as Cloudflare recommends. - Remove the public prefix from anything secret, and call the provider from a Pages Function or Worker instead.
If something was already exposed
- Rotate first. Every value in an exposed
.env,.dev.vars, committed[vars], or bundledVITE_variable is leaked. Revoke it at the provider and issue a new one, following the first hour after an API key leaks. If a whole.envleaked, what to rotate first after the Railway breach gives an order for a full set of variables: database, payment, AI provider, OAuth and signing secrets. Then put the new value in a Cloudflare secret. - Redeploy the right folder, and run the live check again on every domain.
- Delete old Pages deployments. Cloudflare's preview deployments page says each deployment gets a hash based address such as
<hash>.<project>.pages.devthat "may always be visited in the future," and that preview deployments are public by default. A redeploy does not remove the file from those URLs. List deployments withnpx wrangler pages deployment list, then remove each one that contained the file withnpx wrangler pages deployment delete <DEPLOYMENT_ID> --project-name <PROJECT_NAME>. Use--forcefor aliased deployments. The latest deployment for a branch cannot be deleted, so deploy a clean build to that branch first. Cloudflare's Pages API page shows a script for deleting old deployments in bulk. - Check Workers Version URLs. Cloudflare's Version URLs page says they are public when enabled, and on by default when
workers_devis enabled. If an older version uploaded the file, disable Version URLs in Settings, Domains and Routes, or setpreview_urls = falsein your Wrangler config, or put them behind Cloudflare Access. - Clean git if it was committed. See remove an API key from GitHub history, after the key is already dead.
- Review the other exposure paths in is your .env, .git, or source code public?, since a project that leaked one file often serves source maps or a
.gitfolder too.
Common AI-generated mistakes
wrangler pages deploy .to get past a "directory not found" error.assets.directory = "./"in a Worker that serves a single page app.- API keys under
[vars]because the agent could not find where secrets go. VITE_added to a server key so the frontend "can read it," then marked as encrypted in the dashboard.- A
.envcopied intopublic/so a client script can fetch config. - Deleting the file and redeploying, with no rotation and the old deployment URLs still live.
- One key shared by preview and production, so a leak from a preview URL hits production.
Checklist
- Every deploy command and CI step points at the build output folder, not
. - Workers
assets.directoryis the build folder, with a.assetsignorebackstop -
/.env,/.env.local,/.dev.vars, and/wrangler.tomlreturn 404 on every domain, including*.pages.devand*.workers.dev - No
.env*,.dev.vars*, or Wrangler config files indist/ - No secret key prefixes in the built JavaScript
-
.dev.vars*and.env*are in.gitignoreand not tracked - No credentials under
varsinwrangler.tomlorwrangler.jsonc - Credentials set with
wrangler secret put,wrangler pages secret put, or the dashboard Secret type or Encrypt - No secret behind a
VITE_,NEXT_PUBLIC_, orPUBLIC_name - Old Pages deployments that held a leaked file deleted, and Workers Version URLs reviewed
- Every value that was ever exposed rotated at the provider
Copy-paste prompt for your AI coding tool
Paste this into Cursor, Claude Code, Codex, Lovable, or Bolt inside your own project. It asks for names and paths only. Never paste secret values into the chat, and do not let the tool print them.
This project deploys to Cloudflare Pages or Workers. Do not print, log, or ask me for any secret values. Show variable names and file paths only. 1) Find every deploy command (package.json scripts, CI workflows, shell scripts) and tell me which folder it uploads, flagging any wrangler pages deploy . or assets.directory pointing at the project root. 2) Check whether any .env, .env., .dev.vars, or Wrangler config file can end up in the build output, including through public/. 3) List the names under vars in wrangler.toml or wrangler.jsonc and say which look like credentials that should be Cloudflare secrets instead. 4) List every environment variable name with a VITE_, NEXT_PUBLIC_, or PUBLIC_ prefix and say whether it looks like a secret. 5) Check that .gitignore covers .dev.vars and .env*. 6) Propose a fix plan: server side Pages Functions or Worker routes for any secret the frontend uses, secrets set with wrangler secret put, a .assetsignore for Workers assets, and a secrets property listing required names. Do not change any files until I approve the plan.
Set the new secret values yourself with wrangler secret put or in the dashboard. More prompts are in the security review prompt guide.
Check what your live app exposes with LeakCheck
LeakCheck scans any live web app or public GitHub repo, read only, wherever it is hosted. On a live app it requests exposed file paths such as /.env and looks for secret key patterns in the JavaScript your site ships to visitors. On a public repo it checks the files for leaked keys.
Its limits, honestly: LeakCheck cannot see your Cloudflare dashboard, your encrypted secrets, or old deployment URLs you have not given it, and it cannot tell you whether someone already downloaded a file. It sees what anyone on the internet can see at the URL you scan. Scan your *.pages.dev address as well as your custom domain.
Sources
- ThePrimeagen on X: "You published your .env to cloudflare" (October 16, 2026)
- Just_Lingonberry_352 on X: the $90,000 Google bill post (October 15, 2026)
- Cloudflare Workers: Secrets
- Cloudflare Workers: Environment variables
- Cloudflare Wrangler: Configuration (keep_vars, secrets property)
- Cloudflare Pages: Bindings (environment variables and secrets)
- Cloudflare Pages: Build configuration
- Cloudflare API: Pages (env_vars used for builds and Pages Functions)
- Cloudflare Workers Builds: Configuration (build variables and secrets)
- Cloudflare Pages: Wrangler configuration (source of truth)
- Cloudflare Pages: Direct Upload
- Cloudflare Workers: Static assets configuration (.assetsignore)
- Cloudflare Wrangler: Pages commands (secret put, deployment list and delete)
- Cloudflare Pages: Preview deployments (hash URLs, deleting deployments)
- Cloudflare Pages: Use the API to delete old deployments
- Cloudflare Workers: Version URLs
- Wrangler source: Pages upload ignore list
- Vite: Env variables and modes
- Vite: Static asset handling (public directory)
- Next.js: Environment variables
- Next.js: public folder
- Astro: Environment variables
- Astro: Project structure (public/)
Questions
What is the difference between wrangler vars and wrangler secret put?
Both reach your Worker the same way, on env or process.env. Cloudflare says that to your Worker there is no difference between an environment variable and a secret. The difference is where the value sits: vars are plain text, usually written in wrangler.toml or wrangler.jsonc and committed with your code, and they stay visible in the dashboard. A secret set with wrangler secret put, or with the Secret type in the dashboard, is encrypted and hidden in Wrangler and the dashboard after you save it.
Are Cloudflare Pages secrets available at build time?
Yes for Pages. Cloudflare's API describes a Pages project's variables, plain text and encrypted alike, as environment variables used for builds and Pages Functions. That is why encryption does not protect a VITE_ or NEXT_PUBLIC_ value: the build reads it and writes it into your JavaScript. Workers Builds is different. It has its own Build variables and secrets list that only the build can read, and runtime variables go under Settings, Variables and Secrets.
Does wrangler pages deploy upload my .env file?
It uploads the folder you give it. Wrangler's Pages uploader skips only a short list (node_modules, .git, .DS_Store, .wrangler, functions, and Cloudflare's own _worker.js, _redirects, _headers and _routes.json files). A .env or .dev.vars in that folder is uploaded and served. Run wrangler pages deploy dist, or whatever your build output folder is, never wrangler pages deploy . from the project root.
I removed the .env and redeployed. Is it gone?
Not from older deployments. Cloudflare Pages gives every deployment its own hash URL that stays reachable after newer deploys, and preview deployments are public by default. Delete the old deployments with wrangler pages deployment delete. On Workers, check whether Version URLs are on for older versions. Either way, rotate every value in the file first, because anyone could have downloaded it already.
Is a .dev.vars file safe to keep in my project?
Yes, as long as it stays local. Cloudflare tells you to put local secrets in .dev.vars or .env next to your Wrangler config file and to add .dev.vars* and .env* to .gitignore. The risk is that the file sits in the project root, so committing it, or deploying the project root, publishes it.