Lokalite is a local-first secrets manager for macOS, built for how developers work with AI coding agents. Your API keys, tokens, and credentials live in an encrypted vault on your machine. When an agent needs one, the value is delivered into its shell runtime, never into the chat — so secrets stay out of the model's context and your conversation history. Reaches Claude Code, Cursor, and Windsurf through MCP, on your terms, with every access logged. Menu bar app, CLI, and MCP server in one. No cloud, no accounts, no telemetry.
Project page: rubenglez.dev/lokalite
vs.: values are encrypted at rest instead of sitting in plaintext, access is logged, and the same secrets are available to your shell, the CLI, and your AI agents without copy-pasting between files..env
filesvs. cloud secret managers(Doppler, Infisical, 1Password): nothing leaves your machine. No account to create, no per-seat billing, no vendor to trust with your credentials. Local-first is the point, not a limitation.Built for agents: an AI agent loads the secrets it needs through MCP — the value reaches its shell runtime via a one-time handoff, never the chat transcript or the model's context. Logged, read-only by default, scoped per project; no pasting keys into a conversation.
Encrypted vault: AES-256-GCM via CryptoKit, vault key stored in Apple Keychain** Touch ID unlock**: biometric authentication to unlock the vault — from the menu bar app, or via a prompt when a CLI/MCP request arrives while the vault is locked — plus per-read consent onapprove
/strict
secrets. While the vault is unlocked, reads don't re-prompt — seeSecurityMenu bar app: search, copy, and reveal secrets without leaving your workflow; recent secrets surfaced at the top** Global keyboard shortcut**: open the popover from anywhere, configurable in Settings (default⌘⇧Space
)Full CLI: read, write, and inject secrets from the terminal** Projects**: group secrets by project; link to a local directory for automatic resolution** Environments**: per-project environment profiles (dev, staging, production) with per-environment secret values; every project starts with a Default environment: pull from an existing.env
import/export.env
file or export back to oneEncrypted backup/restore:lokalite backup
writes a passphrase-encrypted file;lokalite restore
reads it backShell injection:eval $(lokalite shell)
exports secrets into the current sessionClipboard auto-clear: copied values are wiped after 30 seconds** Session timeout**: vault auto-locks after inactivity** In-app auto-updates**: the menu bar app keeps itself current via Sparkle — a manual*Check for Updates…item plus opt-in automatic background checksMCP integration: expose your vault as tools to Claude Code, Cursor, Windsurf, and any other MCP-compatible agent* Secret references**: commitlokalite://project/KEY
instead of a token in MCP configs;lokalite run --refs-only
resolves it at spawn timeZero runtime dependencies: no cloud, no telemetry, no vendor lock-in
- macOS 14 or later
CLI, MCP server, and menu bar app via Homebrew:
brew install RubenGlez/lokalite/lokalite
brew install --cask RubenGlez/lokalite/lokalite-app
lokalite install # registers the MCP server in ~/.claude.json
If you don't need the menu bar app:
brew install RubenGlez/lokalite/lokalite
lokalite install
If you already have the CLI installed or don't need it:
brew install --cask RubenGlez/lokalite/lokalite-app
Download from the Releases page:
CLI: run the.pkg
installer, thenlokalite install
Menu bar app: drag the.dmg
to Applications and launch it — no quarantine workaround needed.Releases are signed with an Apple Developer ID and notarized by Apple — the app, DMG, and CLI
.pkg
all pass Gatekeeper on first launch. The menu bar app then keeps itself up to date automatically (Sparkle). Every release also ships aSHA256SUMS
file you can verify against the artifacts.
lokalite init
lokalite status
lokalite status --json
lokalite log
lokalite log --limit 20
lokalite log --source cli # filter by app, cli, or mcp
lokalite log --agent claude # filter by the AI agent that accessed the secret
lokalite add OPENAI_API_KEY sk-...
lokalite add OPENAI_API_KEY # no value: prompts for it, keeping it out of shell history
echo -n "sk-..." | lokalite add OPENAI_API_KEY - # or pipe it via stdin
lokalite get OPENAI_API_KEY
lokalite copy OPENAI_API_KEY
lokalite list
lokalite list --search openai
lokalite set OPENAI_API_KEY sk-new-...
lokalite delete OPENAI_API_KEY
lokalite run -- npm start
lokalite run --keys OPENAI_API_KEY,ANTHROPIC_API_KEY -- claude
lokalite run --refs-only -- npx some-server # resolve lokalite:// references only (see Secret references)
lokalite project add MyProject
lokalite project list
lokalite project use MyProject
lokalite project link [<name>] [--path <dir>] # link to a directory; defaults to cwd
lokalite project link --unlink <name> # remove path association
lokalite project delete MyProject
lokalite env add staging
lokalite env list
lokalite env use staging
lokalite env delete staging
lokalite import .env
lokalite import .env --overwrite # overwrite existing secrets
lokalite export --output backup.lk # encrypted (default), includes everything
lokalite export --plain --output secrets.json # plaintext: approve-tier secrets are skipped (named on stderr)
lokalite export --format env # .env format, stdout; approve-tier secrets are skipped
lokalite export --format env --output .env
lokalite backup # prompts for a passphrase, writes a timestamped .lokalite file (approve-tier secrets are skipped)
lokalite backup --output backup.lokalite
lokalite restore backup.lokalite # prompts for the passphrase, skips existing secrets
lokalite restore backup.lokalite --overwrite
eval $(lokalite shell)
eval $(lokalite shell --keys OPENAI_API_KEY,ANTHROPIC_API_KEY)
Shell injection note:eval $(lokalite shell)
makes secrets visible to all child processes and shows up inenv
output for the duration of your session. Uselokalite run
to scope secrets to a single subprocess instead.
Click the armadillo icon in your menu bar (or press the global shortcut, default ⌘⇧Space
) to open the vault popover. The popover shows recently copied secrets at the top, then all secrets for the active project and environment. Use the project and environment menus in the header to switch context, and click Open Lokalite in the footer to open the full secrets manager window.
The secrets manager is a three-column layout:
Left sidebar: project list; create, rename, and delete projects; set icon and link to a local directory** Centre column**: environment switcher and searchable secrets list for the selected project** Right panel**: edit the selected secret's value; save or delete
On first launch, an onboarding screen guides you through creating your first project.
Right-click the menu bar icon (or open Settings → Updates) for Check for Updates…; once you opt in, the app also checks in the background and installs updates in place.
By default lokalite mcp
brokers vault access through the running Lokalite app over a local Unix socket, so the MCP server process never holds the vault key — it asks the app, which is the only process that can decrypt. The app is launched automatically if it isn't running. Pass --local
to open the vault in-process instead (for CI/headless use, where the value then passes through the MCP process).
lokalite install
registers the MCP server with Claude Code automatically (writing ~/.claude.json
). To register with a different client, pass --client
:
lokalite install --client claude # ~/.claude.json (default)
lokalite install --client claude-desktop # ~/Library/Application Support/Claude/claude_desktop_config.json
lokalite install --client cursor # ~/.cursor/mcp.json
lokalite install --client windsurf # ~/.codeium/windsurf/mcp_config.json
For any other MCP-compatible agent, add the same server block to its config manually:
{
"mcpServers": {
"lokalite": {
"command": "lokalite",
"args": ["mcp"]
}
}
}
By default the server is read-only and exposes these tools:
| Tool | Description |
|---|---|
list_secrets |
|
| List secret names, categories, and descriptions (values never exposed) | |
list_projects |
|
| List projects and their linked directories (no values); use when no project resolves | |
list_environments |
|
| List a project's environments, marking the active one (no values) | |
use_environment |
|
| Switch the project's active environment — the one secrets resolve from by default | |
get_secret |
|
| Load a secret into the agent's shell environment via a one-time handoff — the value is never returned to the model |
use_environment
switches the project's single active environment, the same one the menu bar app, the manager, and lokalite env use
use — switching from the agent updates all of them, and the app refreshes live. For a one-off read from a different environment without switching, pass an environment
argument to get_secret
instead.
get_secret
does not return the secret value. It writes the value to a single-use, owner-only shell script and returns a source '<path>'
command; the agent runs that in its own shell to load the variable, then runs its program in the same shell. The raw value never enters the conversation/model context, the script self-deletes on first source, and any unsourced script is swept after 30s. The server also sends instructions
on connect describing this flow and telling agents never to print a loaded variable or copy a secret into .env
/config/source.
When a tool call omits project
, the server auto-resolves it from the caller's working directory using the project's linked path — the same way the CLI does. Pass an absolute path
argument with the directory to resolve (preferred, since the server's own process may run elsewhere); otherwise it falls back to the server process's working directory. An explicit project
argument (or LOKALITE_PROJECT
in the server env
) always wins.
Pass --read-write
to also expose write tools:
{ "command": "lokalite", "args": ["mcp", "--read-write"] }
| Tool | Description |
|---|---|
add_secret |
|
| Create a new secret | |
set_secret |
|
| Update an existing secret's value | |
delete_secret |
|
| Permanently delete a secret |
Security note:the handoff keeps secret values out of the model's context, but an agent that can name a secret can still load it into a shell it controls (list_secrets
gives it the names), and a sourced value is then visible to processes in that shell. Set a per-secret agent-access tier withlokalite agent-access <name> allow|approve|block
(or from the app's secret editor):
block— off-limits:get_secret
refuses it,list_secrets
flags it[off-limits to agents]
.Best-effort:blocking an agent depends on detecting it (a process-tree name match), so a renamed or sandboxed agent can evade it. Treatblock
as a courtesy guard; useapprove
for a secret that must not be released without you in the loop.approve— consent-on-read: every read through the app broker — an agent'sget_secret
, alokalite://
reference, or the CLI'sget
/copy
— prompts for Touch ID before the value is released, whoever is asking; the approval then lasts for the rest of the unlock session.list_secrets
flags it[approval required]
. With--local
(no app to prompt) it fails closed, like block. Unlikeblock
, this doesnotrely on detection — the prompt fires for every caller.allow— the default.The CLI
get
/copy
won't print any secret to stdout or the clipboard when an AI agent is detected in the calling process tree — the same transcript-safety boundary as the MCP handoff, so an agent can't sidestep it by shelling out to read a value directly. Pass--allow-agent
to override for your own use; ablock
secret is refused regardless (no override). Anapprove
/strict
secret is additionally routed through the Lokalite app for the Touch ID prompt, for every caller (and refused, with no override, when the app isn't reachable); bulk reveals (shell
, plaintextexport
, bulkrun
injection,backup
) skipapprove
secrets and name them on stderr. This detection is best-effort (see theblock
note above) — it protects the transcript, not a determined agent's own shell; lean onapprove
/strict
consent for anything that must not be released without you in the loop.Writes are governed too:an agent'sset_secret
/delete_secret
(or CLIset
/delete
) on ablock
secret is refused, and on anapprove
/strict
secret it prompts for Touch ID — so an agent can't overwrite or delete a protected secret without consent. Also keep the server read-only (the default), scope it to a single project by settingLOKALITE_PROJECT
in the server'senv
config, and prefer clients that ask for approval before tool calls. Every MCP access is recorded in the activity log.
There are two independent gates on an agent's get_secret
, and they belong to different systems:
The client gate (above Lokalite). MCP hosts like Claude Code, Cursor, and Windsurf run their own permission check on every tool callbeforethe request ever reaches Lokalite. Depending on the client's mode,mcp__lokalite__get_secret
may be auto-blocked or surfaced for your approval — the agent stops and asks. Lokalite has no say here; this gate is entirely the client's.The Lokalite gate (below the client). Once a call gets through, the per-secret tier (allow
/approve
/block
) and vault-lock state apply, with Touch ID onapprove
/strict
. This is the gate described above.
The two are complementary: the client decides whether the tool may run at all; Lokalite decides whether this secret may be released. A secret can clear the client gate and still hit a Touch ID prompt, or be allowed by the client and refused by a block
tier.
If an agent reports that a get_secret
call was blocked by the permission classifier (not a "vault is locked" error), that is the client gate, and you resolve it in the client — not in Lokalite. In Claude Code, allow the tool by adding it to permissions.allow
in .claude/settings.local.json
(project-scoped, uncommitted):
{ "permissions": { "allow": ["mcp__lokalite__get_secret"] } }
Use mcp__lokalite
to allow the whole server. Weigh this against what you're releasing: allowlisting removes the client's per-call prompt for every secret in the project, so an agent can auto-load production credentials into a shell without you in the loop. Because the client gate can't filter by argument, it can't distinguish prod from dev — if you want a human gate that only fires on sensitive secrets, leave get_secret
out of the allowlist and mark those secrets approve
so Lokalite's Touch ID prompt fires instead.
A lokalite://
reference stands in for a secret's value anywhere an environment variable is configured. References carry no secret material, so they are safe to commit. Three forms:
lokalite://KEY
— project resolved like the CLI does (linked directory, then active project); the project's active environmentlokalite://myproject/KEY
— explicit project, its active environmentlokalite://myproject/staging/KEY
— fully qualified
lokalite run
scans its inherited environment for lokalite://
values and swaps each for the real secret value in the child process's environment before spawning. Pass --refs-only
to skip the bulk injection of project secrets and resolve only the references. Any other environment variable is inherited unchanged.
The primary use case is MCP server configs (~/.claude.json
, a project's .mcp.json
, claude_desktop_config.json
), which otherwise carry tokens in plaintext env
blocks:
{
"mcpServers": {
"github": {
"command": "lokalite",
"args": ["run", "--refs-only", "--", "npx", "-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "lokalite://myproject/GITHUB_TOKEN" }
}
}
}
The MCP host puts the reference in the child environment; lokalite run
replaces it with the real value before the server starts. The value never appears in the config file, the host's context, or the repo.
References are resolved through the Lokalite app (the same broker the MCP server uses), so the per-secret agent-access tiers apply at spawn time — a block
-tier reference is refused, an approve
-tier reference prompts for Touch ID whoever spawned the command — and every read lands in the activity log with agent attribution. Pass --local
to resolve in-process for CI/headless use; approval-tier and blocked secrets are then unavailable when an AI agent is detected.
Resolution fails closed: if any reference is malformed, unknown, or denied, lokalite run
prints the environment variable and the reference (never a value), exits non-zero, and the command is not run.
Values: every secret value is encrypted with AES-256-GCM before being written to disk. The 256-bit vault key is generated on first use and stored in your macOS login keychain — it is never written to the vault file.Metadata: secret names, descriptions, project names, linked paths, and the access activity log are stored unencrypted in~/Library/Application Support/Lokalite/vault.db
. They reveal which services you use, not the credentials themselves; the file relies on home-directory permissions and FileVault.App unlock: the menu bar app requires Touch ID or your device password before showing secrets, and auto-locks after the session timeout. A CLI or MCP request that arrives while the vault is locked triggers the same Touch ID prompt (naming the caller) instead of unlocking silently; if it's declined, the request fails closed.CLI and MCP: while the vault is unlocked, brokered reads don't re-prompt (per-secretapprove
/strict
tiers still do). The CLI's in-process paths (--local
, direct commands) read the vault key from the login keychain without a prompt — there the trust boundary is your unlocked macOS user session, just like with~/.aws/credentials
or.env
files (which Lokalite improves on by encrypting values at rest and logging access).Clipboard: copies are marked withorg.nspasteboard.ConcealedType
so well-behaved clipboard managers skip them, and are auto-cleared after 30 seconds.- Nothing leaves your machine: no cloud, no telemetry.
MIT