ConceptsCredentials
Credentials#
What is a credential, and where may it go?
On this page
Items#
An item is one credential. It has:
- a unique slug (
openai-personal,github-work): lower case letters, digits and single dashes, 2 to 64 characters, starting with a letter, not ending with a dash; unique across the whole workspace, whichever home it has; - a kind from the closed list below;
- a home: the workspace, one project, or one member;
- an optional provider template (Providers and templates);
- the account it acts as, and whether that account was verified with the provider;
- tags, public fields, secret fields (encrypted), a binding, an injection, a policy, a ceiling, a status, an optional review flag and a version.
Agents can describe an item safely (mehen show): kind, provider, account, tags, ceiling, hosts, binding, policy, injection, public fields, and for each secret field whether it is set, its length, a well-known prefix (sk-proj-, ghp_, never secret characters) and a fingerprint. That answers "is the key set, and is it the one I think?" without seeing the value.
A secret field's fingerprint is keyed: the first 12 characters of an HMAC of the item and the value under a per-workspace key derived from the root key, so someone holding only the stored data cannot test guesses against it.
Item status in summaries is one of active, needs_reauth, refresh_retrying or disabled.
Kinds#
The kinds are a closed list. New kinds arrive by code review, never from an agent or a request.
| Kind | Fields (secret in bold) | Default ceiling | Highest ceiling allowed |
|---|---|---|---|
api-key | key | placeholder | lease |
bearer-token | token, expires_at | placeholder | lease |
basic-auth | username, password | relay | lease |
custom-headers | headers (JSON map of header to value) | relay | relay |
oauth-client | client_id, client_secret | relay | relay |
oauth-connection | access_token, refresh_token | relay | mint |
google-service-account | key_json | mint | mint |
github-app | app_id, private_key | mint | mint |
hmac-key | key | sign | lease |
ssh-key | private_key, public_key | sign | sign |
password-login | url, username, password, totp_seed | relay | reveal |
totp | seed | relay | reveal |
database-url | url | lease | lease |
smtp-imap | host, username, password | lease | lease |
cloud-access-key | access_key_id, secret_access_key | lease | lease |
file | filename, content | lease | lease |
note | text | reveal | reveal |
generic | value | lease | lease |
What machines can do with each kind today: the relay sends any item with an injection (api-key, bearer-token, basic-auth, custom-headers, oauth-connection, and anything a template gives an injection); oauth-client items are used only by the vault itself to connect and refresh; lease-ceiling items reach mehen run children. Signing and minting are not built. Service-account, GitHub App and SSH signing uses are unavailable. An HMAC key cannot be used to sign through Mehen; if a human explicitly sets its ceiling to lease and the machine and run allow it, its key field can be delivered to the child. SSH certificates, client certificates, SSH agent forwarding, SigV4, mTLS and protocol connectors are not built.
Homes#
Every credential belongs somewhere (home on every item):
| Home | Who sees its metadata | Who may use it (through a machine, within reach) | Who manages it (rotate, re-bind, delete) |
|---|---|---|---|
| workspace | owners, admins and members; a guest only when a project shared with them references it | any member's machine; a guest's or a team's machine only through a project environment that references it | owners and admins |
| project | anyone with use on the project | the machines of anyone with use on the project | the project's managers |
| private | the owning member only | the owning member's own machines enrolled with the private-credentials tick, and never in a protected environment | the owning member (an owner or admin may purge it, without seeing it) |
The home is chosen when the credential is asked for: mehen request --home private|project|workspace (and mehen connect), private by default, and the human filling in the intake may change it within their rights (a project home needs edit on the project; a workspace home an owner or admin; a machine proposing a workspace home must belong to an owner or admin). The passkey that stores the credential signs the home with the rest. A credential can be re-homed with a passkey: workspace to project or project to workspace by an owner or admin (not while another project's variables reference it), private to a project by its owner with edit there; never shared to private.
Out of reach means not found. Someone else's private credential, a project credential of a project not shared with you, a workspace credential from a guest's or a team's machine: every route answers exactly as it does for a name that does not exist, so nothing can be learnt by naming. The one deliberate exception is creation: a name taken by a credential you cannot see, a pending intake or a reserved word is conflict, "name not available", with no further detail, and after twenty unavailable-name attempts in an hour from one account or machine, further attempts are rate limited.
No credential is ever shared across workspaces, and Mehen offers no page or command to read a stored value back. A value leaves the vault only in the relay's upstream request, a placeholder swap on the way to a bound host, or a lease into a child process of a machine whose reach allows it.
Fields#
Each item has named fields, each public or secret, fixed by its kind. A reference can name one: mehen://github-app-work#private_key. A kind's primary field is the one placeholders and leases deliver by default (for example key for api-key); kinds without one need the field named. Secret fields are stored encrypted, one data key per field, and are never returned to a machine except as a lease.
Bindings#
A binding is where an item may be used. An item without a binding cannot be used at all. One matcher, the same in the vault and in the local relay of mehen run, checks it.
type Binding = {
hosts: string[] // exact names, or "*.example.com" (any depth below, never the apex)
schemes?: Scheme[] // default ["https", "wss"]
ports?: number[] // default: the scheme's default port only
paths?: string[] // globs: "*" one segment, "**" any number including none
methods?: string[] // default: any
}How a URL is checked, in order:
- It is parsed with the WHATWG URL parser. A URL with a username or password is refused. Only
https,http,wssandwsparse, plainhttporwsmatch only when the binding lists them, and the vault relay carrieshttpandhttpsonly. - The path is canonicalised (RFC 3986 section 6.2.2): escapes of unreserved characters are decoded and every other escape gets upper-case hex. The path is refused outright, never guessed at, when it holds an encoded slash, backslash or dot (
%2f,%5c,%2e), a literal backslash, a control character, a malformed escape, an escaped%that starts another escape (%2573), a.or..segment, or an empty segment (//). A single trailing slash is fine. The canonical path is what the vault forwards. - The host, lower-cased and without a trailing dot, must match one pattern. Wildcards need at least two labels after
*.(*.comis invalid). In the vault relay, IP literals,localhostnames,*.workers.devand the vault's own host are reachable only when bound exactly; a wildcard never opens them. This is not a complete private-network firewall: the vault does not resolve names. - The port must be listed, or be the scheme's default when none are listed.
- Path and method must match when listed.
Matching fails closed both ways. Binding paths and allow rules match the canonical path exactly and case-sensitively, so an encoding trick can only narrow what is allowed. deny and approve rules match a fully decoded, lower-cased path, so %73end or /me/SendMail still hits a rule written for send or sendMail.
Only a human can change a binding, with a passkey that signs the change. When an agent asks for a new credential, a provider template's binding wins; without a template the vault needs a proposed binding (mehen request --host, and --path), shown to the human in red with the host spelled out. A rotation keeps the existing binding.
Injections#
An injection says how a secret goes into a request. It is fixed per item by a human or a template and is never chosen by the caller of the relay:
in | Shape | Example |
|---|---|---|
header | name, format with {field} markers | Authorization: Bearer {key}, x-api-key: {key} |
query | name, field | ?key=... for APIs that insist |
basic | username, password field names | Authorization: Basic built inside the vault |
headers | field holding a JSON map | custom-headers: two headers at once |
The vault sets the header or parameter, replacing any copy the caller sent. A credential never goes into a reserved header: framing and hop-by-hop headers, cookie and set-cookie, forwarded, x-forwarded-*, proxy-*, cf-*, mehen-*, client-IP and URL-rewrite headers, and method overrides. An agent proposing an injection for an intake without a template may name only header (never a reserved one), query or basic. A secret, or a placeholder for one, goes only into the declared surface: a placeholder found anywhere else in a request is refused, never substituted.
The exposure ladder#
Each use of a credential sits on a rung. The vault uses the lowest rung that works.
| Rung | Mode | What the caller receives | Typical use | Built |
|---|---|---|---|---|
| 0 | relay | The upstream response after vault processing, not the credential value. The upstream service receives the credential. | Every HTTP API. The default. | Yes |
| 1 | sign | A signature. | Webhook HMACs, JWT assertions, SSH certificates, AWS SigV4. | No |
| 2 | mint | A short-lived derived credential. | GitHub App installation tokens, Google access tokens from a service account. | No |
| 3 | placeholder | A per-run placeholder string, swapped for the real value only on the way to a bound host. | mehen run -- bun dev with SDKs that read keys from the environment. | Yes |
| 4 | lease | The value, into one child process only, with its output redacted. | psql, SMTP, older command-line tools. | Yes |
| 5 | reveal | The value, to a human in the browser after a passkey. Never to a machine. | Copying a password into a phone app. | No: no page shows a value |
An item's ceiling is the highest rung it may ever reach. Each kind has a default ceiling and a hard maximum (the kinds table); raising a ceiling needs a passkey and can never pass the kind's maximum.
Reveal is for humans only. A machine never gets reveal, and a machine lease needs a ceiling of exactly lease. A ceiling raised to reveal so a human could view a password or a note never widens what a machine gets. A device's modes cannot include reveal.
A device's modes scope must also include the rung. A relayed request beyond the item's ceiling is refused with mode_refused, one beyond the device's modes with forbidden; a lease that is not allowed is refused with mode_refused.
References#
A reference names an item and optionally a field, and is safe to commit, paste or log:
mehen://openai-personal
mehen://github-app-work#private_keyThe grammar: mehen:// then a slug, then optionally # and a field name of lower case letters, digits and underscores starting with a letter. Repos, configs, prompts and transcripts hold references and placeholders, never values.