← Blog ·

What Is a Secrets Manager (and Why Your .env File Isn't One)

Every project has secrets: API keys, database tokens, third-party credentials. And almost every team handles them the same way: a .env file at the root of the repo, with a .gitignore entry that's supposed to protect it. That file is not a secrets manager. It's plaintext with good intentions.

A secrets manager is a system built specifically to store, encrypt, distribute, and audit sensitive credentials — fully separated from your code and from generic filesystem permissions. A .env file does none of that. It just holds strings in a text file.

What a secrets manager actually does

A real secrets manager meets, at minimum, these properties:

  • Actual encryption, at rest and in transit — not just "the file lives on a private server."
  • Granular access control, scoped by environment (dev, staging, prod) and by service or person.
  • Audit trail: who accessed which secret, when, and from where.
  • Rotation and revocation of credentials without redeploying half the system.
  • No plaintext secret ever touches disk, logs, or shell history.

A .env file was never designed to do any of this. It became popular as a convenience convention (dotenv and early Rails-style config), not as a security tool.

Why your .env isn't a secrets manager

It lives in plaintext, always

cat .env
DATABASE_URL=postgres://user:pass@host:5432/db
STRIPE_SECRET_KEY=sk_live_51H...
OPENAI_API_KEY=sk-proj-...

Anyone with read access to the filesystem — a teammate, a misconfigured backup, a Docker image that shouldn't include it, a CI job with more permissions than it needs — can read those keys directly. No encryption, no gate, no friction.

It eventually leaks into git

.gitignore helps, but it's not foolproof: a careless git add -A, a fork that copies a poorly-named .env.example, an old commit buried in history that nobody cleaned up. Secrets leak from public repos every day through mistakes exactly like this, and bots actively scan GitHub for live-looking key patterns within minutes of a push.

Checking your own history is quick, and usually a little scary:

git log -p --all | grep -iE \"sk_live|AKIA|-----BEGIN\"

It has no access control or traceability

With a .env, whoever has the file has everything — your Stripe key, your database credentials, your AI provider key, all bundled together. You can't grant one person access to the production secret and not another, and there's no way to know afterward who read it or when.

AI-generated code makes it worse

Code assistants (Copilot, Cursor, agents like Claude Code) read your whole repo to do their job, and that includes .env if it's sitting in the project directory. A secret can end up quoted back in a suggestion, pasted into a chat log, or committed automatically without anyone noticing. In /ai-security we go into why AI-generated code expands the secret-exposure surface — not just from human error, but from the agent's own workflow.

What a real secrets manager should do

Beyond the basics, a modern secrets manager should let you:

  1. Have code reference the secret by name only, never the raw value.
  2. Decrypt the secret only at the point of consumption — an authorized backend or a verified device — never in plaintext as it passes through intermediate servers.
  3. Revoke going forward: cut off future access to a secret immediately. This does not erase copies a consumer already downloaded before revocation — for that, you still need to rotate the underlying credential at the source (Stripe, AWS, OpenAI, etc.).
  4. Fit into your actual dev workflow: CLI, SDK, CI/CD, with minimal friction.

How Koove handles this

Koove is a zero-knowledge secrets manager: the server only ever stores ciphertext and never sees the real value of your keys. Encryption uses envelope encryption with X25519 and AES-256-GCM, built on open, auditable primitives.

In practice, instead of writing a key into a .env, you store it once:

koove set STRIPE_SECRET_KEY sk_live_51H... --env prod

And your code only references the name:

import { getSecret } from \"@koove/sdk\";

const stripeKey = await getSecret(\"STRIPE_SECRET_KEY\");

The real value never shows up in the repo, in your AI assistant's chat history, or in a CI log. Secrets only decrypt on verified consumers: mobile apps authenticated with Play Integrity or App Attest plus biometrics, or backends explicitly authorized to hold the key. Full CLI and SDK reference lives at /docs/sdk.

This fits particularly well when it's your own AI assistant adding new integrations to the project — it can run koove set to store a new key without that value ever touching the source code it generates.

Honest limitations

Koove significantly shrinks the surface where secrets can leak, but it doesn't make your app unhackable. Overall security still depends on how you protect access to your Koove account, your devices, and your CI credentials. Revoking a secret stops future access, but it can't "un-deliver" a copy a consumer already downloaded before the revocation — if you suspect a compromise, rotate the underlying credential at the source too. And to be direct: Koove doesn't carry SOC 2 or ISO certifications today; if that's a hard requirement for your org, it's better to know that upfront. More context is in the /faq.

Getting started

  1. Create an account at /register.
  2. Install the CLI and run koove set for your first secret.
  3. Replace process.env calls with getSecret() from the SDK.
  4. Check /plans if you need separate environments for your team or production.

A .env file still has its place for non-sensitive config — but for API keys, tokens, and credentials, you need something that actually earns the name secrets manager. If you want to see how this fits into an AI-assisted dev workflow, take a look at /ai-security, or sign up and run koove set on your next project.

AI-generated code

Your AI writes the code. Who guards the secrets?

The most common security failure in AI-generated apps is exposed credentials. With Koove, your own assistant stores every token from the CLI — encrypted on your machine, never in the code or the repo.