I work with coding agents almost 100% of my time now. Claude Code, Cursor, the usual suspects. And at some point I realized something uncomfortable: every time I wanted an agent to run a task that needed a real credential, I was either pasting a key into a chat or pointing it at a .env file sitting in plaintext on my disk. Neither felt right.
That is how Lokalite started: a local-first secrets manager for macOS, built around how I actually work with agents.
Where the idea came from
The problem is old, the agents just made it louder.
Secrets on a developer machine usually live in .env files. They are convenient and everybody understands them, but they are plaintext, they get copied between projects, they leak into shell history, and nothing records who read them. For years that was an accepted tradeoff because the only thing reading them was you and your own code.
I looked at the alternatives. 1Password, Doppler, Infisical, they all solve real problems, but most of them are paid, account-based, and cloud-first. For a single dev like me, paying a per-seat price to hand my own credentials to someone else's server felt backwards. I did not want a vendor in the middle of my own API keys. I wanted the opposite: keep everything on my machine, encrypt it properly, and still make it pleasant to use.
So the first version of the wishlist was simple and very personal:
- Keep secrets encrypted at rest, not in plaintext.
- Keep everything local. No account, no cloud, no telemetry.
- Import the
.envfiles I already had, so adopting it was not a chore. - Manage all of it from a nice interface, not a config file.
That last point matters more than it sounds. A secrets manager you avoid opening is a secrets manager you stop trusting.
I started sketching this during paternity leave, in the short windows you get when a newborn is asleep. Leo would go down for a nap, and instead of context-switching into something heavy, I would write a small spec for one piece of this and hand it off. Secrets management turned out to be a good fit for that rhythm: lots of small, well-defined pieces.
The build
At the core there is an encrypted vault. Every secret value is encrypted with AES-256-GCM through CryptoKit before it touches the disk. The 256-bit vault key is generated on first use and stored in the macOS login keychain, so the key itself never lives inside the vault file. The menu bar app asks for Touch ID before it shows you anything, and it auto-locks after a period of inactivity. If a CLI or MCP request lands while the vault is locked, nothing unlocks silently: the app raises the same Touch ID prompt, naming whoever is asking, and the request fails closed if you decline.
On top of that there are three ways to reach your secrets, and they all read from the same vault:
- A menu bar app for the human moments: search, copy, reveal. Recently copied secrets float to the top, and a global shortcut (default
⌘⇧Space) opens the popover from anywhere. - A CLI for the terminal: add, get, copy, list, import, export, and run.
- An MCP server for the agents, which is the part I will get to in a second.
Importing was non-negotiable, so lokalite import .env pulls an existing file straight into the vault. From there you organize things into projects, and each project can have multiple environments (dev, staging, production) with their own values. A project can be linked to a directory, which becomes important later.
The two CLI commands I use the most are these:
# Run a single command with secrets injected as env vars, scoped to that process
lokalite run -- npm start
lokalite run --keys OPENAI_API_KEY,ANTHROPIC_API_KEY -- claude
# Or inject into the current shell session
eval $(lokalite shell)
lokalite run is the safe default because the secrets only exist for that one subprocess. eval $(lokalite shell) is more convenient but exports everything into the session, so the CLI is honest about that tradeoff in its own docs. Copies to the clipboard auto-clear after 30 seconds, and they are marked so well-behaved clipboard managers skip them.
None of these pieces are individually clever. The point was to make the boring, correct thing also the easy thing.
The part that exists because of agents
Here is the reason this stopped being a weekend toy and became something I use every day.
When an agent needs a credential to do real work, the naive path is to paste it into the conversation. Now that secret lives in the model's context, maybe in logs, maybe in a transcript, and you have no record of it. Multiply that across a dozen tasks a day and it gets ugly fast.
Lokalite exposes the vault to agents through MCP instead. The agent does not get your secrets pasted into a chat; in fact, it never sees the value at all. When it calls get_secret, the server writes the value to a single-use shell script and hands back a source command. The agent runs that in its own shell, the variable is loaded, the script deletes itself, and the raw value never enters the model's context. Every access is written to an activity log, stamped with which agent did it; the daemon takes the caller's identity from the kernel peer PID, so a client cannot forge or hide it. With the CLI and the MCP server, agents can manage and use secrets to run tasks on my behalf without those secrets becoming part of the conversation.
lokalite install registers the server with Claude Code automatically, and there are flags for Claude Desktop, Cursor, and Windsurf:
{
"mcpServers": {
"lokalite": {
"command": "lokalite",
"args": ["mcp"]
}
}
}
By default the server is read-only: list_secrets returns names and descriptions but never values, get_secret does the handoff described above, and a few small tools handle projects and environments (list_projects, list_environments, use_environment). There is a --read-write mode that adds add_secret, set_secret, and delete_secret, but you opt into that deliberately.
The nice touch is project resolution. When a tool call does not specify a project, the server figures it out from the caller's working directory using the project's linked path, the same way the CLI does. So if I am working inside a project folder, the agent automatically gets that project's secrets and nothing else.
The plaintext file I had not noticed
Getting secrets out of .env did not get them out of plaintext. MCP configs are the same problem wearing different clothes: ~/.claude.json, claude_desktop_config.json, and a project's .mcp.json all carry tokens in a plain env block, and that last one usually lives in a repo.
So secrets got references. You write lokalite://myproject/GITHUB_TOKEN where the token used to be, and wrap the command in lokalite run --refs-only --:
{
"mcpServers": {
"github": {
"command": "lokalite",
"args": ["run", "--refs-only", "--", "npx", "-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "lokalite://myproject/GITHUB_TOKEN" }
}
}
}
A reference carries no secret material, so the file is safe to commit. lokalite run swaps it for the real value in the child process's environment right before spawning, brokered through the app, so the per-secret rules below still apply and the read still lands in the log. Resolution fails closed: if a reference is malformed, unknown, or denied, you get the variable name and the reference printed back, never a value, and the command does not run.
Being honest about the threat model
I do not want to oversell the security here, because a secrets tool that pretends to be bulletproof is worse than a plain .env file.
A couple of things are worth saying plainly. The handoff keeps values out of the transcript, but an agent that can name a secret can still load it into a shell it controls, and list_secrets gives it the names. So each secret has an agent-access tier: allow is the default, block refuses agents outright, and approve and strict require a Touch ID confirmation before the value is released, no matter who is asking. Agent detection is a process-tree match and a renamed agent can evade it, so block is a courtesy guard; the consent tiers are the real gate, because the prompt does not depend on detection. The MCP server does declare its own caller as an agent when it talks to the daemon, and that hint can only tighten policy and never relax it, so block still holds on MCP calls when detection would have missed. The CLI plays by the same rules: lokalite get and lokalite copy refuse to print anything when an agent is detected in the calling process tree, so an agent cannot sidestep the handoff by shelling out. Writes are governed too; an agent cannot overwrite or delete a protected secret without consent. On top of that, the server ships read-only by default, you can scope it to a single project, and every access is logged.
There is one more gate that is not mine. Your MCP client runs its own permission check before the call ever reaches Lokalite, so Claude Code may stop and ask you, or refuse mcp__lokalite__get_secret outright, depending on how you have it configured. The two layers are complementary: the client decides whether the tool may run at all, Lokalite decides whether this particular secret is released. Worth knowing before you allowlist the tool to stop the prompts, because that removes the client's question for every secret in the project, production included. If you do it, mark the sensitive ones approve and let Touch ID be the thing that asks.
The other honest bit: secret values are encrypted, but metadata is not. Names, descriptions, project names, and the access log sit unencrypted in the vault file. They reveal which services you use, not the credentials themselves, and they rely on your home directory permissions and FileVault. The real trust boundary is your unlocked macOS session, exactly like ~/.aws/credentials or a .env file. Lokalite's improvement over those is concrete and bounded: values encrypted at rest, and access logged. Not magic, just better than plaintext.
I would rather a reader install this knowing the edges than discover them later.
Built During a Nap
The vault core and the CLI came together quickly in those nap-sized blocks. The design was the hard part.
I wanted the menu bar app to feel nice, not like a config file with a window around it, and SwiftUI fought me on the specifics. The framework is great until you need one particular behavior it does not want to give you: a popover that sizes itself correctly when a project has no secrets, an empty state that fills the space instead of leaving grey bars padding the top and bottom, a three-column layout that stays balanced as you resize. Each of those was a small thing on paper and a real iteration loop in practice. I would write a spec, hand off a pass, open the app, see that it felt slightly off, and go again. The look and feel only came together after a lot of those small rounds.
That turned out to be a good match for the constraint. Polishing UI is exactly the kind of work that fragments well: open the app, notice one thing that bothers you, fix that one thing, ship it, close the laptop when Leo wakes up. The logic could be specced and handed off cleanly; the feel had to be earned one nap at a time.
A small story
The menu bar icon is an armadillo. I went looking for a mascot and the armadillo was an easy yes: a small animal that rolls itself into a sealed ball when something gets too close, carrying its protection with it instead of relying on a burrow somewhere else. That is more or less the whole pitch for a local-first vault, so it stuck. It is a silly detail, but these projects are built in stolen time, and the silly details are part of what makes them feel like mine.
If you want to try it, the code is on GitHub, and both pieces install with Homebrew: brew install RubenGlez/lokalite/lokalite for the CLI and brew install --cask RubenGlez/lokalite/lokalite-app for the menu bar app.


Top comments (0)