{"slug": "show-hn-lokalite-local-secrets-for-clis-and-coding-agents", "title": "Show HN: Lokalite – local secrets for CLIs and coding agents", "summary": "RubenGlez released Lokalite, a local-first secrets manager for macOS that encrypts API keys, tokens, and credentials in an AES-256-GCM vault stored in Apple Keychain, delivering secrets to AI coding agents via MCP without exposing them to chat history or model context. The tool, which supports Claude Code, Cursor, and Windsurf, offers a menu bar app, CLI, and MCP server with Touch ID unlock, per-project environments, and logged access, requiring macOS 14 or later and no cloud accounts or telemetry.", "body_md": "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.\n\n**Project page:** [rubenglez.dev/lokalite](https://rubenglez.dev/lokalite)\n\n**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`\n\nfiles**vs. 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.\n\n**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 on`approve`\n\n/`strict`\n\nsecrets. While the vault is unlocked, reads don't re-prompt — see[Security](#security)**Menu 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`\n\n)**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`\n\nimport/export`.env`\n\nfile or export back to one**Encrypted backup/restore**:`lokalite backup`\n\nwrites a passphrase-encrypted file;`lokalite restore`\n\nreads it back**Shell injection**:`eval $(lokalite shell)`\n\nexports secrets into the current session**Clipboard 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 checks**MCP integration**: expose your vault as tools to Claude Code, Cursor, Windsurf, and any other MCP-compatible agent** Secret references**: commit`lokalite://project/KEY`\n\ninstead of a token in MCP configs;`lokalite run --refs-only`\n\nresolves it at spawn time**Zero runtime dependencies**: no cloud, no telemetry, no vendor lock-in\n\n- macOS 14 or later\n\nCLI, MCP server, and menu bar app via Homebrew:\n\n```\nbrew install RubenGlez/lokalite/lokalite\nbrew install --cask RubenGlez/lokalite/lokalite-app\nlokalite install   # registers the MCP server in ~/.claude.json\n```\n\nIf you don't need the menu bar app:\n\n```\nbrew install RubenGlez/lokalite/lokalite\nlokalite install\n```\n\nIf you already have the CLI installed or don't need it:\n\n```\nbrew install --cask RubenGlez/lokalite/lokalite-app\n```\n\nDownload from the [Releases page](https://github.com/RubenGlez/lokalite/releases):\n\n-\n**CLI**: run the`.pkg`\n\ninstaller, then`lokalite install`\n\n-\n**Menu bar app**: drag the`.dmg`\n\nto 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\n\n`.pkg`\n\nall pass Gatekeeper on first launch. The menu bar app then keeps itself up to date automatically (Sparkle). Every release also ships a`SHA256SUMS`\n\nfile you can verify against the artifacts.\n\n```\n# Initialise a project from the current directory\nlokalite init\n\n# Check vault state (active project, env, secret count, MCP status)\nlokalite status\nlokalite status --json\n\n# View the secret access log (which agent read/changed what, and from where)\nlokalite log\nlokalite log --limit 20\nlokalite log --source cli       # filter by app, cli, or mcp\nlokalite log --agent claude     # filter by the AI agent that accessed the secret\n\n# Add a secret\nlokalite add OPENAI_API_KEY sk-...\nlokalite add OPENAI_API_KEY          # no value: prompts for it, keeping it out of shell history\necho -n \"sk-...\" | lokalite add OPENAI_API_KEY -   # or pipe it via stdin\n\n# Get a secret (prints to stdout, pipeable)\nlokalite get OPENAI_API_KEY\n\n# Copy to clipboard (auto-clears after 30s)\nlokalite copy OPENAI_API_KEY\n\n# List all secrets\nlokalite list\n\n# Filter secrets by name or description (case-insensitive substring)\nlokalite list --search openai\n\n# Update a secret (also supports the prompt/stdin forms shown for `add`)\nlokalite set OPENAI_API_KEY sk-new-...\n\n# Delete a secret\nlokalite delete OPENAI_API_KEY\n\n# Run a command with secrets injected as environment variables\nlokalite run -- npm start\nlokalite run --keys OPENAI_API_KEY,ANTHROPIC_API_KEY -- claude\nlokalite run --refs-only -- npx some-server   # resolve lokalite:// references only (see Secret references)\n\n# Manage projects\nlokalite project add MyProject\nlokalite project list\nlokalite project use MyProject\nlokalite project link [<name>] [--path <dir>]   # link to a directory; defaults to cwd\nlokalite project link --unlink <name>            # remove path association\nlokalite project delete MyProject\n\n# Manage environments\nlokalite env add staging\nlokalite env list\nlokalite env use staging\nlokalite env delete staging\n\n# Import from a .env file\nlokalite import .env\nlokalite import .env --overwrite        # overwrite existing secrets\n\n# Export\nlokalite export --output backup.lk          # encrypted (default), includes everything\nlokalite export --plain --output secrets.json   # plaintext: approve-tier secrets are skipped (named on stderr)\nlokalite export --format env                # .env format, stdout; approve-tier secrets are skipped\nlokalite export --format env --output .env\n\n# Encrypted backup / restore (a project's active environment)\nlokalite backup                             # prompts for a passphrase, writes a timestamped .lokalite file (approve-tier secrets are skipped)\nlokalite backup --output backup.lokalite\nlokalite restore backup.lokalite            # prompts for the passphrase, skips existing secrets\nlokalite restore backup.lokalite --overwrite\n\n# Inject secrets into the current shell session (see security note below)\neval $(lokalite shell)\neval $(lokalite shell --keys OPENAI_API_KEY,ANTHROPIC_API_KEY)\n```\n\nShell injection note:`eval $(lokalite shell)`\n\nmakes secrets visible to all child processes and shows up in`env`\n\noutput for the duration of your session. Use`lokalite run`\n\nto scope secrets to a single subprocess instead.\n\nClick the armadillo icon in your menu bar (or press the global shortcut, default `⌘⇧Space`\n\n) 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.\n\nThe secrets manager is a three-column layout:\n\n**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\n\nOn first launch, an onboarding screen guides you through creating your first project.\n\nRight-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.\n\nBy default `lokalite mcp`\n\n**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`\n\nto open the vault in-process instead (for CI/headless use, where the value then passes through the MCP process).\n\n`lokalite install`\n\nregisters the MCP server with Claude Code automatically (writing `~/.claude.json`\n\n). To register with a different client, pass `--client`\n\n:\n\n```\nlokalite install --client claude          # ~/.claude.json (default)\nlokalite install --client claude-desktop  # ~/Library/Application Support/Claude/claude_desktop_config.json\nlokalite install --client cursor          # ~/.cursor/mcp.json\nlokalite install --client windsurf        # ~/.codeium/windsurf/mcp_config.json\n```\n\nFor any other MCP-compatible agent, add the same server block to its config manually:\n\n```\n{\n  \"mcpServers\": {\n    \"lokalite\": {\n      \"command\": \"lokalite\",\n      \"args\": [\"mcp\"]\n    }\n  }\n}\n```\n\nBy default the server is **read-only** and exposes these tools:\n\n| Tool | Description |\n|---|---|\n`list_secrets` |\nList secret names, categories, and descriptions (values never exposed) |\n`list_projects` |\nList projects and their linked directories (no values); use when no project resolves |\n`list_environments` |\nList a project's environments, marking the active one (no values) |\n`use_environment` |\nSwitch the project's active environment — the one secrets resolve from by default |\n`get_secret` |\nLoad a secret into the agent's shell environment via a one-time handoff — the value is never returned to the model |\n\n`use_environment`\n\nswitches the project's **single active environment**, the same one the menu bar app, the manager, and `lokalite env use`\n\nuse — 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`\n\nargument to `get_secret`\n\ninstead.\n\n`get_secret`\n\ndoes **not** return the secret value. It writes the value to a single-use, owner-only shell script and returns a `source '<path>'`\n\ncommand; 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`\n\non connect describing this flow and telling agents never to print a loaded variable or copy a secret into `.env`\n\n/config/source.\n\nWhen a tool call omits `project`\n\n, 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`\n\nargument 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`\n\nargument (or `LOKALITE_PROJECT`\n\nin the server `env`\n\n) always wins.\n\nPass `--read-write`\n\nto also expose write tools:\n\n```\n{ \"command\": \"lokalite\", \"args\": [\"mcp\", \"--read-write\"] }\n```\n\n| Tool | Description |\n|---|---|\n`add_secret` |\nCreate a new secret |\n`set_secret` |\nUpdate an existing secret's value |\n`delete_secret` |\nPermanently delete a secret |\n\nSecurity 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`\n\ngives it the names), and a sourced value is then visible to processes in that shell. Set a per-secret agent-access tier with`lokalite agent-access <name> allow|approve|block`\n\n(or from the app's secret editor):\n\nblock— off-limits:`get_secret`\n\nrefuses it,`list_secrets`\n\nflags it`[off-limits to agents]`\n\n.Best-effort:blocking an agent depends on detecting it (a process-tree name match), so a renamed or sandboxed agent can evade it. Treat`block`\n\nas a courtesy guard; use`approve`\n\nfor a secret that must not be released without you in the loop.approve— consent-on-read: every read through the app broker — an agent's`get_secret`\n\n, a`lokalite://`\n\nreference, or the CLI's`get`\n\n/`copy`\n\n— 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`\n\nflags it`[approval required]`\n\n. With`--local`\n\n(no app to prompt) it fails closed, like block. Unlike`block`\n\n, this doesnotrely on detection — the prompt fires for every caller.allow— the default.The CLI\n\n`get`\n\n/`copy`\n\nwon'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`\n\nto override for your own use; a`block`\n\nsecret is refused regardless (no override). An`approve`\n\n/`strict`\n\nsecret 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`\n\n, plaintext`export`\n\n, bulk`run`\n\ninjection,`backup`\n\n) skip`approve`\n\nsecrets and name them on stderr. This detection is best-effort (see the`block`\n\nnote above) — it protects the transcript, not a determined agent's own shell; lean on`approve`\n\n/`strict`\n\nconsent for anything that must not be released without you in the loop.Writes are governed too:an agent's`set_secret`\n\n/`delete_secret`\n\n(or CLI`set`\n\n/`delete`\n\n) on a`block`\n\nsecret is refused, and on an`approve`\n\n/`strict`\n\nsecret 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 setting`LOKALITE_PROJECT`\n\nin the server's`env`\n\nconfig, and prefer clients that ask for approval before tool calls. Every MCP access is recorded in the activity log.\n\nThere are **two independent gates** on an agent's `get_secret`\n\n, and they belong to different systems:\n\n**The client gate (above Lokalite).** MCP hosts like Claude Code, Cursor, and Windsurf run their own permission check on every tool call*before*the request ever reaches Lokalite. Depending on the client's mode,`mcp__lokalite__get_secret`\n\nmay 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`\n\n/`approve`\n\n/`block`\n\n) and vault-lock state apply, with Touch ID on`approve`\n\n/`strict`\n\n. This is the gate described above.\n\nThe 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`\n\ntier.\n\nIf an agent reports that a `get_secret`\n\ncall 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`\n\nin `.claude/settings.local.json`\n\n(project-scoped, uncommitted):\n\n```\n{ \"permissions\": { \"allow\": [\"mcp__lokalite__get_secret\"] } }\n```\n\nUse `mcp__lokalite`\n\nto 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`\n\nout of the allowlist and mark those secrets `approve`\n\nso Lokalite's Touch ID prompt fires instead.\n\nA `lokalite://`\n\nreference 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:\n\n`lokalite://KEY`\n\n— project resolved like the CLI does (linked directory, then active project); the project's active environment`lokalite://myproject/KEY`\n\n— explicit project, its active environment`lokalite://myproject/staging/KEY`\n\n— fully qualified\n\n`lokalite run`\n\nscans its inherited environment for `lokalite://`\n\nvalues and swaps each for the real secret value in the child process's environment before spawning. Pass `--refs-only`\n\nto skip the bulk injection of project secrets and resolve only the references. Any other environment variable is inherited unchanged.\n\nThe primary use case is MCP server configs (`~/.claude.json`\n\n, a project's `.mcp.json`\n\n, `claude_desktop_config.json`\n\n), which otherwise carry tokens in plaintext `env`\n\nblocks:\n\n```\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"command\": \"lokalite\",\n      \"args\": [\"run\", \"--refs-only\", \"--\", \"npx\", \"-y\", \"@modelcontextprotocol/server-github\"],\n      \"env\": { \"GITHUB_TOKEN\": \"lokalite://myproject/GITHUB_TOKEN\" }\n    }\n  }\n}\n```\n\nThe MCP host puts the reference in the child environment; `lokalite run`\n\nreplaces it with the real value before the server starts. The value never appears in the config file, the host's context, or the repo.\n\nReferences 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`\n\n-tier reference is refused, an `approve`\n\n-tier reference prompts for Touch ID whoever spawned the command — and every read lands in the activity log with agent attribution. Pass `--local`\n\nto resolve in-process for CI/headless use; approval-tier and blocked secrets are then unavailable when an AI agent is detected.\n\nResolution fails closed: if any reference is malformed, unknown, or denied, `lokalite run`\n\nprints the environment variable and the reference (never a value), exits non-zero, and the command is not run.\n\n**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`\n\n. 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-secret`approve`\n\n/`strict`\n\ntiers still do). The CLI's in-process paths (`--local`\n\n, 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`\n\nor`.env`\n\nfiles (which Lokalite improves on by encrypting values at rest and logging access).**Clipboard**: copies are marked with`org.nspasteboard.ConcealedType`\n\nso well-behaved clipboard managers skip them, and are auto-cleared after 30 seconds.- Nothing leaves your machine: no cloud, no telemetry.\n\nMIT", "url": "https://wpnews.pro/news/show-hn-lokalite-local-secrets-for-clis-and-coding-agents", "canonical_source": "https://github.com/RubenGlez/lokalite", "published_at": "2026-07-21 09:14:11+00:00", "updated_at": "2026-07-21 09:53:23.294792+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools", "ai-safety"], "entities": ["RubenGlez", "Lokalite", "Claude Code", "Cursor", "Windsurf", "Apple Keychain", "MCP"], "alternates": {"html": "https://wpnews.pro/news/show-hn-lokalite-local-secrets-for-clis-and-coding-agents", "markdown": "https://wpnews.pro/news/show-hn-lokalite-local-secrets-for-clis-and-coding-agents.md", "text": "https://wpnews.pro/news/show-hn-lokalite-local-secrets-for-clis-and-coding-agents.txt", "jsonld": "https://wpnews.pro/news/show-hn-lokalite-local-secrets-for-clis-and-coding-agents.jsonld"}}