mehendocs

ReferenceThe skill

Mehen#

What do Claude and pi agents load?

On this page

Credentials live in a workspace (a person's own, a company's, a client's) in the vault at https://app.mehen.dev (unless MEHEN_VAULT_URL names another). This machine may be enrolled in several workspaces, with its own key in each; its owner and each workspace's owners decide what it may use there. You use credentials through mehen or the mehen mcp tools. Relay and placeholder modes keep credential values off the agent's machine. Explicitly allowed lease mode delivers values to the child process. Requests marked for approval by policy, and protected runs, need an eligible human's passkey, given on the workspace's pages under https://app.mehen.dev/w/<slug>/. Never read, print or store a value yourself. Full guide: mehen guide.

Use --json whenever a command supports it and a program reads the result; mehen run, mehen env set, mehen init and mehen mcp do not accept it. The human lines are for people; --json is what you read, and what an app that draws a card from your call reads. If MEHEN_SESSION is already set in your environment, leave it: your harness chose the label the human sees. Otherwise prefix each command with MEHEN_SESSION=<label>, a short label for this conversation (letters, digits, .:_-, up to 120), so the human sees which session asked; an export may not survive between harness calls.

Not set up yet?#

If mehen is missing or mehen status --json lists no state: "active" workspace, follow https://docs.mehen.dev/setup.md. You need the workspace slug from the human (the <slug> in https://app.mehen.dev/w/<slug>/, printed on its Connect an agent page); never guess one. The same page adds a second workspace to a machine that already has one.

Never#

Workspace rules#

The ask block#

Whenever a human must open a link, mehen prints one block and --json carries one ask object. Every command that hands over a link prints the same shape: enrol, request, connect, wait, pending, policy, and every refusal that carries a link (approval_required, needs_reauth, frozen, device_pending).

Human needed: approve a request
  open:    https://app.mehen.dev/w/acme/a/apr_7Hq2
  what:    Send an email to sam@example.com, from gmail-personal (you@example.com)
  check:   the page shows code K7Q2MX
  expires: in 58 min (17:12)
  then:    mehen wait apr_7Hq2

ask in --json: kind (enrol, intake, connect, reconnect, approval, policy, unfreeze), id, url, title, what, check ({ label, value } or null; the label is fingerprint, code, account or asked by), expiresAt (ISO or null), account, requestedBy ({ device, session }), context ({ origin, workspace: { id, slug, name } | null, device }: the enrolment that raised it) and wait (the command to run next). next is still there for anything that only knows next. An enrolment ask also carries intent: { workspace, team } and claimed: boolean; its context.workspace and context.device remain null until activation. Describe intent.workspace as the requested workspace, not an authenticated enrolment.

Write this in chat, the same words every time, on every channel:

**Needs you in the vault:** approve my-laptop
Open: https://app.mehen.dev/w/acme/e/ABCD1234
The page must show the fingerprint **5NZV DP67 PXT0 DB6E**. If it differs, do not approve.
Valid until 18:14. I will wait.

The first line is ask.title and the name from ask.what; Open: is ask.url; the third line is ask.check (for an approval: "Tick the binding code K7Q2MX, then read the whole request before you approve."; for a connection: "Sign in as you@example.com"; an intake's check is asked by, this machine and session, which the page must say: relay it too); the last line is ask.expiresAt. Never shorten or hide check.value: it is what the human compares with the page. Say which workspace the ask is for: use ask.context.workspace.slug for an enrolled device, or the explicitly unverified ask.intent.workspace for an enrolment; never dereference a null context.workspace. Where a harness draws a card from your mehen call, the card already shows the link, the check and a countdown, so the harness may tell you to keep only the first line and "I will wait".

The link is always on the vault's own origin under the workspace's slug. If mehen prints warning: The vault returned a link that is not on <vault>, it refused to show a link that was somewhere else; do not go looking for it. Check MEHEN_VAULT_URL and tell the human.

On a terminal the block also draws a QR code of the link between open: and what:, so a human can point a phone at the screen. --no-qr or MEHEN_QR=0 turns it off; it never appears off a TTY or in --json.

Do#

  1. Check the machine: mehen status --json (MCP vault_status; mehen whoami is the same command) lists every workspace this machine may use. None active: follow https://docs.mehen.dev/setup.md. In short: get the slug from the human, run mehen enrol --workspace <slug> --json, which prints the ask and exits 0, give the human the link and the full fingerprint (the page hides one group of it and the human types that group from your message), then mehen wait enr_... --json from any shell until it is active; a timeout (exit 4) means wait again, and waiting keeps the same link alive. The key is kept between attempts, so the fingerprint and the four words stay the same; a terminal expired means run mehen enrol --workspace <slug> --json again and relay the new link. The human accepts their own machine with their passkey; a team's machine (--team <team-slug>) is admitted by a workspace owner or admin. If the vault activates the machine other than as asked, mehen attempts to revoke the new device, deletes the key and writes nothing; the error says whether the revocation succeeded, and names the device if not: stop and tell the human, with that device id. When something fails and the reason is not plain (a proof_invalid, a network error, a key file), mehen doctor --json (MCP vault_doctor) checks everything at once and names the fix.
  2. Find what exists: mehen ls, mehen show <slug> (MCP vault_list, vault_show). Check account and say which account will act. Always name the slug; if two items could serve, ask the human which one. mehen templates [id] lists every template and OAuth provider with what each allows, holds and refuses, offline.
  3. Ask for something new: mehen request <kind> --name <slug> --provider <template> --reason "<why>" --json (MCP vault_request). Templates (kind api-key): AI openai, anthropic, google-gemini, mistral, groq, openrouter, together, ollama-cloud; code github-pat, snyk, vercel, npm; work linear, slack-bot, notion (send Notion-Version yourself), gitbook, fathom; observability sentry, posthog, betterstack-uptime; infrastructure cloudflare-api-token, hetzner-cloud, scaleway, neon; customers and email stripe, hubspot-private-app, intercom, resend, postmark-server, loops; search and media brave-search, serpapi, semrush, firecrawl, browserbase, elevenlabs, heygen. Kind oauth-client: google-oauth-client, github-oauth-client, microsoft-oauth-client. mehen templates (MCP vault_templates) is the live list, grouped the same way. Say who it belongs to with --home (MCP home): private (only the human who fills it in; the default, and usable only through their machines enrolled with the private-credentials tick, which is off by default in a shared workspace, so in a team's project ask for project), project (with --project <prj_id> or the checkout's mehen.toml; MCP project) or workspace (usable by members within their machine scope; a guest's or team's machine needs an authorised project environment that references the credential); the human may change it on the page. A private credential can never back a shared project variable. To rotate an existing credential, name its current home (--home project --project <prj_id>, or --home workspace); a different home is conflict, since rotation keeps the home. Without a template, propose where it goes with --host (and --path), and how with --header <name> --format "Bearer {key}" when the usual way is wrong (MCP hosts, paths, injection); the human sees both marked unverified, and the item holds every request for approval unless the human lets reads through. Write the block from ask, then mehen wait <int_...> (MCP vault_wait), or add --wait to the request. The link lasts 24 hours. Repeating the same open request from the same requester, with the same home and details, reuses its link without extending it. Other requests under that name conflict; --new replaces your own open request.
  4. OAuth: mehen connect <provider> --name <slug> --reason "<why>" --expect <account> --json (MCP vault_connect), with --home as for request. Providers: google-gmail, google-calendar, google-drive, github-oauth, microsoft-graph. The OAuth client is picked when exactly one exists; several is a usage error with choices; none is client_missing (exit 4) whose next.command is the exact mehen request oauth-client ... to run, and whose request object is exactly the vault_request arguments. Or add --request-client (MCP requestClient: true): the vault asks for the client first and the connection follows; --json prints one object per step with step: "client" then step: "connect". Write the block (the check is the account to sign in as), then mehen wait <con_...>. The link lasts 15 minutes.
  5. Before a write: mehen check <slug> <METHOD> <url> [-d body] (MCP vault_check) says whether its base rules would allow it, hold it for approval or refuse it. Check fetches public item metadata from the vault, evaluates base binding and policy locally, sends nothing upstream and creates no approval. It does not predict live permission to send. It is advisory and exits 0, 3 or 5.
  6. Call an API: mehen fetch <slug> <METHOD> <url> [-H 'Name: value'] [-d @body.json] (MCP vault_fetch). Any upstream status exits 0 unless --fail. --select data.0.id (MCP select) prints one part of a JSON answer, -o <file> saves a large or binary body, -i shows the response headers on stderr, --max-time 30s (MCP timeoutSeconds) stops waiting. A protected credential needs the protected environment that references it: --project <prj_id> --env <name> (MCP context: { project, environment }), and a team's or a guest's machine reaching a workspace credential through a shared project needs the same context; a credential whose home is that project, or your owner's own private one, is reachable by name. Give a context when one is required, or to deliberately limit a call to one authorised environment; it is an extra requirement the vault checks, not a safe default: for a non-private credential that environment must reference it (a project credential not referenced in dev is not_found with a dev context and works without one), and for a private one it must be an authorised unprotected environment. The context never changes the workspace. Mehen-Upstream: 1 means the upstream answered; Mehen-Error: 1 means the vault refused. Mehen-Item names the account that acted.
  7. Lost the link? mehen pending (MCP vault_pending) lists every ask this machine made that is still open, plus its own "machine added" notice; mehen pending <id> prints its block again.
  8. Coding project: in any clone or worktree with a mehen.toml, run mehen env check (MCP vault_env with check), then mehen run -- <cmd>. Read the mehen: prj_... env=... worktree=... head=... line on stderr; stop if it is wrong. mehen.toml carries project, workspace (the workspace id) and vault (its origin): mehen init writes them; mehen workspace bind adds them to an older manifest. Set up with mehen init and mehen env set <NAME> --env <env> --item <slug> (unprotected environments only). A human may set a personal override for a variable on the project page; mehen env ls --json marks it override: true and mehen run names it in its banner, never the value. Production needs a literal --env prod and a human's approval; never MEHEN_ENV=prod.

Templates fail closed#

For an item made from a template, reads are allowed, every other method needs a human's approval unless the template allows that narrow everyday write, and the worst routes are denied. mehen templates <id> and mehen show <item> print the three lines: without a human, needs approval, refused. Refused on purpose, and what to do instead:

When refused#

Read error and follow next: write the block from ask when there is one, otherwise relay next.say as it is, and run next.command. With --json the error is { error, message, next, approval?, bindingCode?, expiresAt?, ask?, retryAfter? } on stdout.

ExitMeaningDo
0OKCarry on.
1ErrorRead error, report it. Expired or cancelled: start again (next.command is the exact command). cli_outdated: the vault needs a newer mehen; see "Updating".
2UsageFix the command or request. Also the local workspace refusals (workspace_not_allowed, workspace_unknown, workspace_mismatch, context_ambiguous, workspace_unbound, config_dir_retired, pin_outdated): nothing was signed or sent; fix the selection as "Workspace rules" says, or tell the human when the harness decides. A workspace_mismatch after an activated enrolment has contacted the vault and attempted cleanup: read the message for whether the device was revoked.
3Approval requiredWrite the block (the check is the binding code), mehen wait <apr_...>, then repeat exactly the same request.
4A human must actWrite the block, then wait. A mehen wait timeout is also 4: wait again, and the block comes back with it. client_missing is 4 too, as are identity_required (the human signs in with Google in the workspace), account_pending (their account's security change is still settling), owner_inactive (this machine's owner is no longer an active member: an owner or admin must act) and project_unshared (ask an owner or a project manager to share the project with this machine's owner).
5Refused by ruleDo not retry. Tell the human what was refused; only they can change it. A denied approval is also 5.
6Identitymehen status: not enrolled, revoked, clock more than 60 seconds off, an insecure key file, or a key made for another vault (key_scope; one made for another workspace is workspace_mismatch, exit 2).
7TransientRetry, after retryAfter when given.

On approval required, say for example: "I want to send the reply to Sam from gmail-personal." then the block, with "Tick the binding code K7Q2MX, then read the whole request before you approve." Your own machine's asks go to its owner (a guest's own private credential included); a guest's or a team machine's ask for a shared credential goes to the project's managers or a workspace owner or admin, and next.say names who to send the link to. The approval page leads with the decision and shows its stored request presentation beneath it (for an email, the decoded message); it is not a lossless copy of the original body bytes, so keep bodies that need approval small, plain and uncompressed (the vault refuses compressed bodies and bodies over 64 KiB). After approval, repeat the request byte for byte from the same machine: the grant is matched by a digest of the item, method, URL, content type, content encoding, forwarded headers and body.

MCP setup#

mehen setup --json registers one stdio server named mehen (mehen mcp) in every agent it finds on this machine (Claude Code, Codex, Cursor, pi, Gemini CLI, OpenCode, VS Code, Claude Desktop) and installs this skill; restart the agent afterwards. That one server serves every workspace this machine may use, so a second workspace needs no second server. In agents, inspect mcp, reason, skill, skillReason and restart; in skills, inspect each entry's status and reason. Report MCP results other than added, updated or unchanged, and skill results other than those three successes; skill: null means this client has no known skill folder, not a failed installation. --mcp-file <path> writes mcpServers.mehen into a compatible JSON client's file; use --agent vscode or --agent opencode for those clients' different formats. mehen setup --print --json prints the entry without writing anything. To register only a named client, use its setup writer under the intended home and ceiling; inspect the report and restart as directed.

mehen setup --agent claude-code --json
mehen setup --agent codex --json
mehen setup --agent pi --json

Tools and inputs. Every tool except vault_templates also takes workspace? (a slug or id), needed when this server may use more than one workspace and neither the call nor the checkout decides:

Any result or error that hands over a link carries ask in structuredContent and a resource_link in content; isError stays true for approval_required, needs_reauth, client_missing and timeout. No MCP tool starts an enrolment, approves, revokes or freezes. vault_wait can finish an enrolment already started at the terminal. No tool opens a link. mehen run, mehen env set, mehen init, mehen workspace, mehen enrol, mehen revoke and mehen freeze are CLI only. Other harnesses: https://docs.mehen.dev/setup.md.

Updating#

Installed release builds with a compiled trusted release key check for signed updates automatically; a build with no trusted key updates nothing. Either way, you never install anything on a machine that has mehen. Never reinstall over an installed mehen: mehen update --json brings it to the current release now, and its signature is checked. cli_outdated (exit 1) means the vault needs a newer mehen: it has usually updated itself already and returns a refusal whose next.command is your command to run again; if it still fails, run mehen update --json once and retry. After an update the command line uses the new version at once; your MCP tools come from it when the agent restarts, and the server says so in a tool result.

AGENTS.md snippet#

Paste this into a repo's AGENTS.md (or CLAUDE.md) when the repo uses Mehen:

## Credentials

This repo uses Mehen. Secrets never appear in this repo, in chat or in your context.

- Never ask a human to paste a secret into chat, and never write one to a file, `.env` or commit.
  If one is pasted, do not use it; say it should be rotated.
- Run `mehen` with `--json`. Variables are declared in `mehen.toml` (committed, no values), which also
  names the project's workspace and vault. Check them with `mehen env check` and run anything that needs
  them with `mehen run -- <cmd>`. Read the `mehen:` line it prints on stderr and stop if the project or
  environment is wrong.
- Production runs need a literal `--env prod` and a human's approval. Never set `MEHEN_ENV=prod`.
- Call authenticated APIs with `mehen fetch <item> <METHOD> <url>` or the `vault_fetch` MCP tool.
  Never set auth headers or put a key or placeholder in a URL or body yourself.
- Missing credential: `mehen request <kind> --name <slug> --provider <template> --reason "<why>"`,
  write the human the `ask` (link, what it is for, what to check, when it expires), then
  `mehen wait <id>`. `mehen pending` lists the links still open. Trust only links under
  `https://app.mehen.dev/w/<slug>/` that `mehen` handed over.
- On any refusal, follow the `next` field and relay the `ask` or `next.say` to the human. Exit 3
  means approval required: send the link and the binding code, `mehen wait <approval>`, then repeat
  the same request unchanged. Exit 5 means refused by rule: do not retry or work around it.
- Full guide: `mehen guide`.