Secrets Without .env Files

Secrets Without .env Files

Tako-kun ·

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.

Tako 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.

The problem with .env

The .env convention started as a convenience and became load-bearing infrastructure. Here’s what you’re actually trusting when you use one:

RiskWhat happens
Plaintext on diskAnyone with server access reads your secrets
Environment variablesInherited by child processes, visible in /proc/<pid>/environ
Manual distributionCopy-paste via Slack, email, or shared drives
No encryptionAccidentally committed = fully exposed
No typesTypo in process.env.DATBASE_URL fails silently at runtime
No audit trailNo way to know which secrets exist in which environments
Agent-readableAI coding agents can read .env files — one prompt injection away from exfiltration

Some 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.

How Tako handles secrets

Encrypted at rest

When you run tako secrets set, Tako encrypts the value with AES-256-GCM before writing it to .tako/secrets.json. The first secret set for an environment creates a random local key, cached under Tako’s data directory.

$ tako secrets set DATABASE_URL --env production
Enter value: ****
  Set secret DATABASE_URL for environment production

The resulting file is safe to commit. It contains only encrypted blobs and a per-environment key id:

{
  "production": {
    "key_id": "0123456789abcdef",
    "secrets": {
      "DATABASE_URL": "base64(nonce + ciphertext + GCM tag)",
      "STRIPE_KEY": "base64(nonce + ciphertext + GCM tag)"
    }
  }
}

Secret names are visible (so you can list what exists without decrypting), but values are useless without the matching local key. Each environment gets its own key id, so production and staging can be shared independently.

Team sharing without a vault

No external service required. When a new team member joins:

  1. They pull the repo (which includes .tako/secrets.json)
  2. A teammate runs tako secrets key export --env production
  3. They send the single exported key string out of band
  4. The new team member runs tako secrets key import
  5. The key is cached locally at $TAKO_HOME/keys/{key_id} with 0600 permissions

Share only the environment keys people need. For CI, import the exported key bundle into the runner’s Tako data directory before decrypting or syncing secrets.

fd 3 injection — secrets never hit disk on the server

This 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.

Diagram

Your 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.

The Tako SDK handles this automatically. In JavaScript, tako generate emits a project-local tako.d.ts that types the tako.secrets bag from tako.sh:

import { tako } from "tako.sh";

const db = tako.secrets.DATABASE_URL;

console.log(tako.secrets); // "[REDACTED]"
JSON.stringify(tako.secrets); // "[REDACTED]"

The 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.

Typed secrets with tako generate

Run tako generate and Tako reads your encrypted secrets file to generate type definitions — without decrypting the values (remember, names are plaintext).

TypeScript gets a tako.d.ts that augments TakoSecrets:

declare module "tako.sh" {
  export interface TakoSecrets {
    readonly DATABASE_URL: string;
    readonly STRIPE_KEY: string;
  }
}

Go gets a tako_secrets.go with PascalCase accessors:

var Secrets = struct {
  DatabaseUrl func() string
  StripeKey   func() string
}{...}

Autocomplete in your editor. Compile-time errors for typos. No more process.env.DATBASE_URL bugs discovered in production.

What about rotation?

Change a secret locally, then sync it to your servers:

tako secrets set STRIPE_KEY --env production
tako secrets sync --env production

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.

The full picture

.env filesExternal vaultTako secrets
Encryption at restNoneVault-sideAES-256-GCM, local
StoragePlaintext, gitignoredExternal serviceEncrypted, committed
DistributionManual copy-pasteAPI calls at deployPassphrase-derived keys
Runtime injectionEnvironment variablesEnvironment variablesfd 3 pipe
Type safetyNoneNoneGenerated types
Leak surfaceDisk, env, logs, /procEnv, logs, /procProcess memory only
External dependencyNoneVault serviceNone

Try it

tako secrets set API_KEY --env production
tako secrets set API_KEY --env development
tako secrets list

Check the CLI reference for the full command set, or the deployment docs for how secrets flow during a deploy. The development guide covers how secrets work locally with tako dev.

Your secrets deserve better than a plaintext file in .gitignore.