mehen.dev

The key never leaves.

A credential vault and proxy for AI agents. Agents get capabilities, never secrets.

Mehen makes the call for the agent, returns the response scrubbed, and asks a human before anything risky goes out.

$ curl -fsSL https://mehen.dev/install | sh
zsh, ~/code/app
$ mehen run -- bun dev
STRIPE_SECRET_KEY=mehen://stripe-live/b07e1a
  ready on http://localhost:3000
allowed  GET api.stripe.com/v1/balance  188 ms
held     POST api.stripe.com/v1/refunds
         waiting for Alex's passkey
allowed  POST api.stripe.com/v1/refunds
         approved. The key never left.

Get started

Give your agent one prompt.

You need an invite from a workspace owner first. Your agent does the setup. You, or a workspace owner, approve the machine with a passkey.

Set me up with Mehen by following https://mehen.dev/setup.md

Or install it yourself: curl -fsSL https://mehen.dev/install | sh, then mehen setup. Read the setup guide.

Where keys leak today

A key the agent can read is a key that leaks.

Once the model has seen the value, masking it in the output changes nothing. It is already in too many places.

  • .envRead by every tool with file access.
  • the transcriptStored with the provider, forever.
  • the logsRequest bodies, tool results, stack traces.
  • shell historyOne export, kept for years.
  • prompt injectionA page that asks nicely gets the key.

How it works

Ask, use, approve.

The key crosses from the human to the vault once. After that the agent only ever handles a placeholder or a scrubbed response.

  1. 01

    Ask

    The agent needs a key, so it asks. A human gets an intake link and types the key into the vault's page, never into the chat.

    Intake linkexpires in 15 min
    mehen.dev/i/7h3q
    Stripe secret keysk_live_••••••••••••••••
    Typed by Alex on alex-laptop. Stored in the vault.
  2. 02

    Use

    The vault makes the call and returns the response, scrubbed. Or mehen run hands your program a placeholder that becomes the real key only on the way to the right host.

    Environment under mehen run
    OPENAI_API_KEY=mehen://openai-personal/4f91c2
    STRIPE_SECRET_KEY=mehen://stripe-live/b07e1a
    Swapped for the real value at api.openai.com and api.stripe.com only.
  3. 03

    Approve

    A write, or anything in production, waits for a person who sees the exact request and approves it with a passkey. Agents cannot approve.

    Waiting for approvalap_7f3k2q
    POST api.stripe.com/v1/refunds
    Approve with passkeyDecline
    Expires in 9:41. The agent waits.

Approvals

Humans approve what they see.

The exact request, the binding it runs under, and why it waited. One passkey tap, and the agent carries on. Nothing is sent until then.

  • Rules decide what waits: writes, production, new hosts.
  • Approve from the vault's page, phone or laptop.
  • Every decision lands in the crossings log.
Waiting for approval ap_7f3k2q

Claude Code on alex-laptop wants to POST to api.stripe.com/v1/refunds

Credential
stripe-live
Binding
api.stripe.com, /v1/*, GET POST
Why it waits
write production
Requested
just now, 14:32:07
Request bodyx-www-form-urlencoded
payment_intent=pi_3Mt7XoLkdIwHu7ix0kjN2wKg
amount=4200
reason=requested_by_customer
metadata[ticket]=SUP-1187
Expires in 9:41

The vault

Every secret has a destination.

Each credential is bound to hosts, paths and methods. Sent anywhere else, it is refused, and the refusal says the fix.

Credentials

New credential
  • openai-personalOpenAIapi.openai.comPOST /v1/chat/* POST /v1/embeddingsplaceholder
  • stripe-liveStripeapi.stripe.comGET /v1/* POST needs approvalrelay
  • gmail-alexGooglegmail.googleapis.comread sendoauth
  • github-ciGitHubapi.github.comGET /repos/* POST /repos/*/statusesrelay

Crossings live

this evening
  • claude-code@studioopenai-personalPOST api.openai.com/v1/chat/completionsAllowed
  • cron/nightly-reportstripe-liveGET api.stripe.com/v1/balanceAllowed
  • codex@laptopstripe-livePOST api.stripe.com/v1/refundsHeldwaiting for Alex's passkey
  • claude-code@studioopenai-personalPOST api.openai.com/v1/filesRefusedoutside the binding. Fix: mehen.dev/a/7KQ2
  • agent/inboxgmail-alexgmail sendAllowed
  • codex@laptopopenai-personalPOST evil-proxy.example/v1/chatRefusednot a bound host. Nothing was sent. Fix: ask a human at mehen.dev/a/7KQ2

The CLI and MCP

Placeholders in, answers out.

Run your program under mehen run and SDKs work unchanged. Over MCP, the agent names the capability, never the value. Every refusal says the fix.

zsh, ~/code/app
$ mehen request stripe
mehen  intake link sent to Alex: mehen.dev/i/7h3q
mehen  expires in 15 min. The key is typed into the
       vault's page, never into this chat.

$ mehen connect gmail
mehen  opening Google sign-in for Alex
mehen  connected  gmail-alex  read, send
       tokens stay in the vault

$ mehen run -- bun dev
mehen  OPENAI_API_KEY=mehen://openai-personal/4f91c2
  ready on http://localhost:3000
mehen  allowed  POST api.openai.com/v1/chat/completions
mehen  refused  openai-personal is bound to api.openai.com.
       Nothing was sent.
       fix: ask a human at mehen.dev/a/7KQ2
MCP, tool call from the agent
// the agent names the capability, never the value
{
  "tool": "vault_fetch",
  "arguments": {
    "item": "openai-personal",
    "method": "POST",
    "url": "https://api.openai.com/v1/chat/completions",
    "body": { "model": "gpt-5",
              "input": "Summarise the log" }
  }
}

// the response comes back scrubbed
{
  "status": 200, "scrubbed": true,
  "body": { "output": "Six crossings, two refused,
                       one waiting." }
}

$ mehen enrol
mehen  this machine is quiet-heron-lamp
mehen  match the words at mehen.dev/machines to approve it

Everything the vault does

Small rules, kept every time.

  • Bindings

    Hosts, paths and methods per credential. Anywhere else is refused.

    stripe-liveapi.stripe.com/v1/*GETPOST needs approval
    openai-personalapi.openai.com/v1/chat/*/v1/embeddingsPOST
  • Relay

    The vault makes the HTTP call and returns the response, scrubbed.

    200 in 188 mskey never left
  • Placeholders

    mehen run gives programs a stand-in that becomes real only at the right host.

    OPENAI_API_KEY=mehen://openai-personal/4f91c2
  • Approvals

    Risky calls wait for a passkey. Agents cannot approve.

    held POST api.stripe.com/v1/refundswaiting for Alex
  • Intake links

    The human types the key into the vault's page, never into the chat.

    mehen.dev/i/7h3qexpires in 15 min
  • OAuth connections

    "Connect my Gmail": tokens go to the vault, the agent gets the capability.

    connected gmail-alexread, send
  • Enrolled machines

    Each device holds its own key, approved by matching fingerprint words.

    quietheronlamp
  • Audit

    Who, which credential, where, when, what was decided. Every time.

    23:39:18 codex@laptop stripe-liveheld POST /v1/refunds
  • Every refusal says the fix

    No dead ends: each "no" carries the link to open or the command to run. Nothing is sent in the meantime.

    refused openai-personal is bound to api.openai.com. Nothing was sent.fix: ask a human at mehen.dev/a/7KQ2
  • Works with

    • Claude Code
    • Codex
    • Cursor
    • Any MCP client
    • Cron jobs
    • CI

Keep the key. Give the capability.

Install in a minute. Free for one person; a link covers the rest.

The serpent coils round the sun, and nothing gets past.