DEV Community

Muhammad Ahmad
Muhammad Ahmad

Posted on

Every tutorial tells you to add .env to .gitignore. That's not enough.

Here's something nobody talks about.

.gitignore doesn't encrypt your secrets. It just hides them from git.

They're still sitting on your laptop as plaintext. Every tool you install can read them. Every script that runs can read them. One accidental commit and your database password is public on GitHub forever.

So I built dotlock — an encrypted .env vault with a terminal UI, written in Go.

Before and after

Before dotlock

DATABASE_URL=postgres://localhost/myapp   # plaintext, readable by anything
STRIPE_KEY=sk_live_abc123                 # one grep away from anyone
Enter fullscreen mode Exit fullscreen mode

After dotlock

# .dotlock file on disk — looks like this:
[encrypted binary — unreadable without your private key]
Enter fullscreen mode Exit fullscreen mode

How it works under 10 seconds

cd my-project
dotlock set DATABASE_URL # prompts for value, input is masked
dotloc               # opens the terminal UI
Enter fullscreen mode Exit fullscreen mode

Secrets are encrypted with age — X25519 key agreement and ChaCha20-Poly1305 authenticated encryption. The same primitives serious security engineers use. No master password. No cloud. No telemetry. 100% offline.


What it looks like


Two panels — profiles on the left, secrets on the right. Values are masked by default. Press v to reveal for 30 seconds, then it hides itself automatically.

Switch between dev, staging, and prod profiles. Run a diff before deploying to catch missing variables before they break your app.


The interesting technical bit

The hardest part wasn't the encryption — filippo.io/age makes that straightforward.

It was the TUI.

BubbleTea uses the Elm architecture — Model, Update, View. Everything is a message. A keypress is a message. A timer firing is a message. Your Update function receives messages and returns a new model.

The 30-second auto-hide on secret reveal works like this — no time.Sleep, no goroutines:

type secretReveal struct {
    key    string
    value  []byte
    expire time.Time  // Now() + 30 seconds
}
Enter fullscreen mode Exit fullscreen mode

On every render, check if time.Now() is past the expiry. If it is, zero the bytes and clear the display. Simple once you understand the model but it took me longer than I expected to get right.


Install it

go install github.com/ahmadraza100/dotlock@latest
cd your-project
dotlock init
dotlock ui
Enter fullscreen mode Exit fullscreen mode

Or download a binary from the releases page — Mac, Linux, and Windows all supported.

Full source, architecture docs, and security model on GitHub:

👉 github.com/ahmadraza100/dotlock


What do you currently use for managing local secrets? Curious what others are doing — drop it in the comments.

Top comments (0)