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 mehen.dev/install | sh
zsh, ~/code/app
$ mehen ls
name              provider   bound to                 mode
openai-personal   OpenAI     api.openai.com           placeholder
stripe-live       Stripe     api.stripe.com           relay
gmail-alex        Google     gmail.googleapis.com     oauth

$ mehen run -- bun dev
mehen  OPENAI_API_KEY=mehen://openai-personal/4f91c2
mehen  allowed  POST api.openai.com/v1/chat/completions  212 ms
mehen  held     POST api.stripe.com/v1/refunds  waiting for Alex
mehen  allowed  POST api.stripe.com/v1/refunds  approved with passkey

The problem

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.

  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 link
    mehen.dev/i/7h3qexpires in 15 min
    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 approval
    POST api.stripe.com/v1/refundsap_7f3k2q
    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 bodyapplication/x-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.

mehen.dev/vault/alex alex@studio

Credentials

New credential
NameProviderBound hostsMethodsMode
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
TimeAgentCredentialDestinationDecision
23:41:07claude-code@studioopenai-personalPOST api.openai.com/v1/chat/completionsAllowed
23:40:52cron/nightly-reportstripe-liveGET api.stripe.com/v1/balanceAllowed
23:39:18codex@laptopstripe-livePOST api.stripe.com/v1/refundsHeld for approvalwaiting for Alex's passkey
23:37:03claude-code@studioopenai-personalPOST api.openai.com/v1/filesRefusedoutside the binding. Fix: mehen.dev/a/7KQ2
23:36:21agent/inboxgmail-alexgmail sendAllowed
23:35:40codex@laptopopenai-personalPOST evil-proxy.example/v1/chatRefusednot a bound host. Nothing was sent. Fix: ask a human at mehen.dev/a/7KQ2

The CLI

Placeholders in, answers out.

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

zsh, ~/code/app
$ mehen request stripe
mehen  intake link sent to Alex: mehen.dev/i/7h3q  expires in 15 min
mehen  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  212 ms
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 in the box

Small rules, kept every time.

  • Relay

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

  • Placeholders

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

  • Bindings

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

  • Approvals

    Risky calls wait for a passkey. Agents cannot approve.

  • Intake links

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

  • OAuth connections

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

  • Enrolled machines

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

  • Audit

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

  • Every refusal says the fix

    No dead ends: each "no" carries the link to open or the command to run.

Works with

Any agent, any job.

  • Claude Code
  • Codex
  • Cursor
  • Any MCP client
  • Cron jobs
  • CI
  • The mehen CLI
mehen

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.