{"slug":"secrets-without-env-files","url":"https://tako.sh/blog/secrets-without-env-files/","canonical":"https://tako.sh/blog/secrets-without-env-files/","title":"Secrets Without .env Files","date":"2026-04-06T11:39","description":"How Tako encrypts secrets at rest, injects them via fd 3 at runtime, and generates typed accessors — so plaintext never touches disk.","author":null,"image":"8f2cb3d41b80","imageAlt":null,"headings":[{"depth":2,"slug":"the-problem-with-env","text":"The problem with .env"},{"depth":2,"slug":"how-tako-handles-secrets","text":"How Tako handles secrets"},{"depth":3,"slug":"encrypted-at-rest","text":"Encrypted at rest"},{"depth":3,"slug":"team-sharing-without-a-vault","text":"Team sharing without a vault"},{"depth":3,"slug":"fd-3-injection--secrets-never-hit-disk-on-the-server","text":"fd 3 injection — secrets never hit disk on the server"},{"depth":3,"slug":"typed-secrets-with-tako-typegen","text":"Typed secrets with tako typegen"},{"depth":2,"slug":"what-about-rotation","text":"What about rotation?"},{"depth":2,"slug":"the-full-picture","text":"The full picture"},{"depth":2,"slug":"try-it","text":"Try it"}],"markdown":"Every deploy tool has a secrets story. Most of them end with \"add it to your `.env` file.\" The `.env` file sits in `.gitignore`, gets copy-pasted between teammates over Slack, and lives as plaintext on every server it touches. If someone commits it by accident — and someone always does — you're rotating every key in the file.\n\nTako does secrets differently. Encrypted at rest, injected at runtime through a file descriptor, and typed so your editor knows what's available. No plaintext touches disk. No environment variables leak to child processes.\n\n## The problem with `.env`\n\nThe `.env` convention started as a convenience and became load-bearing infrastructure. Here's what you're actually trusting when you use one:\n\n| Risk                      | What happens                                                                         |\n| ------------------------- | ------------------------------------------------------------------------------------ |\n| **Plaintext on disk**     | Anyone with server access reads your secrets                                         |\n| **Environment variables** | Inherited by child processes, visible in `/proc/<pid>/environ`                       |\n| **Manual distribution**   | Copy-paste via Slack, email, or shared drives                                        |\n| **No encryption**         | Accidentally committed = fully exposed                                               |\n| **No types**              | Typo in `process.env.DATBASE_URL` fails silently at runtime                          |\n| **No audit trail**        | No way to know which secrets exist in which environments                             |\n| **Agent-readable**        | AI coding agents can read `.env` files — one prompt injection away from exfiltration |\n\nSome tools improve on this by integrating with external vaults — 1Password, AWS Secrets Manager, Doppler. That works, but it's another service to configure, pay for, and debug when deploys fail at 2 AM.\n\n## How Tako handles secrets\n\n### Encrypted at rest\n\nWhen you run [`tako secrets set`](/docs/cli), Tako encrypts the value with **AES-256-GCM** before writing it to `.tako/secrets.json`. The encryption key is derived from a team passphrase using **Argon2id** (64 MiB memory, 3 iterations, 4 lanes) — the same KDF recommended by OWASP for password hashing.\n\n```bash\n$ tako secrets set DATABASE_URL --env production\nEnter value: ****\n  Set secret DATABASE_URL for environment production\n```\n\nThe resulting file is safe to commit. It contains only encrypted blobs and a per-environment salt:\n\n```json\n{\n  \"production\": {\n    \"salt\": \"base64_encoded_salt\",\n    \"secrets\": {\n      \"DATABASE_URL\": \"base64(nonce + ciphertext + GCM tag)\",\n      \"STRIPE_KEY\": \"base64(nonce + ciphertext + GCM tag)\"\n    }\n  }\n}\n```\n\nSecret names are visible (so you can list what exists without decrypting), but values are useless without the key. Each environment gets its own salt, so the same passphrase produces different keys for production and staging.\n\n### Team sharing without a vault\n\nNo external service required. When a new team member joins:\n\n1. They pull the repo (which includes `.tako/secrets.json`)\n2. They run `tako secrets key derive --env production`\n3. They enter the team passphrase\n4. Argon2id derives the same key from the same passphrase + salt\n5. The key is cached locally at `$TAKO_HOME/keys/` with `0600` permissions\n\nOne passphrase, shared once. After that, every team member can encrypt and decrypt independently. For CI, set `TAKO_PASSPHRASE` as an environment variable or use `tako secrets key export` to copy the raw key.\n\n### fd 3 injection — secrets never hit disk on the server\n\nThis is the part that matters most. When `tako-server` spawns your app, it doesn't set environment variables. Instead, it opens **file descriptor 3** as a pipe and writes the decrypted secrets as JSON before your code starts.\n\n```d2\ndirection: right\n\nserver: tako-server {\n  style.font-size: 13\n}\n\npipe: fd 3 pipe {\n  shape: circle\n  style.font-size: 13\n}\n\napp: Your App {\n  shape: hexagon\n}\n\nserver -> pipe: \"write JSON\"\npipe -> app: \"read + close\"\n```\n\nYour app reads fd 3 once at startup, parses the JSON, and the pipe is closed. The secrets exist only in process memory — never written to disk on the server, never in environment variables, never visible in `/proc/<pid>/environ` or `ps auxe`.\n\nThe [Tako SDK](/docs) handles this automatically. In JavaScript, `tako typegen` emits a project-local `tako.gen.ts` that exports a typed `secrets` bag. Your app imports what it needs:\n\n```typescript\n// tako.gen.ts is populated from fd 3 before your code runs\nimport { secrets } from \"../tako.gen\";\n\nconst db = secrets.DATABASE_URL;\n\nconsole.log(secrets); // \"[REDACTED]\"\nJSON.stringify(secrets); // \"[REDACTED]\"\n```\n\nThe SDK wraps secrets in a Proxy that redacts on `toString()` and `toJSON()` — so accidental logging never leaks values. In Go, it's the same idea with thread-safe accessors.\n\n### Typed secrets with `tako typegen`\n\nRun [`tako typegen`](/docs/cli) and Tako reads your encrypted secrets file to generate type definitions — without decrypting the values (remember, names are plaintext).\n\n**TypeScript** gets a `tako.gen.ts` that exports a typed `Secrets` interface and a `secrets` instance:\n\n```typescript\nexport interface Secrets {\n  readonly DATABASE_URL: string;\n  readonly STRIPE_KEY: string;\n  toString(): \"[REDACTED]\";\n  toJSON(): \"[REDACTED]\";\n}\n```\n\n**Go** gets a `tako_secrets.go` with PascalCase accessors:\n\n```go\nvar Secrets = struct {\n  DatabaseUrl func() string\n  StripeKey   func() string\n}{...}\n```\n\nAutocomplete in your editor. Compile-time errors for typos. No more `process.env.DATBASE_URL` bugs discovered in production.\n\n## What about rotation?\n\nChange a secret locally, then sync it to your servers:\n\n```bash\ntako secrets set STRIPE_KEY --env production\ntako secrets sync --env production\n```\n\n`tako secrets sync` decrypts locally, pushes to the server over SSH, and triggers a rolling restart. New instances get the updated values via fd 3. The old instances keep running with old values until they're drained — zero downtime, same as a regular deploy.\n\n## The full picture\n\n|                         | **.env files**           | **External vault**    | **Tako secrets**        |\n| ----------------------- | ------------------------ | --------------------- | ----------------------- |\n| **Encryption at rest**  | None                     | Vault-side            | AES-256-GCM, local      |\n| **Storage**             | Plaintext, gitignored    | External service      | Encrypted, committed    |\n| **Distribution**        | Manual copy-paste        | API calls at deploy   | Passphrase-derived keys |\n| **Runtime injection**   | Environment variables    | Environment variables | fd 3 pipe               |\n| **Type safety**         | None                     | None                  | Generated types         |\n| **Leak surface**        | Disk, env, logs, `/proc` | Env, logs, `/proc`    | Process memory only     |\n| **External dependency** | None                     | Vault service         | None                    |\n\n## Try it\n\n```bash\ntako secrets set API_KEY --env production\ntako secrets set API_KEY --env development\ntako secrets ls\n```\n\nCheck the [CLI reference](/docs/cli) for the full command set, or the [deployment docs](/docs/deployment) for how secrets flow during a deploy. The [development guide](/docs/development) covers how secrets work locally with `tako dev`.\n\nYour secrets deserve better than a plaintext file in `.gitignore`."}