17 min read

Why Your .env File Is the Least Secure Thing on Your Mac (And How to Fix It)

Your .env file is probably the least secure thing on your Mac, and git is quietly helping it spread. This post walks through what actually goes wrong in real projects and shows how to switch to a macOS-native secrets manager backed by Keychain, without breaking your workflow.

You probably have a .env file sitting in some project folder on your Mac right now, full of API keys and tokens. From a security point of view, that file is basically a sticky note with your passwords on your laptop lid.

If you care at all about how you store API keys securely on Mac, the fix is straightforward: stop putting secrets in flat files, and start using the thing macOS already gives you for that job. KeyStack is a native macOS secrets manager that sits on top of the system Keychain and gives you a developer-friendly way to manage environment variables on macOS without plain text .env files.

What Actually Goes Wrong With .env Files

The honest threat model on a real MacBook

Ignore the textbook threats for a second. Think about your actual laptop, in your actual life.

On my desk right now: a 14" MacBook Pro with VS Code, Docker, Postgres, a couple of AI tools, and about 40 repos. Before I got more disciplined, every one of those apps had at least one .env sitting in the project folder. Several had .env.local, .env.development, and .env.production too.

Here is what I was actually seeing with clients and my own machines:

  • Developers checking a repo and finding a secrets.bak or .env.old that someone forgot to delete 8 months ago.
  • People AirDropping entire project folders to a second Mac, complete with .env and .env.local, then forgetting they ever did it.
  • Spotlight indexing .env files and happily showing you secrets in the search preview.
  • Time Machine and cloud backup clients quietly archiving every secret you have ever used, for years, onto disks and services you don't control.

None of that feels like "advanced adversary" stuff. It's just what happens when secrets are regular files.

How .env files actually leak into git

The most painful pattern I see with small teams is not a zero-day, it's git gymnastics around .env.

A few real examples:

  • Ottawa SaaS shop with 6 devs, building a custom CRM. They had .env in .gitignore, but one dev new to the team added .env.example by copying .env and then stripping values. They missed two keys. Those went straight to GitHub. Nothing catastrophic happened, but they spent a day rotating keys and checking logs.
  • Single indie dev in Montreal building a Next.js + Supabase app. They did everything "right" for a year, then one day ran git add . from the project root after a big restructure. The .env.production file sitting in an "old" folder went into history. Not current HEAD, but still in the git log that now existed on three machines.

You can get clever with pre-commit hooks and linters that scream when they see patterns like sk_live_ or ghp_. I still recommend those. But you are fighting your own tools: git's job is to track file contents over time. If secrets live in files, eventually git will get one.

Why .gitignore is not a security control

.gitignore is a hint, not a guardrail. It stops git from adding new files with those names. It does nothing for:

  • Files that were tracked in the past and are now ignored.
  • Typos (env.local, .env-local, env.dev) that miss the ignore rule.
  • Archive folders like /old, /backup, /2023 that someone committed months ago with a stray .env inside.

If your strategy for .env file security is "we added it to .gitignore", you are very likely one rushed Friday deploy away from secrets in a remote repo. I watched a 5-person agency in Toronto go through this, and the remediation time cost them more than they billed on the project that week.

macOS Already Has a Secrets Manager, You Just Aren't Using It

Keychain and Secure Enclave in plain language

Every recent Mac has two important pieces of security plumbing: Keychain and the Secure Enclave.

Keychain is a database of secrets that macOS encrypts and manages for you. Passwords, Wi-Fi keys, certificates, tokens, all live there. The Secure Enclave is a hardware-backed bit of silicon that handles the cryptography and Touch ID. Together, they give you something that is very hard to get from a random JSON file on disk:

  • Per-user access control tied to your login session.
  • Hardware-backed key storage, so your secrets are encrypted with keys that never leave the chip.
  • System-level features like auto-locking when the screen locks.

Apple has already built the "secure vault" part of a macOS secrets manager. The missing piece for developers is a usable interface for environment variables and API keys that fits how you actually work with .env files today.

Why I stopped recommending DIY Keychain scripts

For a couple of years, what I told solo devs was: "Just script around security. Keychain has a CLI, write a small wrapper that grabs secrets and writes a .env file at runtime." It works, technically.

Then I watched a Rails shop in Vancouver try to live with that. They had a bash script that:

  1. Read secrets from Keychain.
  2. Generated .env at app start.
  3. Deleted .env after shutdown.

On day one it felt clean. By month three, they had:

  • Four slightly different scripts across four repos.
  • One junior dev who could not get the script to work on their M2 Air because the Keychain item names didn't match.
  • A weird bug where .env stuck around after a failed boot, so they had secrets lingering on disk anyway.

They had moved the secrets source of truth into Keychain, but the workflow was still fragile and annoying enough that people bypassed it when in a hurry. That, for me, was the point where "just use scripts" stopped being a serious long term answer.

"We tried rolling our own Keychain integration. It technically worked, but the friction meant people started sneaking .env files back into repos 'temporarily' and then forgetting about them."

- Lead engineer at a 7-person AI startup in Waterloo

What KeyStack Actually Does On Your Mac

A local macOS secrets manager built around your projects

KeyStack is the productized version of that "use Keychain instead of files" idea, built specifically for macOS Sonoma and later, in SwiftUI, and sold as a one-time purchase on the Mac App Store.

At a high level, it does one core thing: every environment variable and API key you put into KeyStack is stored in your macOS Keychain, protected by the Secure Enclave, not in a plain text .env file. You authenticate once per session with Touch ID or your login password, and the vault auto-locks when you switch to another app. No accounts, no sync, no network calls.

Instead of a pile of .env files, you see a structured view:

  • Projects that roughly line up with your repos or deployed services.
  • Environments like Development, Staging, Production, or custom labels.
  • Variables that can belong to multiple projects when it makes sense.

So your DATABASE_URL can show up in both your Next.js front-end project and your background worker repo, while still being a single secret in the vault.

Importing your existing mess of .env files

The first time I tried KeyStack on my own machine, I wondered how much manual re-entry I was in for. The answer was less than an hour, because the importer actually does what you'd hope.

You point KeyStack at a project folder. It scans for these files:

  • .env
  • .env.local
  • .env.development
  • .env.staging
  • .env.production
  • .env.test

It then surfaces what it finds in a preview. If the same key shows up in multiple files, it flags them so you can see duplication. You decide what to import, and where.

For a small Django + Postgres app for a non-profit in Halifax, we did exactly this:

  1. Clone repo to a new MacBook for a contractor.
  2. Install KeyStack.
  3. Point at the repo folder and import .env.development and .env.staging.
  4. Delete the .env files from the project and rely on KeyStack exports when needed.

The migration was fast enough that nobody complained, which is my bar for "sane developer tooling."

Exporting when you still need a .env file

Most of the time, your app still expects actual environment variables. Frameworks like Next.js, Django, Rails, Node/Express, and Firebase CLIs often read from .env or from the shell environment. KeyStack does not inject values directly into your runtime or your CI. It sticks to local secrets management on one machine.

So you use export flows when you need them:

  • Export a selection of variables to a .env file for local use.
  • Export a keys-only .env.example for source control, so teammates know what variables they need without seeing real values.
  • Copy a bunch of KEY=value pairs to the clipboard for quick shell work. The clipboard auto-clears after 30 seconds so you are not pasting secrets into Slack by accident later.

Is exporting a .env file less safe than never writing to disk at all? Obviously. But this is where you balance "perfect" with "I need to run npm run dev in 10 seconds." The improvement is that the source of truth is in an encrypted vault, and you only emit .env snapshots when you actually need them.

Working With Projects, Environments, and Stack Templates

Mapping KeyStack to how you actually build apps

KeyStack nudges you into a structure that matches how most of you already organize services:

  • You create a Project for each codebase or logical service: "marketing-site", "billing-worker", "customer-dashboard".
  • Inside each Project you tag variables by environment: Development, Staging, Production, or any custom label you care about ("Local", "Demo", "Canary").
  • Variables themselves are shared objects. So STRIPE_SECRET_KEY can live in three projects if they genuinely share the same Stripe account.

This turns into something surprisingly useful when you are juggling 10+ services on your Mac. You stop thinking "where did I put that key" and instead think "this is for the billing worker, staging" and filter to what you need.

Using stack templates instead of inventing names badly

One of my quiet frustrations helping teams is how many bugs come from typos and inconsistent environment variable names. Was it NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_API_URL? Does DJANGO_SECRET_KEY have underscores or not?

KeyStack ships with pre-built stack templates for common tech stacks:

  • Next.js + Supabase
  • Next.js + Postgres
  • Django + Postgres
  • Rails + Postgres
  • Node.js / Express
  • Firebase

When you start a new project using one of those stacks, you can pick the template and get a set of variable names that match what the framework and ecosystem expect. You still fill in your own values, but you are not copy-pasting from random README examples.

This sounds like a tiny thing until you have spent an hour debugging why your Firebase app in production cannot talk to storage, only to realize the environment variable name had an extra _URL on one machine. I have done that. More than once.

Diffing environments before they break production

The environment diff view is the feature I did not think I needed, then became mildly addicted to.

You pick two environments, for example Development and Production, for a given project. KeyStack shows your variables side by side and highlights:

  • Keys that exist in one environment but not the other.
  • Keys that exist in both but have obviously different shapes, like a different region in an AWS ARN, or a missing prefix.

In a small AI product for an insurance broker in Calgary, we caught a nasty bug this way. The model serving endpoint for Production had never been set; the variable existed only in Development. The code path only hit that endpoint under a strange set of user actions, so nobody saw it in staging. A quick diff in KeyStack made the missing variable obvious before go-live.

You could script environment diffs with shell tools and jq. I used to. The reality is that when it's a one-click visual, people are much more likely to glance at it before a deploy.

Rotating API Keys Without Needing a Compliance Department

What happens when you never rotate

Most indie devs and small teams I talk to do not rotate API keys on any schedule. They rotate when something goes wrong, or when a provider forces it.

The underlying reasons are pretty rational:

  • Rotation touches a lot of surfaces: secrets store, app config, CI, sometimes mobile or desktop clients.
  • It is boring work with no visible upside when it goes well.
  • The calendar reminders you set yourself get ignored when you are in a crunch.

On one client engagement with a 4-person data startup, we discovered a production DB password that had not changed in 4 years and was sitting in three different .env files and one Notion page. They knew it was bad. They also knew that rotating it was going to cost them half a day of risk and annoyance.

How KeyStack's rotation reminders actually behave

The one feature in KeyStack that I haven't really seen in other local tools is rotation reminders baked per secret.

For any variable, you can set a rotation interval: 30, 60, 90, 180, or 365 days. When a secret comes due, a few things happen:

  • KeyStack sends you a macOS notification that this key is due for rotation.
  • Variables that are overdue get a red badge in the list, so they are visually noisy.
  • You can hit a "Needs Rotation" filter to see only secrets that are due or overdue.

KeyStack does not rotate for you, you still need to go to Stripe or Supabase or AWS and generate new keys. But the two main failure modes go away: forgetting to rotate, and losing track of what is now stale.

On my own machine, I started conservatively, with a 180 day interval for payment providers and a 90 day interval for auth tokens. It took a couple of cycles to get used to, but at this point, key rotation is a 15 minute maintenance task every few weeks, not a "we should probably do something about that" cloud hanging over the app.

Where rotation falls apart and why I still recommend it

The pushback I hear is valid: "If I rotate in KeyStack but forget to update the secret in CI or on another machine, I can break production." That is true, and it is why KeyStack is intentionally a local-only, single-user macOS secrets manager. It is meant to handle the secrets that live on your laptop, not the whole universe.

My own rule of thumb looks like this:

  • If a secret lives only on your dev machine (local DB, AI experiment, a side project), manage and rotate it entirely in KeyStack.
  • If a secret also lives in a team cloud secrets manager, use KeyStack's reminders to prompt you to go update the source of truth, then sync down locally.

You will still have the occasional "oh right, I forgot to update staging" moment. But in practice, having a concrete, visible list of "these 5 secrets need attention" beats the invisible risk of 3-year-old keys any day.

Batch Ops, Backups, and the Limits of a Local Secrets Manager

Batch operations that save you from click fatigue

Managing secrets one by one is tedious, so KeyStack supports batch operations. You can select multiple variables and:

  • Copy them to the clipboard in one shot (again, auto-clearing in 30 seconds).
  • Export them together to a .env file.
  • Link them to a project or environment.
  • Set rotation intervals for the whole batch.
  • Delete them when you are cleaning up dead configs.

For a Node/Express + Postgres stack a freelancer in Ottawa was running, we used this to clean up a bunch of staging-only keys that were not needed anymore after a provider change. Instead of hunting them one by one, we filtered by project, selected, and removed them.

Encrypted whole-vault backup and restore when you get a new Mac

The question that made me hesitant with local-only tools for a while was: "What happens when my Mac dies or I replace it?" With secrets in flat files, you get automatic backup, but to things you might not want (cloud syncs, offsite disks). With secrets only in Keychain, migration is less obvious.

KeyStack's answer is a whole-vault encrypted backup. You can export the entire vault to a single password-protected file, store that wherever you like, then restore it on a new Mac. The backup file is never transmitted by KeyStack anywhere; it is just a file on disk that you are responsible for handling.

For a solo indie in Quebec City with three macOS machines (desktop, laptop, Mac mini home lab), this ended up as a simple pattern: every so often, export a vault backup to an encrypted disk image they already used for other sensitive stuff, copy that around as needed, restore where necessary.

What KeyStack does not try to solve

There are some real limits here, and they are intentional.

  • KeyStack is not a cloud secrets manager. If you need shared, synced secrets across a team, across CI, and in production, you want something like Doppler, Infisical, or dotenv-vault on top of your infra.
  • KeyStack is single-user, macOS Sonoma and later only. There is no Windows or Linux build, no team workspace, no web UI.
  • KeyStack does not inject environment variables into your CI pipeline or your Kubernetes cluster. That is a separate problem and lives in the world of Terraform, Vault, or your chosen platform.

In practice, that splits the world pretty cleanly:

  • Use a cloud secrets manager for shared, production-facing secrets that multiple people and systems need.
  • Use KeyStack as the local half of the problem, for the developer workstation on your desk, with macOS Keychain as the backing store.

And yes, that means you might have the same logical secret represented twice: once in your cloud manager, once in your local KeyStack vault. The upside is that your laptop stops being the weakest link.

How To Get From "Messy .env" To "Reasonably Safe" In One Afternoon

A concrete Monday afternoon migration plan

If you want to harden your macOS secrets situation without derailing a sprint, here is roughly what I have done with a few clients:

  1. Pick one Mac and one project to start with. Do not try to migrate everything at once.
  2. Install KeyStack from the Mac App Store. It is a one-time purchase, not a subscription. Check the App Store for current pricing at the listing.
  3. Open the app, authenticate with Touch ID or password, and create a Project that matches your repo name.
  4. Use the import feature pointed at that repo folder. Pull in .env, .env.local, and whichever environment-specific .env.* files you are actually using.
  5. Assign variables to the right environments and projects. Clean up duplicates.
  6. Delete or archive the old .env files, keeping only .env.example with no values committed to git.
  7. When you need env vars locally, export from KeyStack or copy to clipboard as needed.
  8. Optionally, set rotation reminders on your more sensitive keys (payments, auth, production DB).

Once you are comfortable with that flow on one project, repeat across the rest of your repos. The first one takes the longest because you are making decisions about naming and structure. After that, it is muscle memory.

Where NerdSnipe fits if you want help

If you are a small team and this sort of plumbing is the thing that never quite makes it to the top of the backlog, it is the kind of detail work we handle for clients all the time. We help teams standardize how they manage secrets, wire dev environments, and keep AI-heavy projects from quietly accumulating risk on someone's laptop.

Whether you DIY it or bring in help, the basic move is the same: pull secrets out of plain text .env files, put them into something safer like macOS Keychain through KeyStack, and make rotation and environment parity a routine, not a panic.

If you are reading this on a Mac and you know there is at least one ugly .env file in your home directory, you are the target user for KeyStack. You can grab the app on the Mac App Store at this link, or read a bit more about it on the product page at nerdsnipe.cc/keystack. Try it on one project, see how it feels, and if you want a second set of eyes on hardening the rest of your stack, that is exactly the sort of thing we do all week.

Frequently Asked Questions

Ready to act on this?

Book a free 45-minute AI strategy call.

We'll look at your specific business, find the highest-value AI opportunity, and give you a clear next step — no pitch, no pressure.