rosostolato

Lockbox

Community rosostolato
Updated

An MCP server and secure input broker: an AI agent can place a credential without ever receiving it, while a trusted human-controlled UI authorizes where it goes.

Lockbox

Let your AI agent set up API keys without ever seeing them.

CILicense: Apache 2.0Node 20+

You ask Claude, Cursor or any MCP client to "put the Stripe key in our .env" — or in SecretManager, Vercel, your own API. The agent describes where it goes. You read that in Lockbox'sown window and paste the value there. The key goes straight to the destination and neverenters the chat, the prompt or the logs.

  1. The agent describes the destination. A file, a program such as gcloud, or an HTTPSrequest, with {{secret}} where the value goes. The tool has no field that could carry avalue.
  2. You check and paste. Lockbox's local window shows where the key goes in one line, warnsabout anything new or odd, and checks the key's format. The exact request is one clickaway.
  3. Lockbox sends it. The agent only learns STORED and a reference. It never sees the key.
claude mcp add lockbox -e LOCKBOX_ENV_ALLOWED_ROOTS="$PWD" -- npx -y lockbox-mcp serve

Then ask your agent: "store my Stripe test key in .env".

Install

Lockbox is a stdio MCP server, so you do not run it yourself — your MCP client starts it, thesame way it starts any other npm-published server. Requires Node 20+.

Claude Code

cd ~/some/project
claude mcp add lockbox -e LOCKBOX_ENV_ALLOWED_ROOTS="$PWD" -- npx -y lockbox-mcp serve

Add -s project to record it in the repository's .mcp.json instead of your own config.

Any other client (Claude Desktop, Cursor, Windsurf, VS Code, Zed…)

Drop this into the client's MCP configuration — the mcpServers block is the same shapeeverywhere:

{
  "mcpServers": {
    "lockbox": {
      "command": "npx",
      "args": ["-y", "lockbox-mcp", "serve"],
      "env": {
        "LOCKBOX_ENV_ALLOWED_ROOTS": "/absolute/path/to/your/project"
      }
    }
  }
}

Prefer a pinned install to an ephemeral one:

npm install -g lockbox-mcp
# then use `lockbox-mcp serve` as the command, with no npx

The package is published to npm, unscoped, as lockbox-mcp.

Set LOCKBOX_ENV_ALLOWED_ROOTS. File destinations refuse to write outside thosedirectories, and they default only to the server's working directory. Everything else isoptional — see Configuration.

Destinations behind a login (Google Secret Manager, Vercel, GitHub) are reached through theirown CLI, which uses the login you already have — gcloud, vercel, gh. SeeDestinations.

First run

Ask your agent for something like "store my Stripe test key in .env". Lockbox opens itswindow on your machine; the agent does not receive that link. The band at the top tells youthe environment at a glance, and the field tells you whether the key looks right before yousend it.

Catches the wrong key. A test key headed for production gets flagged in the window. The check runs locally and never shows the value.

Confirms before sending. Nothing is sent until you type where it goes: the host, the program or the file.

Tells the agent only "stored". Lockbox wipes the value and hands back a reference such as the file and variable.

Enter continues and Esc cancels. The link works once: if anythingopened it before your browser did, Lockbox shows an alarm and a Cancel button instead of therequest. Lockbox's stderr carries structured audit JSON; nothing else is expected of you in theterminal.

What Lockbox solves

It removes an entire class of failures caused by the agent knowing the secret.With Lockbox in the loop, a credential does not pass through:

  • LLM prompts or conversation history
  • MCP tool arguments or tool results
  • agent memory or generated code
  • shell command arguments or process argv
  • logs, debug traces or telemetry
  • URLs
  • model-visible command output

What Lockbox does not solve

Lockbox does not make an AI agent trustworthy, and it is not "safe AI". It does notguarantee that the agent picked the right destination, that it understood you, that itis free of prompt injection, that the destination is itself secure, that your machine isuncompromised, or that a credential cannot be misused later by software that legitimatelyreceives it.

There are two separate problems here:

Question Lockbox's answer
Should the agent know the secret? No.
Should the agent decide alone where the secret goes? Not without human authorization.

Lockbox answers those two. It does not claim to answer the rest.

Trust model

Trusted with the credential value:

  The human at the keyboard
  Lockbox's secure input UI       (loopback only, in your control)
  Lockbox's secure input broker   (this process)
  The destination you approved    (a file, a program, an HTTPS endpoint)

NOT trusted with the credential value:

  The LLM
  The agent / MCP client
  The conversation
  The prompt and any repository content it read
  Generated code
  Logs, telemetry, crash reports

Lockbox's runtime dependency list is one package (zod, for input validation). The MCPprotocol is implemented in-tree rather than through the official SDK, which would add ~90packages — including HTTP servers and an OAuth stack — to a process that holds plaintextcredentials.

This diagram does not claim the trusted components are invulnerable. It says where thecredential is allowed to exist. Lockbox is security-sensitive software: if Lockbox itself ismalicious or compromised, the boundary is gone. Its source, dependencies and releasesdeserve the scrutiny you would give any credential-handling tool.

The two flows

The secret flow — the human's path, which the model cannot observe:

Human ─▶ Lockbox secure UI (127.0.0.1) ─▶ Broker ─▶ approved definition ─▶ Destination

The agent flow — everything the model sees:

LLM ─▶ MCP client ─▶ Lockbox MCP server ─▶ non-sensitive result metadata

The MCP tool schema has no property capable of carrying a credential. That is structural,not a prompt instruction: there is no value, secret_value, password, token,content or raw_secret field to abuse, closed schemas reject unknown properties, andarguments are screened for credential-shaped values before they are parsed.

What the agent calls

{
  "destination": "custom",
  "name": "STRIPE_SECRET_KEY",
  "target": {
    "definition": {
      "kind": "command",
      "label": "Google Secret Manager",
      "executable": "/opt/homebrew/bin/gcloud",
      "args": ["secrets", "versions", "add", "stripe-secret-key",
               "--project", "acme-prod", "--data-file=-"],
      "stdin": "{{secret}}"
    }
  },
  "environment": "production",
  "description": "Stripe production API key"
}

Lockbox replies with a request_id, a risk classification and the normalized destination —and opens its own authorization window on your machine. The agent polls secret_status.destination: "saved:<name>" reuses a destination the human saved earlier.

The agent does not get the authorization link. That link is a capability: anythingholding it can complete the human's half of the flow, and an agent with a shell or an HTTPtool is precisely the threat model. Lockbox hands it straight to your browser. SetLOCKBOX_DISCLOSE_AUTHORIZATION_URL=true if your setup needs the agent to relay the link (forexample, a remote or headless session) — and understand that this lets a compromised agentauthorize its own request.

The link works once. Opening it trades it for an HttpOnly; SameSite=Lax session cookiescoped to that request and redirects to an address with no token in it; every later page andform needs the cookie. A second opening of the same link gets an alarm page ("This link wasalready opened") with a Cancel button, never the request. So a copy left in browser history,a process list or a log is dead once you open it, and one used by someone else first isvisible to you.

Logs never hold the identity phrase, and hold the link only as a last resort. MCP hostssave server stderr to files any process running as you can read (Claude Desktop under~/Library/Logs/Claude/, Claude Code under its cache directory), so the audit record carriesa short fingerprint of the link instead. The full link is logged only when no browser couldbe opened (LOCKBOX_OPEN_BROWSER=false, too many windows, or the opener failed), because theoperator then has no other way in.

Tool Purpose
secret_store Create a credential request. Returns non-sensitive metadata and a request id.
secret_status Poll a request. Never returns credential material.
secret_cancel Cancel a pending request; any entered value is destroyed.
secret_revise Invalidate an authorization and start a new one. Nothing is edited in place.
secret_destinations The definition contract, worked examples, and the user's saved destinations.

What the human sees

Stage A shows the credential name and one destination line — the definition's label andits host, program or file — plus a line for anything worth a second look: a host neverused before, a host spelled with non-ASCII characters, an IP address instead of a name, aprogram outside the system folders, a saved destination that changed. The full request (theURL, every header and the body; or the program, each argument and stdin; or the file andits content) sits behind "See exactly what will be sent", closed by default, with thecanonical JSON and its hash. Every custom destination needs a second confirmation in StageB, after entry and before anything is sent: the human types the host, the program name orthe file (.env:API_TOKEN for a variable), and the server checks it. This is an attentioncheck, not authentication: the expected text is on the page. What keeps a second party outof Stage B is the session cookie. The value is never displayed back.

POSTs are refused unless they come from Lockbox's own page (Origin and Sec-Fetch-Site), andevery response sets Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy tosame-origin. The credential field asks browser and third-party password managers not tosave it.

The page runs one inline script under a per-response CSP nonce. It describes the value'sshape (length, a live/test marker against the destination's environment, straywhitespace), counts down the expiry and maps Esc to Cancel. The CSP gives it nonetwork access, and Cancel is a separate form, so cancelling never sends the value. WithoutJavaScript the page still works, minus those three aids.

The page the human reads and the operation the executor performs are the same immutableobject — there is no separate "display destination". Any change to destination, project,secret name, operation, write mode or adapter invalidates the authorization and requires anew one.

Destinations

There are no built-in destinations. The agent writes a definition — JSON with{{secret}} exactly once where the value goes, and optionally {{name}} for the credentialname — and the human approves it as rendered. Three kinds:

Kind What it does Rules
file Writes a local file. Mode env sets one variable in a .env and keeps every other line; create and replace write the whole template. Inside LOCKBOX_ENV_ALLOWED_ROOTS; .env / .env.<suffix> names only by default; no symlinks; atomic 0600; git-tracked files refused, and a new file must be git-ignored.
command Runs a program with fixed arguments and hands it the value on stdin. Absolute path, pinned by real path and SHA-256 at approval and re-checked before it runs; no shell; minimal environment; {{secret}} never in an argument; timeout. The agent learns only the exit status.
http Sends one HTTPS request. https only; POST/PUT/PATCH; {{secret}} only in the body or a header value, never the URL; host resolved once and pinned; private and metadata addresses refused by default; redirects not followed. The agent learns the status and an optional reference read from the response, never the response.
// A .env variable
{ "kind": "file", "label": "Project .env", "path": ".env",
  "template": "{{name}}={{secret}}", "mode": "env" }

// Google Secret Manager, through gcloud's own login
{ "kind": "command", "label": "Google Secret Manager", "executable": "/opt/homebrew/bin/gcloud",
  "args": ["secrets", "versions", "add", "stripe-secret-key", "--project", "acme-prod", "--data-file=-"],
  "stdin": "{{secret}}" }

// An HTTPS API that needs no other token
{ "kind": "http", "label": "Internal secrets API",
  "url": "https://secrets.example.com/v1/projects/acme/secrets", "method": "POST",
  "headers": { "Content-Type": "application/json" },
  "body": { "type": "json", "value": { "name": "{{name}}", "value": "{{secret}}" } },
  "reference": "$.id" }

Saved destinations. At Stage B the human can tick Save as a trusted destination andname it; it is saved only if the write succeeds. Next time the agent asks forsaved:<name>, and the window reads "Saved · name · host". Each entry is pinned to theSHA-256 of its canonical JSON: if the file is edited, the window says Changed since you savedit and reviews it as new. The store lives at $XDG_CONFIG_HOME/lockbox/destinations.json(mode 0600), written only by Lockbox; the window can forget an entry.

What is not covered yet. An HTTPS API that needs its own token besides the credential(Vercel, GitHub, Doppler REST) is reached through its CLI instead: the agent cannot supplythat token, and Lockbox does not ask for two values in one request. Firestore has no CLI thatwrites a single field cleanly, so it has no recipe today.

Security assumptions and limitations

Stated plainly, because a security tool that oversells itself is worse than none:

  • The broker process sees the secret. That is the point: something must, or storage isimpossible. The guarantee is that only the minimal trusted transport and destinationcomponents do.
  • Memory erasure is best-effort. SecretBuffer wipes the exact Buffer it owns, andthe HTTP body is percent-decoded into buffers Lockbox wipes too. But any conversion to aJavaScript string — which writing a .env line requires — creates an immutable copy thatV8 may keep until garbage collection, and that copy cannot be wiped. Lockbox minimizes thoseconversions rather than pretending they do not happen.
  • The UI is loopback HTTP. Any process running as your user on your machine can reachit, and any such process could also imitate it. Lockbox refuses to bind anywhere butloopback. Request pages show a per-process identity phrase, but it is no longer logged,so today there is no second channel to compare it against: treat it as decoration, notas anti-spoofing. Withholding the link and making it single-use raise the bar; they donot stop a process that wins the race to the link, reads the browser's memory, or bindsits own lookalike port.
  • An agent with an unrestricted shell is out of reach. Running as you, it can readfiles Lockbox writes (including a .env destination), browser state and MCP logs. No pagedesign fixes that. Run the agent sandboxed — for example Claude Code's sandboxed Bash,with loopback and the MCP log directories outside what it may reach.
  • Lockbox sends where you approved, not where it is wise to. An agent writes thedefinition, so a hijacked agent can propose an attacker's endpoint. Lockbox shows the hostfirst, flags it when it is new or disguised, and makes you type it — but a human whoapproves what they did not read has approved it.
  • A definition is only as safe as what it runs. A command destination runs theprogram you approved with the value on stdin; what that program then does with it isoutside Lockbox.
  • Timeouts are enforced by Lockbox. HTTP requests and commands are cut off atLOCKBOX_ADAPTER_TIMEOUT_SECONDS, and a command is killed with SIGKILL.
  • Preflight is best-effort. A provider that is unreachable at preflight is reported asunavailable rather than guessed at.
  • Crash semantics. A crash between the provider write and the response can leave acredential written with no local record of success. Lockbox reports the request as failed;the destination is the source of truth.

Local development

git clone https://github.com/rosostolato/lockbox-mcp && cd lockbox-mcp
npm install
npm run check      # typecheck + lint + format + tests
npm run build

To point a client at your checkout, use node /path/to/lockbox-mcp/dist/index.js as thecommand instead of npx.

Configuration

Configuration is read from Lockbox's own environment — never from tool arguments, so an agentcannot relax a policy:

Variable Default Meaning
LOCKBOX_REQUEST_TTL_SECONDS 300 Request expiry.
LOCKBOX_ADAPTER_TIMEOUT_SECONDS 30 Upper bound on one destination write.
LOCKBOX_STAGE_B_FOR_MEDIUM true Require confirmation for medium-risk operations.
LOCKBOX_UI_HOST / LOCKBOX_UI_PORT 127.0.0.1 / ephemeral Secure UI bind address. Loopback only; anything else refuses to start.
LOCKBOX_OPEN_BROWSER true Open the authorization window automatically.
LOCKBOX_DISCLOSE_AUTHORIZATION_URL false Return the authorization link to the agent.
LOCKBOX_ENV_ALLOWED_ROOTS current directory Roots file destinations may write inside.
LOCKBOX_ALLOW_GIT_TRACKED_ENV false Permit writing into a git-tracked, or new and un-ignored, file.
LOCKBOX_ENV_ALLOW_ANY_FILENAME false Permit file names other than .env / .env.<suffix>.
LOCKBOX_CUSTOM_DESTINATIONS save save lets the human save approved destinations; once approves every request on its own.
LOCKBOX_CUSTOM_KINDS http,command,file Kinds an agent may define.
LOCKBOX_CUSTOM_HTTP_HOSTS any Comma-separated allowlist of HTTPS hosts; empty means any host, still reviewed.
LOCKBOX_CUSTOM_HTTP_ALLOW_PRIVATE false Permit hosts that resolve to loopback, private or link-local addresses.
LOCKBOX_CUSTOM_STORE_PATH $XDG_CONFIG_HOME/lockbox/destinations.json Where saved destinations live.

Tests

npm test                # everything
npm run test:security   # the adversarial suite only
npm run check           # what CI runs

The security suite is a product requirement, not a nicety. It contains canary-leakagedetection across every observable channel, malicious-agent tests, prompt-injectionfixtures, TOCTOU and replay tests, 100-way concurrency stress, race conditions, crashpaths (including a real SIGKILL mid-write), provider-failure simulation, UI checks andfuzzing. A release is blocked if any canary leaks, any authorization bypass succeeds, anypost-approval mutation succeeds, any completed request is replayable, any secret crosses arequest boundary, any raw provider error reaches MCP, or any high-risk operation skipsconfirmation.

See docs/SECURITY_MODEL.md for the invariant-to-test map.

Project status

Version 0.1.0, built to SPEC.md, which stays in the repository as theauthoritative description of the intended behaviour. The implementation is TypeScript onNode; it was ported from an equivalent Python implementation, which remains in the githistory. Every substantial module and testcites the section it implements, so a reviewer can check the code against the requirementrather than against a summary of it.

The MVP is complete and the full suite — including the adversarial one — passes. Whatremains before anyone should rely on it in anger: independent review, human-factor testingof the confirmation UI (SPEC.md §35), and signed release artefacts (§43).

Contributing

Security is the product here, so the bar for changes is specific rather than bureaucratic:

  • A change that touches credential handling, authorization or the MCP surface needs a testthat attempts to break the invariant it affects, not only one that shows it working.
  • Never weaken a security test to make a suite pass. If a test reveals an architecturalflaw, the architecture is what changes.
  • New runtime dependencies in the core are opposed by default. The broker is the trustedcomputing base for credential material; provider SDKs belong behind an optional extra.
  • Run npm run check before opening a pull request.

Found a vulnerability? Please report it privately through GitHub's security advisoriesrather than opening a public issue.

License

Apache License 2.0 © 2026 Eduardo Rosostolato.

MCP Server · Populars

MCP Server · New

    xerj-org

    XERJ

    AI search engine and MCP server for bots and agents. One Rust binary indexes code, docs, logs, JSONL/CSV and PDFs for BM25 full-text, vector and hybrid search, code search without grep, agent memory and RAG. Elasticsearch-compatible API; Corpus Hub serves hundreds of reference memory corpora.

    Community xerj-org
    storytold

    FilmCraft

    FilmCraft: An open-source, nonlinear video editor written by scratch in pure Rust

    Community storytold
    feder-cr

    AIHawk

    An AI web agent that applies to jobs for you while you sleep.

    Community feder-cr
    danielgallen

    Burp Suite MCP Server Extension

    MCP Server for Burp

    Community danielgallen
    SchmidtDSE

    MCP Data Server

    Duckdb-based MCP server for datasets

    Community SchmidtDSE