# Botpasses > Botpasses stores API keys and lets your agents call APIs over MCP with your approval. It attaches the key to approved requests and removes secrets from responses before returning them to the agent. The hosted process decrypts keys in memory; Botpasses is not zero-knowledge. MCP endpoint: https://botpasses.com/mcp (OAuth 2.1 with PKCE and dynamic client registration on botpasses.com, or a console-issued bearer token). Primary tool: http_request. Also find_items, list_items, request_grant, list_grants, setup. There is no get_secret. This file is every documentation page, generated at site build. The short index is https://botpasses.com/llms.txt. # Start Botpasses holds named credentials (API keys, tokens, OAuth client secrets) for AI agents. You are the **operator**: the person who stores credentials and approves their use. An **agent** is a model client such as Claude, Cursor, ChatGPT, or Grok connected over **MCP** (the Model Context Protocol, the standard way those clients call tools). The agent asks to use a credential by name. You approve. Botpasses makes the HTTPS call with the credential attached and returns a redacted result. The agent gets that result, not the key. ## 1. Create an account 1. Open [Create account](/sign-up) on botpasses.com. 2. Enter your email. Botpasses sends an 8-digit code. Enter it on the page. 3. Enroll an authenticator app (TOTP: the six-digit codes that apps like 1Password, Google Authenticator, or Authy generate). Scan the QR code, open the `otpauth://` link, or type the key. Save the backup codes it shows you. Email alone is not enough to operate the vault. There is no password. Every sign-in is an email code plus your authenticator. ## 2. Connect an agent Pick the client you use. Each page has the copy-paste config. - [Claude (web and desktop)](/docs/connect/claude) - [Claude Code](/docs/connect/claude-code) - [Cursor](/docs/connect/cursor) - [ChatGPT](/docs/connect/chatgpt) - [Grok](/docs/connect/grok) Any other MCP client that supports remote servers over HTTP works with the URL `https://botpasses.com/mcp` and OAuth sign-in on botpasses.com. Then paste the [hosted agent prompt](/docs/prompts#hosted) for help storing credentials and connecting API accounts. You type secrets on botpasses.com, never in chat. A laptop CLI vault (`VAULT_HOME`) uses the [local prompt](/docs/prompts#local). The laptop hosted kernel (`npm run hosted:dev`) is the same console and MCP as botpasses.com on `http://127.0.0.1:8788`; see [Install](/docs/install). The full set is on [Copy-paste prompts](/docs/prompts). ## 3. Set up a credential Ask the connected agent in plain language: "Set up Spotify so you can call the API for me." The agent calls `setup` and gives you a Botpasses link. Open it, type the Client ID and secret there (never in chat), and check **Always allow this agent to use this credential** if you want that agent to keep using it. For Spotify and Google you then Connect the user account from the Inbox. You can also store a credential yourself in the [console](/console) (**Credentials** → **Store credential**). Full steps: [Store a credential](/docs/how-to/store-a-secret) and [Guided setup](/docs/how-to/guided-setup). ## 4. Ask for a task and approve it Ask the agent in plain language: "check my Stripe balance". The agent calls the `http_request` tool. If the credential needs an **approval** (a grant: your permission for one agent to use one credential), the request appears in your console **Inbox**. Approve it there, or type the 8-digit approval code the agent shows you. The agent retries and receives a redacted response. If nothing is stored for that API, the agent gets a `collect_url`. Open it, sign in, and type the value on botpasses.com. Never paste a secret into chat. ## Laptop and self-host Two laptop paths: `npm run hosted:dev` (hosted console and OTP on loopback) or a CLI vault under `VAULT_HOME` (no account). Self-host the same hosted process on any Postgres 16. See [Install](/docs/install), the [CLI reference](/docs/reference/cli), and [Self-hosting](/docs/self-hosting). # Install There are three ways to use Botpasses: the hosted service on botpasses.com, the laptop hosted kernel (`npm run hosted:dev`), and a local CLI vault (`VAULT_HOME`). Paste the [local machine prompt](/docs/prompts#local) into an agent on this laptop if you want it to clone the repo, init the CLI vault, and write the MCP snippet. Do not paste the master key into chat. ## Copy-paste prompt ``` Set up a local Botpasses vault on this machine and wire it into my MCP client. Secrets must never enter chat. Facts: - Repo: https://github.com/naffis/botpasses (MIT). npm package not published yet; run CLI from a clone. - Need Node.js 22.14+. - Local home: set VAULT_HOME to an absolute path (e.g. $PWD/.botpasses or $HOME/.botpasses). - Local MCP exposes the same six tools as hosted: list_items, find_items, request_grant, list_grants, setup, http_request. Values inject only into approved calls or vault run child env. - The hosted kernel on a laptop is npm run hosted:dev (console, OTP, sqlite-hosted at http://127.0.0.1:8788). That is not this VAULT_HOME vault. Its connect URI is http://127.0.0.1:8788/connect/callback, not :8888. Steps: 1. Clone (or reuse) the repo, npm install, export VAULT_HOME to an absolute path, run npx vault init if the vault does not exist. Do not print the master key into chat; tell me where it lives and that I must keep VAULT_MASTER_KEY out of transcripts. 2. Show me the mcp.json snippet for my client (Cursor ~/.cursor/mcp.json or project .cursor/mcp.json) using command npx with args ["vault","mcp"] and env names VAULT_HOME and VAULT_MASTER_KEY (references only, never a literal key). I paste or approve the file edit; you do not echo the master key value in the reply. 3. After MCP connects, call list_items to prove the server is up. 4. For the first secret I name (or Stripe test key if I say so), use setup so I get a collect_url, or tell me to run npx vault set NAME in my own terminal so the CLI can prompt. Do not accept the value in this chat. Do not write a command that contains the secret. Then vault grant --secret NAME --agent --tool http_request (once or standing as I choose). 5. Smoke http_request to an allowlisted host I approve. Report status only. 6. Point me at https://botpasses.com/docs/install and https://botpasses.com/docs/reference/cli for vault run (child process inject) when I need CLI scripts instead of MCP. If I already have a hosted account and only need stdio to hosted: use VAULT_PUBLIC_URL=https://botpasses.com, vault login, vault mcp --user-jwt. Still never paste JWTs into chat; use env on the machine. ``` ## Hosted (recommended) Nothing to install. [Create an account](/sign-up) on botpasses.com, then follow [Start](/docs/start). Agents connect to `https://botpasses.com/mcp`. The hosted service is free to use. `staging.botpasses.com` is the pre-release plane. Accounts and data are separate between the two. To run the same hosted kernel on this laptop (loopback, sqlite, OTP printed in the terminal): ```bash npm run hosted:dev ``` That writes `.botpasses-hosted/secrets.json` once (mode `0600`), binds `127.0.0.1:8788`, and does not inherit a leftover `DATABASE_URL`. `--check` evaluates boot without listening. `--postgres` plus `VAULT_HOSTED_DEV_DATABASE_URL` is the opt-in Postgres path. This is not the local CLI vault (`VAULT_HOME`). Run one `hosted:dev` process per sqlite file; a second writer can get SQLITE_BUSY. ## CLI from the repository The `botpasses` npm package is not published yet. Until it is, run the CLI from a clone. Requirements: Node.js 22.14 or newer. ```bash git clone https://github.com/naffis/botpasses.git cd botpasses npm install export VAULT_HOME="$PWD/.botpasses" npx vault init ``` `vault init` creates the SQLite vault at `$VAULT_HOME` and a master key. The default home when `VAULT_HOME` is unset is `$HOME/.botpasses`. You can also run `npm run vault -- `. Store and use a credential locally: ```bash printf '%s' 'sk_test_example_not_real' | npx vault set STRIPE_KEY npx vault list npx vault grant --secret STRIPE_KEY --agent invoicer --tool stripe --once npx vault run --with STRIPE_KEY --agent invoicer --tool stripe -- \ node -e 'console.log("injected", Boolean(process.env.STRIPE_KEY))' ``` `vault run` **injects** the value into the child process environment. That is the only place the local plane reveals it. See the [CLI reference](/docs/reference/cli). ## Local MCP server (stdio) Point a desktop MCP client at the local vault: ```json { "mcpServers": { "botpasses": { "command": "npx", "args": ["vault", "mcp"], "env": { "VAULT_HOME": "/absolute/path/.botpasses", "VAULT_MASTER_KEY": "set-me" } } } } ``` The local server exposes the same six tools as hosted: `list_items`, `find_items`, `request_grant`, `list_grants`, `setup`, and `http_request`. `http_request` calls an allowlisted API with a stored credential once you approve the agent (`vault grant --secret NAME --agent AGENT --tool http_request`); `vault run` is the other inject path, for a child process. Details: [MCP tools](/docs/reference/mcp-tools#local-mcp-sqlite). ## Hosted MCP over stdio Some clients only speak stdio. The CLI can proxy hosted MCP over an access token: ```bash export VAULT_PUBLIC_URL=https://botpasses.com npx vault login export VAULT_USER_JWT=eyJ... npx vault mcp --user-jwt ``` `vault login` prints the sign-in, console, and device-login URLs. Device login is RFC 8628: you enter a code on botpasses.com after your authenticator. ## Self-hosting Self-host the same process on any Postgres 16 and a custom `https` origin. Fly, Neon, and AWS KMS are the reference stack, not a requirement. See [Self-hosting](/docs/self-hosting). # Copy-paste prompts Paste one of these into your agent instead of inventing setup steps. The agent walks Collect and Connect. You type secrets on botpasses.com, never in chat. Use [Hosted](#hosted) after you add `https://botpasses.com/mcp`. [Local](#local) is a laptop CLI vault (`VAULT_HOME`) with no account. To run the hosted kernel on this laptop (console, OTP, MCP on loopback) see [Install](/docs/install) (`npm run hosted:dev`). [Self-host](#self-host) deploys the same hosted process on any Postgres 16 and an `https` origin you control. ## Hosted Paste into Claude, Cursor, ChatGPT, or Grok after you add https://botpasses.com/mcp. Your agent walks the Collect and Connect links; you type secrets on botpasses.com. ``` You are setting up Botpasses for me (https://botpasses.com). Botpasses is a grant-vault for agents: I store API credentials once; you call APIs through MCP; the key is attached inside the vault and must never appear in chat, logs, or your context. There is no get_secret. Do this end to end. Pause and give me a link or checkbox whenever a human step is required. Do not ask me to paste secrets into this chat. 1. Confirm Botpasses MCP is connected. - Hosted MCP URL: https://botpasses.com/mcp - If tools are missing, tell me which client I am on and open the matching docs: Cursor https://botpasses.com/docs/connect/cursor Claude https://botpasses.com/docs/connect/claude Claude Code https://botpasses.com/docs/connect/claude-code ChatGPT https://botpasses.com/docs/connect/chatgpt Grok https://botpasses.com/docs/connect/grok - Grok often needs a console-issued bearer (avm_…) as Authorization: Bearer (single Bearer, no double Bearer). If tools/call falls into OAuth redirect_uri errors, say so and point me at a one-time model token from the Botpasses console. 2. Call the setup tool for the first API I name (default: Spotify if I do not name one). Prefer provider=spotify|stripe|github|google|slack, or host= for other APIs. 3. When setup returns collect_url, give me that URL only. Tell me to open it, sign in, type the credential there, and never paste the value here. For Client ID and secret kinds, remind me the redirect URI on the provider app is the one Collect shows: https://botpasses.com/connect/callback on production, https://staging.botpasses.com/connect/callback on staging, and http://127.0.0.1:8788/connect/callback on npm run hosted:dev. Never use http://127.0.0.1:8888/callback on a hosted plane; that URI is the CLI vault (vault serve) only. 4. If setup or http_request returns connect_url (user OAuth, e.g. Spotify /v1/me), give me that console link and wait until I confirm Connect is done. 5. Prefer Always-allow only when I say so. Otherwise one-shot Inbox approvals are fine. 6. When status is ready, smoke a read-only call (for Spotify: GET /v1/search or /v1/me after Connect). Report origin status and a short redacted summary. Do not print tokens. 7. Stop with: (a) item name, (b) hosts, (c) whether user Connect is done, (d) one example ask I can type next in plain language. If anything fails (need_item, host_mismatch, mfa_required, 429, redirect_uri), use https://botpasses.com/docs/troubleshooting and keep secrets out of chat. ``` ## Local For a laptop CLI vault with SQLite (VAULT_HOME). No botpasses.com account. Same MCP tools as hosted. The laptop hosted kernel is npm run hosted:dev; see Install. ``` Set up a local Botpasses vault on this machine and wire it into my MCP client. Secrets must never enter chat. Facts: - Repo: https://github.com/naffis/botpasses (MIT). npm package not published yet; run CLI from a clone. - Need Node.js 22.14+. - Local home: set VAULT_HOME to an absolute path (e.g. $PWD/.botpasses or $HOME/.botpasses). - Local MCP exposes the same six tools as hosted: list_items, find_items, request_grant, list_grants, setup, http_request. Values inject only into approved calls or vault run child env. - The hosted kernel on a laptop is npm run hosted:dev (console, OTP, sqlite-hosted at http://127.0.0.1:8788). That is not this VAULT_HOME vault. Its connect URI is http://127.0.0.1:8788/connect/callback, not :8888. Steps: 1. Clone (or reuse) the repo, npm install, export VAULT_HOME to an absolute path, run npx vault init if the vault does not exist. Do not print the master key into chat; tell me where it lives and that I must keep VAULT_MASTER_KEY out of transcripts. 2. Show me the mcp.json snippet for my client (Cursor ~/.cursor/mcp.json or project .cursor/mcp.json) using command npx with args ["vault","mcp"] and env names VAULT_HOME and VAULT_MASTER_KEY (references only, never a literal key). I paste or approve the file edit; you do not echo the master key value in the reply. 3. After MCP connects, call list_items to prove the server is up. 4. For the first secret I name (or Stripe test key if I say so), use setup so I get a collect_url, or tell me to run npx vault set NAME in my own terminal so the CLI can prompt. Do not accept the value in this chat. Do not write a command that contains the secret. Then vault grant --secret NAME --agent --tool http_request (once or standing as I choose). 5. Smoke http_request to an allowlisted host I approve. Report status only. 6. Point me at https://botpasses.com/docs/install and https://botpasses.com/docs/reference/cli for vault run (child process inject) when I need CLI scripts instead of MCP. If I already have a hosted account and only need stdio to hosted: use VAULT_PUBLIC_URL=https://botpasses.com, vault login, vault mcp --user-jwt. Still never paste JWTs into chat; use env on the machine. ``` ## Self-host Run the same hosted process on your plane. One machine, Postgres (any vendor), KMS-wrapped platform key, custom https origin. Swap botpasses.com for your origin in agent prompts and OAuth redirect URIs. ``` Help me deploy a self-hosted Botpasses plane from https://github.com/naffis/botpasses. Goal: a private grant-vault my agents hit at our origin (not necessarily botpasses.com). Follow docs/self-hosting and docs/ops in the repo. Do not invent secret values; ask me to set them in the secret store. Reference shape (adapt to our cloud if we are not on Fly): - One Node process: VAULT_MODE=hosted vault serve (one Machine per plane; in-memory enroll/rate-limit state). - Postgres 16 (any vendor): separate database per plane; pooled DATABASE_URL + direct DATABASE_URL_DIRECT. - Edge DNS/WAF (Cloudflare or ours) with SSL full strict. - AWS KMS (or approved KMS) wrapping VAULT_KEK_WRAPPED; VAULT_KMS_APP_ID or FLY_APP_NAME; Fly OIDC or equivalent for AWS_ROLE_ARN. - Email provider for codes (Resend pattern: RESEND_API_KEY + VAULT_EMAIL_FROM). - Optional: R2/S3 encrypted backups, Sentry without values. - Laptop hosted kernel is npm run hosted:dev (VAULT_DEPLOY_PLANE=dev, loopback, sqlite). That plane is refused when FLY_APP_NAME is set. Required config checklist (confirm each is set in secrets, never paste into chat): DATABASE_URL (Postgres URL), DATABASE_URL_DIRECT, VAULT_PUBLIC_URL (our https origin), VAULT_DEPLOY_PLANE (staging|production), VAULT_KEK_WRAPPED, VAULT_KMS_KEY_ID, VAULT_KMS_APP_ID or FLY_APP_NAME, AWS_ROLE_ARN, VAULT_KEK_REQUIRE_KMS=1 after cutover, VAULT_SESSION_SECRET (>=32 bytes), VAULT_OIDC_PRIVATE_JWK, VAULT_APPROVAL_HMAC (64 hex), bootstrap token pair only for break-glass window, VAULT_TRUST_PROXY as appropriate. Build: npm ci npm --prefix site ci && npm --prefix site run build VAULT_MODE=hosted VAULT_BIND_HOST=0.0.0.0 PORT=8788 npx vault serve Exit 78 means config is wrong; fix from the self-hosting table. After /health and /ready pass: 1. Create the first operator account on our VAULT_PUBLIC_URL (email code + TOTP). 2. Issue a model/agent token or complete OAuth for our MCP clients against https:///mcp. 3. Give developers the hosted agent bootstrap prompt, with every botpasses.com URL replaced by our origin (including /connect/callback on provider apps). 4. Write a short internal runbook: who holds KMS, how to revoke an agent, how to rotate KEK (vault kek-rotate), backup restore pointer under docs/ops. Constraints: do not scale to two Machines without moving in-memory state to the database. Do not branch production Postgres from staging. Never log or return credential values. ``` ## Smaller prompts Shorter follow-ups once MCP is connected. ### First API only MCP is already connected. The agent runs setup for one API and gives you Collect and Connect links. ``` Botpasses MCP should already be connected. Call setup for . Give me collect_url (and connect_url if needed). I will type secrets on Botpasses only. When ready, run one read-only smoke and tell me the item name and a plain-language ask to use next. ``` ### Second agent A second client on the same hosted vault. Grants for items you already stored. No new secrets unless needed. ``` I already have Botpasses working for one client. Help a second agent (name the client) connect to the same hosted vault at https://botpasses.com/mcp, store nothing new unless needed, request grants for the existing item names I list, and use Always-allow only if I say so. Keep staging agents off production credentials. ``` ### Staging Test on staging.botpasses.com with a separate account from production. ``` Use https://staging.botpasses.com (separate accounts from production). Connect MCP to https://staging.botpasses.com/mcp, set up a test credential, smoke one call, and remind me production agents cannot use staging items and vice versa. ``` ### Grok redirect_uri Tools list works but tools/call fails with redirect_uri. Prefer a console-issued avm_ bearer. ``` My Botpasses MCP lists tools but tools/call fails with redirect_uri or Model OAuth required. Diagnose against https://botpasses.com/docs/connect/grok and troubleshooting. Prefer a console-issued avm_ bearer as Authorization: Bearer (one Bearer prefix). Do not put the token in chat; use the client's secret/env field. Re-test list_items then a setup or http_request. ``` # Store a credential A **credential** is an API key, a token, or an OAuth client secret. It has a name, a value, a kind, an environment, and a list of allowed hosts. The value is encrypted at rest and never shown again after you submit it. ## In the console 1. [Sign in](/sign-in) and open the [console](/console). 2. In **Credentials**, choose **Store credential**. 3. **Name.** Env-var style: `STRIPE_SECRET_KEY`. Uppercase letters, digits, and underscores, starting with a letter (`[A-Z][A-Z0-9_]{0,127}`). Names are unique per environment. 4. **Kind.** *API token* sends the value as `Authorization: Bearer`. *Client ID and secret* is an OAuth app secret; enter the public Client ID as well. It is stored as `client_secret` and Botpasses mints app tokens with it instead of sending it as a Bearer. See [Use an OAuth client secret](/docs/how-to/use-an-oauth-client-secret). 5. **Allowed hosts.** The HTTPS hostnames the agent may call with this credential, such as `api.stripe.com`. Hostnames only, not URLs. Only these hosts will ever receive the value. 6. **Environment.** `staging` or `production`. Agents are bound to one environment and only see credentials in it. On `staging.botpasses.com` the only choice is staging. 7. **Change how it is sent** (optional). Bearer is the default for API tokens. Choose HTTP Basic (with a username) or a raw header (`header:X-Api-Key`) when the API needs it. 8. **Store.** If the request is rejected, the dialog stays open and shows the error. The value field keeps what you typed. ## Edit, rotate, delete - **Edit** opens the same form prefilled. Leave the value blank to keep the current secret. - **Rotate** replaces the value only. - **Delete** removes the credential. Agents with an approval for it lose access at once. ## When an agent asked first If you asked the agent to set up a provider, follow [Guided setup](/docs/how-to/guided-setup). If an agent called an API and nothing was stored for that host, the tool result carries a `collect_url` and the request appears in your **Inbox** as *Store the credential*. Either open the Inbox card or the `collect_url`, sign in, and type the value on botpasses.com. For a known API the form is prefilled (name, kind, hosts). Check **Always allow this agent to use this credential** if you want a standing approval. Left unchecked, Botpasses gives the requesting agent a one-time approval so its retry succeeds. Never paste a secret into the chat. The model does not need it and Botpasses never asks for it there. ## Local CLI ```bash printf '%s' 'sk_test_example_not_real' | npx vault set STRIPE_KEY ``` The output is the name and the last four characters, never the value. ## API `POST /api/items` with an operator session and `X-CSRF-Token`. Fields: `name`, `value`, `environment`, `kind`, `allowed_hosts`, `inject`, optional `username` and `folder_name`. See the [HTTP API](/docs/reference/http-api#items-and-collect). The MCP side of a miss is `need_item` plus `collect_url` in the [MCP reference](/docs/reference/mcp-tools#find_items). # Approve a request An **approval** (the API calls it a grant) is your permission for one agent to use one credential. Agents cannot approve anything themselves, and nothing in MCP can revoke. ## How a request arrives 1. You ask the agent for a task in plain language: "get my Spotify profile". The agent calls `http_request` with the host, method, and path. 2. Botpasses finds the matching credential by host. If this agent has no active approval for it, Botpasses creates a pending request and returns an `approval_code` to the agent. The agent tells you to approve. 3. Approve in one of the ways below. The agent retries `http_request` and gets a redacted response. Agents may also call `request_grant` directly with an `item_name`. Same flow. ## Ways to approve | Where | How | | --- | --- | | Console **Inbox** | The card reads "cursor wants STRIPE_SECRET_KEY" with the agent's reason. **Approve** is one call. **Always approve for this agent** is a standing approval for this credential and this agent on the hosts already on the item. Approve with limits can also check Always approve. | | **Approve by code** | Type the 8-digit `approval_code` the agent showed you. A code works once; reusing it is a 409. | | Email | If email is configured for your account, the request also arrives as a link. Opening it shows a confirm page on botpasses.com. | ## Policies | Policy | Behaviour | | --- | --- | | `prompt` | One successful API call, then consumed. The default. | | `session` | Active for 8 hours or until you revoke. | | `item_standing` | This agent may use this credential without asking again, until you clear the standing approval (Agents client row, Approvals, or the credential drawer). | | `folder_standing` | Owner only. Every credential in the folder or environment is pre-approved for this agent. You type the folder or environment name to confirm. | A `prompt` approval is one call. Any answer from the API spends it, including a 401, 410, or 5xx, because the credential has already been sent. It comes back only when the request never left Botpasses (a host mismatch, or a DNS, connect, or TLS failure). If the agent needs to retry, use **Approve with limits** with a call quota or a duration instead of approving one call at a time. **Always approve for this agent** writes `item_standing` for that pair. Later `http_request` calls that match the standing scope proceed without a new Inbox card. A call outside that scope (another agent, another credential, or a method/path the approval did not cover) still waits in the Inbox. The audit log records `auto_approved` with the item name, client, and host. Clear the standing approval anytime from Agents or the credential drawer. ## By API Operators can `POST /api/grants/:id/approve` with `{ "policy": "prompt" }` or `POST /api/grants/approve-by-code` with `{ "code": "12345678" }`. Both need a session cookie and `X-CSRF-Token`. See the [HTTP API](/docs/reference/http-api#clients-grants-inbox). # Guided setup After you [create an account](/docs/start) and [connect an agent](/docs/start), you can ask the agent to set up a provider. The agent does not get the secret. Or paste a prompt from [Copy-paste prompts](/docs/prompts). The hosted prompt covers account connection, credential setup, and a test API call. The short prompt below is enough when MCP is already connected. ## Copy-paste prompts Hosted agent (Claude, Cursor, ChatGPT, or Grok after `https://botpasses.com/mcp`): ``` You are setting up Botpasses for me (https://botpasses.com). Botpasses is a grant-vault for agents: I store API credentials once; you call APIs through MCP; the key is attached inside the vault and must never appear in chat, logs, or your context. There is no get_secret. Do this end to end. Pause and give me a link or checkbox whenever a human step is required. Do not ask me to paste secrets into this chat. 1. Confirm Botpasses MCP is connected. - Hosted MCP URL: https://botpasses.com/mcp - If tools are missing, tell me which client I am on and open the matching docs: Cursor https://botpasses.com/docs/connect/cursor Claude https://botpasses.com/docs/connect/claude Claude Code https://botpasses.com/docs/connect/claude-code ChatGPT https://botpasses.com/docs/connect/chatgpt Grok https://botpasses.com/docs/connect/grok - Grok often needs a console-issued bearer (avm_…) as Authorization: Bearer (single Bearer, no double Bearer). If tools/call falls into OAuth redirect_uri errors, say so and point me at a one-time model token from the Botpasses console. 2. Call the setup tool for the first API I name (default: Spotify if I do not name one). Prefer provider=spotify|stripe|github|google|slack, or host= for other APIs. 3. When setup returns collect_url, give me that URL only. Tell me to open it, sign in, type the credential there, and never paste the value here. For Client ID and secret kinds, remind me the redirect URI on the provider app is the one Collect shows: https://botpasses.com/connect/callback on production, https://staging.botpasses.com/connect/callback on staging, and http://127.0.0.1:8788/connect/callback on npm run hosted:dev. Never use http://127.0.0.1:8888/callback on a hosted plane; that URI is the CLI vault (vault serve) only. 4. If setup or http_request returns connect_url (user OAuth, e.g. Spotify /v1/me), give me that console link and wait until I confirm Connect is done. 5. Prefer Always-allow only when I say so. Otherwise one-shot Inbox approvals are fine. 6. When status is ready, smoke a read-only call (for Spotify: GET /v1/search or /v1/me after Connect). Report origin status and a short redacted summary. Do not print tokens. 7. Stop with: (a) item name, (b) hosts, (c) whether user Connect is done, (d) one example ask I can type next in plain language. If anything fails (need_item, host_mismatch, mfa_required, 429, redirect_uri), use https://botpasses.com/docs/troubleshooting and keep secrets out of chat. ``` First API only: ``` Botpasses MCP should already be connected. Call setup for . Give me collect_url (and connect_url if needed). I will type secrets on Botpasses only. When ready, run one read-only smoke and tell me the item name and a plain-language ask to use next. ``` ## Ask the agent In the client you connected (Cursor, Claude, ChatGPT, or Grok), say: > Set up Spotify so you can call the API for me. Other examples: "store a Stripe secret key", "set up GitHub so you can call the API", "connect Google credentials". The agent calls the `setup` tool with `provider` (`spotify`, `github`, `google`, `slack`, or `stripe`) or an API `host`. It gives you a Botpasses `collect_url`. Open that page and type the credential there. Do not paste the value into chat. ## What you type on Collect For a known provider the form is prefilled: | Provider | Name | What to store | | --- | --- | --- | | Spotify | `SPOTIFY_SECRET` | Client ID and Client Secret from [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) | | Stripe | `STRIPE_SECRET_KEY` | Secret key (`sk_`), not a Stripe Connect OAuth client | | GitHub | `GITHUB_TOKEN` | Fine-grained personal access token | | Google | `GOOGLE_CLIENT_SECRET` | OAuth client ID and secret from Google Cloud | | Slack | `SLACK_BOT_TOKEN` | Bot token (`xoxb-`) or user token (`xoxp-`) | Check **Always allow this agent to use this credential** if you want that agent to keep using it without a new Inbox card each time. The box starts unchecked. When Kind is Client ID and secret, Collect shows the redirect URI to add on the app. On production it is `https://botpasses.com/connect/callback`, on staging `https://staging.botpasses.com/connect/callback`, and on `npm run hosted:dev` it is `http://127.0.0.1:8788/connect/callback`. The CLI vault (`vault serve`) still uses `http://127.0.0.1:8888/callback`. Same URI for every provider on a given plane. ## Connect a user account Spotify and Google also need a signed-in user for paths such as `/v1/me`. After you store the app secret, the agent gives you a `connect_url` (a console link). Connect the account there. The dialog shows the same redirect URI again, and can also allow that agent to use the connected account. App-token calls (for example Spotify search) work after store even if you skip Connect. ## Then ask for data Once setup is `ready`, ask for the data you want: "what's on my Spotify profile?" The agent calls `http_request`. You do not name Botpasses tools. ## Store it yourself If you prefer not to use `setup`, store the credential in the console. See [Store a credential](/docs/how-to/store-a-secret). # Revoke access Everything that can hold access is listed in one place: the console **Access** panel. Revoke is operator-only. There is no revoke over MCP. ## What you see - **Agents** (clients). Each row shows the name, the last four characters of its bearer token, when it was issued, first and last access, and which credential names it has fetched. The full token was shown once at issue time and cannot be shown again. **Rotate** issues a new token and kills the old one. - **Approvals** (grants). Agent, credential, policy, status. - **Sessions.** Your own signed-in browsers and CLIs. - **Activity.** Actions and names only: `request_grant`, `inject`, `token_issued`, and so on. Secret values are never written to the log. Open **Audit log** on an agent row to see only that agent's activity. ## Revoke | Action | Effect | | --- | --- | | Revoke an agent | Its bearer token and any OAuth access tokens stop working on the next request (401). Every OAuth connection any member of the organisation made for that agent is ended, so a refresh cannot bring it back; only a new sign-in and consent can. Its approvals are revoked. | | Revoke an approval | That agent can no longer use that credential. Other approvals stay. | | Revoke a session | That browser or CLI is signed out. You cannot revoke the session you are using; sign out instead. | | Revoke other sessions | Signs out everything except the current session. | Every revoke asks you to confirm in a dialog before the request is sent. ## Deleting a credential Deleting a credential in **Credentials** also ends every approval for it. Rotating a credential keeps approvals and replaces the value. ## By API `POST /api/clients/:id/revoke`, `POST /api/grants/:id/revoke`, `POST /api/sessions/:id/revoke`, `POST /api/sessions/revoke-others`. Operator session plus `X-CSRF-Token`. See the [HTTP API](/docs/reference/http-api#access). # Use an OAuth client secret An OAuth **client secret** is the password of an app, not of a user. It must never be sent as `Authorization: Bearer` to a resource API. Botpasses stores it as kind `client_secret` and handles the token exchange itself. Spotify is the worked example; the same shape applies to other OAuth 2 client-credentials APIs that Botpasses supports. ## Store the client secret 1. In the console, **Store credential**. Kind: **Client ID and secret**. Enter the public Client ID and the Client Secret. 2. Allowed hosts: `api.spotify.com, accounts.spotify.com` (the resource host and the token host). 3. Store. The value is kept as `client_secret`, and the vault row shows *app secret*, not *token*. ## What Botpasses does on a call When an agent calls `http_request` against `api.spotify.com` with this credential: 1. Botpasses sends `POST https://accounts.spotify.com/api/token` with HTTP Basic (`client_id:client_secret`) and a form body `grant_type=client_credentials`. JSON plus a Bearer of the secret is invalid at that endpoint, so Botpasses never does that. 2. It caches the minted app token for its lifetime. 3. It calls `api.spotify.com` with `Authorization: Bearer `. 4. Any access token in the response is replaced with `[redacted]` before the result reaches the model. The agent can also pass a public `client_id` argument on `http_request` if the credential was stored without one. ## App token or user token An app token can call endpoints that are not about a person, for example `GET /v1/search`. Endpoints about the signed-in user, such as `GET /v1/me` or private playlist writes, need a **user token** obtained with Authorization Code and PKCE. Add, list, reorder, or remove playlist contents with `/v1/playlists/{id}/items` (not `/tracks`). If an agent still calls the old `/tracks` path, Botpasses rewrites it to `/items` before Spotify sees it and says so on the result. When an agent hits one of those with only the app credential, Botpasses does not send the call (Spotify would answer 401 and the approval would be spent). The agent gets `user_connect_required` with a link, and you get a card in the **Inbox**: " needs a Spotify account for SPOTIFY_SECRET". To fix it: 1. Open the link the agent gives you, or press **Connect Spotify** on the Inbox card. Either opens the connect dialog for `SPOTIFY_SECRET` with the Client ID filled in from the credential and **Also allow to use the connected account** checked. Leave it checked so the agent's next call goes through without another approval; uncheck it to approve the agent by hand later. **Deny** on the card refuses the request. 2. Add the redirect URI the console shows. On production it is `https://botpasses.com/connect/callback`, on staging `https://staging.botpasses.com/connect/callback`, and on `npm run hosted:dev` it is `http://127.0.0.1:8788/connect/callback`. The local CLI vault (`vault serve`) still uses `http://127.0.0.1:8888/callback`. Same URI for every provider on a given plane. Copy it from Store credential (Kind: Client ID and secret) or from Connect. Older apps that already registered `/integrations//callback` still work. 3. Approve in the Spotify consent screen. Botpasses stores the refresh token in the vault as `SPOTIFY_REFRESH` (next to `SPOTIFY_SECRET`), closes the Inbox card, and tells you the agent can retry. The refresh token and the access tokens stay out of the model. 4. Tell the agent you are done. It retries the same call once and gets the user's data. Later calls to that API with **Also allow** left checked do not open another Inbox card. The standing approval is on `SPOTIFY_SECRET` (the API), not only on the token mint. The same request repeated by the agent reuses the one Inbox card; it expires like an approval link if nobody acts on it. On the local CLI there is no connect flow: the agent's hint names the `vault set SPOTIFY_REFRESH --inject refresh` command to store a refresh token you obtained yourself. ## Refresh through the token endpoint directly An agent that holds an approval on `SPOTIFY_REFRESH` alone can also call the token endpoint itself: `http_request` with `item_name: SPOTIFY_REFRESH`, `POST https://accounts.spotify.com/api/token`, and a body of `grant_type=refresh_token` (or no body). Botpasses finds the client secret in `SPOTIFY_SECRET` (or `SPOTIFY`) in the same environment, sends it the way the provider expects (HTTP Basic for Spotify and Slack, `client_id` and `client_secret` form fields for Google, GitHub, and Stripe Connect) with the refresh token in the form, and returns the token body with every token replaced by `[redacted]`, plus `refreshed: true` and `token_last4`. The agent never needs an approval on the client secret item, and neither value reaches the result. Both items must list the token host in their allowed hosts. If the client secret item is missing, a provider that refuses public clients (Slack, GitHub, Stripe Connect) gets a clear `inject_denied` result naming the item to store instead of a vendor `invalid_client`; Spotify and Google, which support PKCE, are sent the public-client exchange with `client_id` only. ## Retry after a failed call A `prompt` approval is spent by any answer from Spotify, including a 401 or 410, because the client secret was already sent. If the agent retries and gets a pending grant, approve it again, or approve with limits (a call quota or a duration) so a token mint that fails once can be retried without a new code. If Spotify rotates the refresh token on a user-token call, Botpasses stores the new one in place. ## Related - [MCP tools](/docs/reference/mcp-tools#http_request): `client_id` and `content_type` arguments. - [HTTP API](/docs/reference/http-api#items-and-collect): `POST /api/integrations/spotify/start` and the callback route. # Connect Claude Claude connects to Botpasses as a remote MCP server with OAuth. botpasses.com is the authorization server, so sign-in and consent happen on botpasses.com, not on a third-party identity host. You never paste a token into Claude. For the terminal client, see [Connect Claude Code](/docs/connect/claude-code). ## Add the connector 1. In Claude, open **Settings**, then **Connectors**. 2. Choose **Add custom connector**. 3. Name: `Botpasses`. Remote MCP server URL: `https://botpasses.com/mcp`. Leave the OAuth client fields empty; Claude registers itself with botpasses.com (dynamic client registration) and uses PKCE. 4. Save, then choose **Connect**. A browser window opens on botpasses.com. Sign in with your email code and authenticator, then allow the connection on the consent page. The page names the client and where it will send you back. 5. In a new chat, open the tools menu and make sure the Botpasses connector is enabled. Claude Desktop uses the same Settings > Connectors screen and syncs with the web. ## Use it Ask for the task in plain language: "check my Stripe balance" or "get my Spotify profile". The server tells Claude to call `http_request` in the same turn, so you do not need to mention Botpasses or name a tool. - If the credential needs your approval, Claude shows an 8-digit code and asks you to approve in the [console](/console) Inbox or with the code. Approve, then tell Claude to retry (or it retries on its own). - If nothing is stored for that API, Claude gives you a `collect_url`. Open it, sign in, and type the key on botpasses.com. Do not paste it into the chat. After the connector is on, paste a [hosted bootstrap prompt](/docs/prompts#hosted) for help storing credentials and connecting API accounts. Enter secrets on the Botpasses site you connected to. Do not paste them into chat. For a laptop CLI vault, use the [local prompt](/docs/prompts#local). The laptop hosted kernel is `npm run hosted:dev` at `http://127.0.0.1:8788/mcp`; provider apps get `http://127.0.0.1:8788/connect/callback`. See [Install](/docs/install). ## What Claude can and cannot do Claude sees credential names, the last four characters, approval status, and redacted API responses. It cannot read a value, and there is no tool that returns one. Revoke Claude at any time from the console **Access** panel; its tokens stop working on the next call. ## Environments Each connected agent is bound to one vault environment (`staging` or `production`) and only sees credentials in it. If Claude cannot find a credential you stored, check that both are in the same environment. ## If the connect card keeps appearing See [Troubleshooting](/docs/troubleshooting#the-connect-card-keeps-appearing). # Connect Claude Code Claude Code (the terminal client) connects to Botpasses as a remote MCP server over HTTP with OAuth. Nothing secret goes into your shell history or your project files. ## Add the server ```bash claude mcp add --transport http botpasses https://botpasses.com/mcp ``` Add `--scope user` to make it available in every project instead of the current one. ## Sign in 1. Start `claude` and run `/mcp`. 2. Pick **botpasses** and choose **Authenticate**. A browser window opens on botpasses.com. 3. Sign in with your email code and authenticator, then allow the connection on the consent page. Back in the terminal, the server shows as connected. `claude mcp list` prints it as well. ## Use it Ask for the task: "list my open Stripe disputes". Claude Code calls `http_request` with the host, method, and path. Botpasses attaches the credential and returns a redacted response. - Pending approval: Claude Code shows an 8-digit code. Approve in the [console](/console) Inbox or type the code under **Approve by code**. Then let it retry. - Missing credential: it shows a `collect_url`. Open it in a browser, sign in, and store the key there. Never paste a key into the terminal chat. After MCP is connected, paste a [hosted bootstrap prompt](/docs/prompts#hosted) for help storing credentials and connecting API accounts. Enter secrets on the Botpasses site you connected to. Do not paste them into chat. For a laptop CLI vault, use the [local prompt](/docs/prompts#local). The laptop hosted kernel is `npm run hosted:dev` at `http://127.0.0.1:8788/mcp`; provider apps get `http://127.0.0.1:8788/connect/callback`. See [Install](/docs/install). ## Team projects You can commit a project-scoped server entry in `.mcp.json` at the repo root. Each person authenticates on their own account; the file holds only the URL. ```json { "mcpServers": { "botpasses": { "type": "http", "url": "https://botpasses.com/mcp" } } } ``` ## Remove or revoke `claude mcp remove botpasses` removes the local entry. To stop the tokens it already holds, revoke the agent in the console **Access** panel. ## Local vault instead If you want a local SQLite vault with no account, register the stdio server from [Install](/docs/install#local-mcp-server-stdio). It has the same six tools, `setup` and `http_request` included; approvals come from `vault grant --secret NAME --agent AGENT --tool http_request` or the local console instead of the hosted Inbox. The laptop hosted kernel (`npm run hosted:dev`) is a different process: point Claude Code at `http://127.0.0.1:8788/mcp`. # Connect Cursor Cursor reads MCP servers from `mcp.json`. Use the remote URL for the hosted service (recommended), or the stdio command for a local vault. ## Remote server (hosted) Create or edit `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` for every project: ```json { "mcpServers": { "botpasses": { "url": "https://botpasses.com/mcp" } } } ``` Then: 1. Open **Cursor Settings**, then **MCP**. The `botpasses` server appears with a **Needs login** state. 2. Choose it. Cursor discovers botpasses.com as the OAuth authorization server, registers itself, and opens your browser. 3. Sign in on botpasses.com with your email code and authenticator, then allow the connection. Cursor registers `cursor://anysphere.cursor-mcp/oauth/callback`, `http://localhost:8787/callback`, and (for cloud agents) `https://www.cursor.com/agents/mcp/oauth/callback`. All three are accepted. The server shows its tools once connected. Do not paste an operator token or any key into Cursor. ## Use it Ask the agent for the task in plain language. It calls `http_request`. Pending approvals show an 8-digit code you approve in the [console](/console) Inbox. A missing credential returns a `collect_url` to open in a browser. After MCP is connected, paste a [hosted bootstrap prompt](/docs/prompts#hosted) for help storing credentials and connecting API accounts. Enter secrets on the Botpasses site you connected to. Do not paste them into chat. For a laptop CLI vault (`VAULT_HOME`), use the [local prompt](/docs/prompts#local). To run the hosted kernel on this laptop, `npm run hosted:dev` and point Cursor at `http://127.0.0.1:8788/mcp`; provider apps get `http://127.0.0.1:8788/connect/callback`. See [Install](/docs/install). ## Stdio (local vault, no account) ```json { "mcpServers": { "botpasses": { "command": "npx", "args": ["vault", "mcp"], "env": { "VAULT_HOME": "/absolute/path/.botpasses", "VAULT_MASTER_KEY": "set-me" } } } } ``` Run it from the cloned repository until the npm package is published (see [Install](/docs/install)). The local server has the same six tools as hosted, `setup` and `http_request` included; approve an agent with `vault grant --secret NAME --agent AGENT --tool http_request`, where the agent is the name Cursor sends on `initialize`. Values still never reach the model. ## Stdio proxy to the hosted service If you need stdio but want the hosted vault: ```bash export VAULT_PUBLIC_URL=https://botpasses.com npx vault login export VAULT_USER_JWT=eyJ... npx vault mcp --user-jwt ``` Cursor's OAuth loopback uses port 8787. Botpasses never binds it; the local product server listens on 8788. ## Revoke Remove the entry from `mcp.json` to disconnect locally. Revoke the agent in the console **Access** panel to invalidate the tokens Cursor already holds. # Connect ChatGPT ChatGPT connects to remote MCP servers as **connectors**. Remote MCP in ChatGPT requires OAuth 2.1 with dynamic client registration on the server side, and botpasses.com provides both. You never paste a token into ChatGPT. Connectors are available on plans that support them; check your workspace settings if the option is missing. ## Add the connector 1. In ChatGPT, open **Settings**, then **Connectors** (developer mode may need to be on for custom connectors). 2. Choose **Create** (or **Add custom connector**). 3. Name: `Botpasses`. MCP server URL: `https://botpasses.com/mcp`. Authentication: **OAuth**. Leave client ID and secret empty; ChatGPT registers itself with botpasses.com. 4. Create, then connect. Sign in on botpasses.com with your email code and authenticator, and allow the connection on the consent page. 5. In a chat, enable the Botpasses connector from the tools menu. ## Use it Ask for the task in plain language. ChatGPT calls `http_request`; Botpasses attaches the credential and returns a redacted response. - Pending approval: ChatGPT shows an 8-digit code. Approve in the [console](/console) Inbox or with the code, then retry. - Missing credential: ChatGPT shows a `collect_url`. Open it, sign in, and type the key on botpasses.com. Never paste it into the chat. After the connector is on, paste a [hosted bootstrap prompt](/docs/prompts#hosted) for help storing credentials and connecting API accounts. Enter secrets on the Botpasses site you connected to. Do not paste them into chat. For a laptop CLI vault, use the [local prompt](/docs/prompts#local). The laptop hosted kernel is `npm run hosted:dev` at `http://127.0.0.1:8788/mcp`; provider apps get `http://127.0.0.1:8788/connect/callback`. See [Install](/docs/install). ## Notes - Access tokens are short-lived JWTs bound to `https://botpasses.com/mcp`. ChatGPT refreshes them; refresh tokens rotate. - Each connected agent is bound to one vault environment and only sees credentials in it. - Revoke ChatGPT from the console **Access** panel. Its tokens stop working on the next call. The old page at `/docs/connect/openai` redirects here. # Connect Grok Grok Bot can connect with interactive OAuth (Authorize opens a browser on botpasses.com) or with a console-issued **model token** (`avm_...`). Use OAuth when the host shows a connect card. Use the token when you want to skip the card. ## 1a. Grok Bot with OAuth (Authorize) 1. Add a custom connector with Server URL `https://botpasses.com/mcp` and no Authorization header. 2. Click **Authorize**. The host should open a browser to botpasses.com. 3. Sign in, Allow the client, and wait for the redirect back (`https://www.cursor.com/agents/mcp/oauth/callback`, or `grokbot://` / `http://localhost` on a desktop host). 4. The connector should show tools. Ask in plain language ("get my Spotify profile"). Approve in the Inbox if asked. If Authorize spins and falls back to Retry with no browser, remove the connector and add it again after botpasses.com is on a build that 401s unauthenticated `initialize`. A 200 handshake leaves the host with no auth URL. After the connector is on, paste a [hosted bootstrap prompt](/docs/prompts#hosted) for help storing credentials and connecting API accounts. Enter secrets on the Botpasses site you connected to. Do not paste them into chat. If `tools/call` fails with `redirect_uri`, paste the [Grok redirect_uri prompt](/docs/prompts#grok-redirect_uri). Grok Bot in a cloud VM cannot reach `npm run hosted:dev` on your laptop; use botpasses.com or a public self-host origin. ## 1b. Grok Bot with a model token 1. Open the [console](/console) **Access** panel. 2. Choose **Issue token**, give the agent a name (for example `grok`), and pick its environment. 3. Copy the token. It is shown once. The Access list keeps only the last four characters. If you lose it, **Rotate** issues a new one. 4. Server URL: `https://botpasses.com/mcp` 5. Header: `Authorization: Bearer avm_...` (Grok adds the `Bearer` scheme if you enter only the token) Save the connector. The header is enough: `initialize`, `tools/list`, and `http_request` all work without a connect card. Put the `avm_` token in the client's secret or env field, not in chat. Grok Bot runs in a cloud VM, so a local stdio server is not reachable. Use the remote URL. ## 2. Grok Build (CLI) ```bash export BOTPASSES_MODEL_TOKEN=avm_... grok mcp add --transport http botpasses https://botpasses.com/mcp \ --header "Authorization: Bearer ${BOTPASSES_MODEL_TOKEN}" ``` Then paste a [hosted bootstrap prompt](/docs/prompts#hosted). Keep the token in the env field, not in chat. ## What not to paste Do not paste an operator session, an OAuth client secret, or any API key into Grok. The model token and the OAuth access token stay in the host. The model can list names, request approvals, and call `http_request`. ## Spotify and other OAuth apps Client secrets, token minting, and user connect are covered in [Use an OAuth client secret](/docs/how-to/use-an-oauth-client-secret). ## Revoke Revoke the Grok agent in the console **Access** panel. The next call is 401. # MCP tools Hosted MCP is `POST https://botpasses.com/mcp` (staging: `https://staging.botpasses.com/mcp`). Protocol version `2024-11-05`, `serverInfo.name` is `botpasses`. No tool returns a secret. There is no `get_secret`, `read_value`, `reveal_secret`, or `revoke_grant`. The primary tool is `http_request`. The earlier name `http.request` is accepted as an alias for one release; new integrations should use `http_request`. ## Auth | Credential | How it is obtained | Notes | | --- | --- | --- | | OAuth access token (JWT) | Client completes OAuth on botpasses.com (PKCE S256, dynamic client registration) | `aud` is exactly `https://botpasses.com/mcp`, lifetime 600 s, refresh rotation | | Model token `avm_...` | Issued once in the console Access panel | Sent as `Authorization: Bearer avm_...`. Sufficient for every method | Unauthenticated `POST /mcp` (including `initialize`, `ping`, `tools/list`, and `tools/call`) and `GET /mcp` (the SSE stream) are 401 with `WWW-Authenticate` carrying `resource_metadata` for `/.well-known/oauth-protected-resource/mcp` and `scope="mcp"`. Grok Bot builds the Authorize URL from a 401 on `initialize`. A preconfigured `avm_...` Bearer is enough for every method and skips the connect card. Trusted runtime tokens (`avt_...`) cannot call MCP. Every agent is bound to one vault environment (`staging` or `production`) when it is issued or connected. Tools act in that environment; there is no environment argument. ## JSON-RPC methods | Method | Result | | --- | --- | | `initialize` | `protocolVersion`, `capabilities.tools`, `serverInfo`, and `instructions` (below) | | `ping` | `{}` | | `tools/list` | The six tools with JSON schemas | | `tools/call` | One `text` content item whose text is the JSON payload described per tool | | `notifications/*` | No body, HTTP 202 | Unknown methods return JSON-RPC error `-32601`. Tool exceptions return `-32000` or a result with `isError: true`. `GET /mcp` is an SSE keepalive stream; `GET /mcp/tools` returns the same list as `tools/list`. ## http_request Call a third-party API. Botpasses finds the credential by host or name, requests an approval if this agent has none, attaches the credential, and returns a redacted response. | Argument | Required | Notes | | --- | --- | --- | | `method` | yes | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` | | `path` | yes | `/v1/me`, or a full `https://` URL. The host is taken from the URL when present | | `host` | one of host, item_name, or a URL path | Hostname such as `api.stripe.com` | | `item_name` | one of host, item_name, or a URL path | Exact stored credential name | | `body` | no | Object for POST, PUT, PATCH. JSON by default; form-encoded when `content_type` says so | | `content_type` | no | `application/json` or `application/x-www-form-urlencoded`. Anything else is refused | | `client_id` | no | Public OAuth client ID when the credential is a client secret | | `task_description` | no | Shown to the operator in the Inbox, truncated to 500 characters | | `timeout_ms` | no | Origin deadline in ms, 1000 to 30000 (default 10000) | | `dry_run` | no | `true`: report which credential and approval would be used without calling the API. `reason` is `scope_denied` when the approval would not cover the call | The path must start with `/` and is validated once: backslashes, percent-encoded `/` or `.` inside a segment, and `.` or `..` segments are refused. That validated path is what the operator approves. One exception on the wire: for `api.spotify.com`, playlist content calls on `/v1/playlists/{id}/tracks` (`GET`, `POST`, `PUT`, `DELETE`) are sent as `/v1/playlists/{id}/items` (Spotify February 2026 rename). Other `/tracks` paths are unchanged. A `DELETE` body that still uses the `tracks` key is sent with `items`. The result includes `path_rewritten` and `next.for_model` names the change. Botpasses connects on port 443 only; a URL with another port is refused with a hint. Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "http_request", "arguments": { "method": "GET", "path": "https://api.stripe.com/v1/balance", "task_description": "Check the account balance" } } } ``` Success payload: the origin HTTP `status`, the redacted `body` as a string, and sometimes `hint` and `next`. ```json { "status": 200, "body": "{\"object\":\"balance\",\"available\":[{\"amount\":125000,\"currency\":\"usd\"}]}", "next": { "for_model": "Answer the user from body." } } ``` Other payloads: | Payload | Meaning | | --- | --- | | `grant_id`, `status: "pending"`, `approval_code`, `item_name`, `notify_failed` | The agent has no active approval. The operator approves in the Inbox or with the 8-digit code. Not an MCP error | | `status: "need_item"`, `collect_url`, `suggested_name`, `host`, `need_id`, `message` | Nothing stored for that host. The operator opens `collect_url` and types the value. Not an MCP error | | `status: "user_connect_required"`, `provider`, `item_name`, `refresh_item_name`, `connect_url`, `need_id`, `hint` | A path that answers only for a signed-in user (Spotify `/v1/me`) was called with the app credential and no connected account. Nothing was sent and no approval was spent. The operator opens `connect_url` (a console link that also shows the request in the Inbox), connects their account, and allows the agent to use it; the agent then retries the same call once. Not an MCP error | | `host_mismatch` | The named credential is not allowed for that host. MCP `isError: true` | Connector rules: exact `allowed_hosts` match, no IP literals, DNS resolved and pinned to public addresses, no redirects followed, a 10 s default deadline (`timeout_ms`, 1000 to 30000), the credential and any minted access tokens replaced with `[redacted]` in the body and in `origin_headers`. Responses over 1 MiB are cut at the socket and returned as `origin_status: 502` with `{ "error": "body_too_large" }`; do not retry the same call, narrow it (pagination, a smaller page, a fields filter) first. A bodiless answer (204, 304) comes back with an empty `body`. Paths that need a user token exchange the stored `_REFRESH` item with the client secret item's credentials, placed as the provider expects (HTTP Basic or `client_id` and `client_secret` form fields); both items must allow the provider token host. An agent approved on `_REFRESH` alone can post it to the provider token endpoint itself: Botpasses adds the client secret from the sibling `_SECRET` (or ``) in the same environment, returns the redacted token body with `refreshed: true` and `token_last4`, stores a rotated refresh token in place, and audits both items. Without that sibling, only a PKCE provider (Spotify, Google) is sent the public-client exchange; the others get `status: "inject_denied"` with `missing_item` naming the item to store, not a vendor `invalid_client`. The model sees at most 256 KiB of text. A `prompt` approval is one call: any answer from the API spends it, including 401, 410, or a 5xx. It comes back only when the request never left Botpasses (a host mismatch, `user_connect_required`, or a DNS, connect, or TLS failure). When the agent needs to retry, approve with limits (max calls or a duration) instead. ## find_items Look up a stored credential by exact `item_name` and/or exact API `host`. Prefer `http_request`, which does this for you. | Argument | Required | | --- | --- | | `item_name` | name and/or host | | `host` | name and/or host | | `task_description` | no | | `status` | Fields | `isError` | | --- | --- | --- | | `found` | `item`: `name`, `kind`, `last4`, `allowed_hosts`, `inject`, `environment` | no | | `ambiguous` | `items` (up to five, `truncated` when more) | no | | `need_item` | `collect_url`, `suggested_name`, `host`, `client_name`, `need_id`, `message`, optional `recipe` | no | | `host_mismatch` | `item` | yes | Calling with neither name nor host is not an error and does not dump the vault. New `need_item` rows share the 30 per hour limit with `request_grant`. ## setup When the user asked to set up, store, or connect credentials for a provider or API and is not asking for data yet. Pass exactly one of `provider` (`spotify`, `github`, `google`, `slack`, `stripe`) or `host`. Both, even when they agree, is refused. Optional `task_description` and `dry_run`. Never a secret argument. | `status` | Meaning | | --- | --- | | `need_item` | Nothing stored. Open `collect_url` on botpasses.com. Not an MCP error | | `user_connect_required` | App secret stored; open `connect_url` to connect the user account | | `ready` | Standing approval (and Connect when the recipe needs it). Call `http_request` | | `ready_prompt` | Credential stored. Ask the operator to click Always-allow in Inbox. Do not invent a second Collect URL | | `ambiguous` | More than one stored credential matches those hosts | Results include a public `recipe` when the host is known, `steps[]`, and `next`. After Collect, call `setup` again. After `ready`, call `http_request`. A leftover host-named item (for example `API_SPOTIFY_COM` bearer) is reused as `ready` or `ready_prompt`; setup does not create `SPOTIFY_SECRET` beside it or start Connect. ## list_items Inventory of credentials in the agent's environment: `name`, `kind`, `last4`, `username`, `environment`, `inject`. No values, no ciphertext. ## request_grant Requires `item_name`; optional `task_description`. Creates a pending approval and returns the public grant plus `approval_code` and `notify_failed`. Standing policies may activate it at once when they cover the stated host, method, and path. A standing grant that does not cover the call does not come back as `active`; a pending grant is created for that scope. Public grant fields: `grant_id`, `policy`, `status`, `environment_id`, `expires_at`, `created_at`, `approved_at`, `consumed_at`, `task_id`, `task_description`. ## list_grants Approvals for this agent only. Same public grant fields. No values. ## The next field Most results include `next.for_model`, and often `next.tool` and `next.arguments` (the host, method, path, or item_name to retry with). Clients that follow `next` never need the user to explain Botpasses.
Instructions the server sends to the model The `initialize` response carries these instructions. They are written for the model, not for people, and are reproduced here so you know what your agent is told. > You can call third-party APIs (Spotify, Stripe, GitHub, and the rest) through Botpasses. The user does not need to say Botpasses or name a tool. You never see secret values. When the user wants data from an API, call http_request in the same turn. Do not list_items or find_items first. Do not ask which tool to use. When the user asks to set up, store, or connect credentials and is not asking for data yet, call setup with provider or host. Give them collect_url or connect_url. Do not ask them to paste a secret. Botpasses finds the credential, asks the operator to grant if needed, and attaches it. Follow next.for_model. Retry with next.arguments when present. If the result has collect_url, tell the user to open that Botpasses page and enter the key there. Do not ask them to paste a secret into this chat. Then retry http_request. If the result has approval_code or grant status pending, tell them to approve in the Botpasses inbox. Then retry http_request with next.arguments. There is no get_secret. Never put a secret in a tool argument. A Client Secret is not a user access token. On 4xx, fix the path, query, or body and retry http_request with next.arguments; on 5xx retry once, except body_too_large, which means the answer was over 1 MiB: narrow the call (pagination, a smaller page, a fields filter) before calling again. A one-call approval is spent by any answer from the API; if the retry returns a pending grant, tell the user to approve it.
## What never appears - Secret values, ciphertext, raw JWTs, or minted access tokens - A `get_secret`, `read_value`, or `revoke_grant` tool - A collect URL that encodes the secret (`collect_url` is a path on botpasses.com with no signature) ## Local MCP (SQLite) `npx vault mcp` (stdio, SQLite) exposes the same six tools as hosted with the same argument shapes: `list_items`, `find_items`, `request_grant`, `list_grants`, `setup`, and `http_request`. Local `setup` returns a `vault set` command instead of `collect_url`. `list_secrets` and `http.request` are accepted as aliases for one release. The agent id is the MCP client's name from `initialize`; an argument the tool does not define (such as `agent_id`) is refused with JSON-RPC error `-32602`. Over `POST /mcp` on `vault serve`, `initialize` opens a session (the `Mcp-Session-Id` response header, sent back on every later frame), so each connected client keeps its own agent id and approvals. `http_request` needs an active grant for that credential, agent, and tool (`vault grant --secret NAME --agent A --tool http_request`), decrypts in-process, and calls the same connector, so results are redacted the same way and `client_id`, `timeout_ms`, and `dry_run` work as on hosted. Store hosts, an inject mode (the same vocabulary as hosted), and a username with `vault set NAME --host api.example.com --inject basic --username svc`. A miss returns `need_item` with a message to run `vault set`; there is no `collect_url` locally. A provider user-only path called with a `client_credentials` item and no local `_REFRESH` is `user_connect_required` without `connect_url`; the hint names the `vault set _REFRESH --inject refresh` command. ## Result shape A successful call returns `origin_status` (the API's HTTP status), a redacted `body`, and `origin_headers` limited to `content-type`, `link`, `retry-after`, `x-ratelimit-*`, and `x-request-id`. `status` duplicates `origin_status` for one release. With `dry_run: true` the result names the credential, host, method, path, approval state, inject mode, and provider, and nothing is sent. ## Providers and inject modes Botpasses mints and refreshes OAuth tokens for Spotify, GitHub, Google, Slack, and Stripe Connect; pass `client_id` when the credential is an OAuth client secret. Credentials can be sent as `bearer`, `basic`, `header:`, `query:`, `cookie:`, AWS `sigv4`, or a request signature (`hmac:stripe_sig`, `hmac:slack_sig`, `hmac:github_sig`). Unknown modes are refused when stored. ## Related - [HTTP API](/docs/reference/http-api): the routes that wrap the same vault. - [Rate limits](/docs/reference/rate-limits). - [Troubleshooting](/docs/troubleshooting): `host_mismatch`, pending approvals, connect cards. # HTTP API Hosted JSON lives under `/api`. MCP is `POST /mcp` (see [MCP tools](/docs/reference/mcp-tools)). OAuth is `/oauth/*` on botpasses.com, which is its own authorization server. Local `npx vault serve` is a smaller loopback API, listed at the end. Operator JSON never includes secret values after submit. Model tokens cannot resolve a value. Trusted tokens cannot call MCP. JSON bodies must be sent as `Content-Type: application/json` (anything else is 415); bodies over 128 KiB are 413. ## Who may call what | Who | Credential | May call | | --- | --- | --- | | Operator | HttpOnly session cookie after email code and authenticator. Send `X-CSRF-Token` on POST and DELETE | `/api/*` operator routes, Inbox, Access, approve, revoke | | Model (agent) | OAuth access JWT (`aud` is `https://botpasses.com/mcp`; its `org_id` is the organisation the operator consented in, and the operator must still be a member) or a console-issued `avm_...` bearer | `POST /mcp`, `GET /mcp`, `GET /mcp/tools`, `POST /api/grants/request` | | Trusted runtime | `avt_...` bearer | `POST /runtime/resolve` only | A session that has not completed the authenticator step gets 403 `{ "error": "mfa_required", "enroll_url": "/enroll-totp" }` when no authenticator is enrolled, or `{ "error": "mfa_required", "verify_url": "/verify-totp" }` when one is and this session has not passed it yet. Ids from another organisation are 404. A foreign browser `Origin` on `/api`, `/console`, `/collect`, and auth is 403 with no CORS allow header. `/mcp`, the well-known documents, and `/oauth` reflect the caller's Origin so browser-based MCP hosts can connect. Public site GET/HEAD/OPTIONS (pages, `/og.png`, `/logo.png`, `/favicon.ico`) also reflect a foreign Origin so a composer or unfurler can load the card and icon. Invalid bearer on public HTML is ignored (the page loads); the same bearer on `/api` or `POST /mcp` is 401. ## Health and discovery | Method | Path | Notes | | --- | --- | --- | | GET | `/health` | `{ "ok": true, "product": "botpasses" }`. No key fingerprint | | GET | `/ready` | 200 or 503 from the database ping | | GET | `/.well-known/oauth-protected-resource` | `resource` is `https://botpasses.com/mcp`. Same document at the `/mcp` path-aware URLs | | GET | `/.well-known/oauth-authorization-server` | RFC 8414. `response_types_supported` is `["code"]`, PKCE `S256` only. Also at `/.well-known/openid-configuration` | | GET | `/robots.txt` | Production disallows console, auth, collect, api, mcp, oauth. Lists `Sitemap: https://botpasses.com/sitemap.xml`. Comments point at `/llms.txt` and `/llms-full.txt` | | GET | `/sitemap.xml` | Public urlset of every canonical page | | GET | `/llms.txt`, `/llms-full.txt` | Machine-readable site index and full docs for AI crawlers | | GET | `/.well-known/security.txt`, `/security.txt` | Vulnerability contact. Same bytes at both paths. See [Security disclosure](/docs/security/disclosure) | | GET | `/favicon.ico`, `/apple-touch-icon.png`, `/favicon.svg`, `/mark.svg`, `/mark-on-dark.svg`, `/og.png`, `/og-square.png`, `/logo.png`, `/android-chrome-192x192.png`, `/android-chrome-512x512.png`, `/site.webmanifest` | Browser, iOS, Android, and social-card URLs. `/mark.svg` is the navy ticket. `/mark-on-dark.svg` is the white ticket. `/favicon.svg` is the simplified 16 px tile. `/logo.png` is the square favicon raster; `/og.png` is the 1200x630 card | ## Sign-in and account | Method | Path | Body | | --- | --- | --- | | GET | `/sign-in`, `/sign-up` | HTML. A ready session redirects to `/console`; a verified email without an authenticator redirects to `/enroll-totp`; an enrolled account that has not passed the authenticator step this session redirects to `/verify-totp` | | GET | `/enroll-totp` | HTML. Session required. Shows backup codes once after confirm. A ready session that started a re-enroll from the console lands here and sees the pending QR | | GET | `/verify-totp` | HTML. The sign-in authenticator step: one field for an authenticator code or a backup code | | POST | `/api/auth/otp/send` | `{ "email" }`. Returns `{ "ok": true }` for known and unknown emails. Sending again replaces the previous code; five sends per email in 15 minutes | | POST | `/api/auth/otp/verify` | `{ "email", "otp" }`. Sets a pending session cookie. Returns `{ "ok", "enroll", "verify" }`: `enroll` means go to `/enroll-totp`, `verify` means go to `/verify-totp`. A 401 carries `attempts_remaining` | | POST | `/api/auth/totp/start` | Begin authenticator enrollment. Returns `otpauth_url` and a locally rendered `qr_svg`. Re-enrolling needs a ready session and `{ "current_code" }` | | GET | `/api/auth/totp/pending` | The enrollment in flight (`otpauth_url`, `qr_svg`) while it is live, 10 minutes from `start`; 404 `no_pending_enrollment` otherwise. The enroll page reads this before calling `start`, so a reload or a re-enroll shows the secret that `confirm` will check | | POST | `/api/auth/totp/confirm` | `{ "code" }`. Marks the session ready and returns backup codes once | | POST | `/api/auth/totp/verify` | `{ "code" }`. The sign-in authenticator step: an authenticator code or an unused backup code. Marks the session ready. Ten failures lock the account for 15 minutes (429 with `retry_after`) | | GET | `/api/auth/me` | `{ "email", "totp_enabled", "backup_codes_remaining", "created_at" }` | | POST | `/api/auth/backup-codes/regenerate` | `{ "code" }`. Replaces every unused backup code and returns the new set once | | POST | `/api/auth/logout` | Clears the session cookies and ends the OAuth sign-in session. Needs `X-CSRF-Token` | | POST | `/api/orgs` | `{ "name" }` (1 to 80 characters). Creates an organisation for the signed-in user after the authenticator step; up to ten owned organisations per account (402 `plan_limit`) | | DELETE | `/api/orgs` | `{ "confirm_name" }`. Deletes the organisation and everything in it | Email codes are 8 digits, valid 10 minutes, single use; five wrong attempts end the challenge. Authenticator codes are RFC 6238 (SHA-1, 6 digits) with replay protection. Authenticator secrets are encrypted under a key that `vault kek-rotate` re-wraps. ## Items and collect | Method | Path | Notes | | --- | --- | --- | | GET | `/api/items` | Names, last-4, hosts, inject. No values. Omit `environment` for every environment this plane serves. On staging, `environment=production` is 404 | | POST | `/api/items` | `name`, `value`, `environment`, `kind` (`secret`, `client_secret`, `login`), `allowed_hosts`, `inject` (`bearer`, `client_credentials`, `basic`, `header:Name`), optional `username`, `folder_name`. A `client_secret` needs the client ID in `username` | | POST | `/api/items/:id` | Edit name, kind, environment, hosts, client ID, inject. `value` optional; blank keeps the current secret | | POST | `/api/items/:id/meta` | Same as `POST /api/items/:id` | | POST | `/api/items/:id/rotate` | `{ "value" }`. Replaces the value only | | DELETE | `/api/items/:id` | Removes the credential and its approvals | | POST | `/api/folders` | `{ "environment", "name" }` | | GET | `/api/need-items/:id` | Operator only. Metadata of a pending collect request, including `recipe` when the host matches a known provider. Unauthenticated, or a request from another organization, is 404 | | POST | `/api/need-items/:id/fulfill` | `{ "value", "name?", "allowed_hosts?", "inject?", "kind?", "username?", "always_allow?" }`. Stores the value from the collect page and activates the requesting agent's approval. `always_allow: true` writes a standing approval for that agent in the same transaction. A `connect` request (an agent needs a provider account) is 409: it is answered from the Inbox card, not typed in | | POST | `/api/need-items/:id/deny` | Denies a pending request from the Inbox | | GET | `/collect/:id` | HTML shell for the collect page. Details load only for a signed-in operator | | POST | `/api/integrations/:provider/start` | `:provider` is `spotify`, `github`, `google`, `slack`, or `stripe` (unknown is 404). `{ "item_name", "environment?", "client_id?", "redirect_uri?", "agent_client_id?", "need_id?" }`. Returns `authorize_url`, `redirect_uri`, and `provider` for a user connect. `agent_client_id` names one agent that gets a standing approval on the source credential and the refresh item after connect; `need_id` closes the Inbox request the connect answers | | GET | `/connect/callback` | Operator cookie, authenticator step passed. Exchanges the code, stores the refresh token as `_REFRESH`, redirects to `/console#vault?connected=` or `?connect_error=&reason=` (`state_expired`, `provider_denied`, `exchange_failed`, or `no_refresh_token`). The provider id is in the sealed `state`. `/integrations/:provider/callback` is the expand-only alias for apps that already registered that URI | Names match `[A-Z][A-Z0-9_]{0,127}`. A duplicate name is 409. An empty value is 400. ## Clients, grants, inbox | Method | Path | Notes | | --- | --- | --- | | POST | `/api/clients/model` | Issues an `avm_...` once, plus `mcp_url`. The token is not listed later | | POST | `/api/clients/trusted` | Issues an `avt_...` once for `/runtime/resolve` | | POST | `/api/clients/:id/rotate` | New token once. The old hash stops working | | POST | `/api/clients/:id/revoke` | Later bearer calls are 401; OAuth tokens are denylisted; approvals revoked. Every OAuth grant this organisation's members gave for the client is destroyed; the same person's grants for it in other organisations survive | | POST | `/api/grants/request` | Model or operator. `{ "item_name", "task_description?" }`. Optional `host`, `method`, `path`. Returns the covering grant, or a pending grant when standing scope does not admit the call. 30 per organisation per hour | | POST | `/api/grants/:id/approve` | `{ "policy", "confirm_name?" }`. `folder_standing` is owner-only and needs `confirm_name` | | POST | `/api/grants/:id/revoke` | Status becomes `revoked`. The row stays listed | | POST | `/api/grants/approve-by-code` | `{ "code" }`, 8 digits. Reuse is 409, expired is 410 | | GET, POST | `/approve?token=...` | Email approval link. Operator session required; the token is single use | | GET | `/api/inbox` | Pending approvals and requests. Each request has a `kind`: `secret` (typed on the collect page) or `connect` (an agent needs a provider account for a stored client secret; the card's Connect button opens the connect dialog with that agent pre-selected) | | GET | `/api/audit` | Actions, names, and `host` on `auto_approved`. Optional `client_id` and `item_name` filters. No values | Policies: `prompt` (one successful call, then consumed; a failed call keeps it usable), `session` (8 hours), `item_standing`, `folder_standing`. ## Access | Method | Path | Notes | | --- | --- | --- | | GET | `/api/access` | Live snapshot: operators, clients, grants, sessions, with created, first and last access, fetched names, and bearer last-4. Grants include `item_id` to distinguish same-name credentials in different environments. No ledger array, no tokens | | GET | `/api/access/events` | Issuance ledger, newest first, limit 200. `jti` is hashed | | POST | `/api/sessions/:id/revoke` | Only sessions acting in this organisation are listed and revocable. 400 `cannot_revoke_current` for the session making the call | | POST | `/api/sessions/revoke-others` | Ends every other operator session | ## Trusted resolve `POST /runtime/resolve` with an `avt_...` bearer and `{ "item_name": "STRIPE_KEY" }` returns the plaintext to the trusted process. This is the hosted equivalent of `vault run`, and it is the inject. A model token is 403. ## MCP `POST /mcp` JSON-RPC with `http_request`, `find_items`, `list_items`, `request_grant`, `list_grants`. There is no `get_secret`. Unauthenticated `POST /mcp` (including `initialize`) and `GET /mcp` (SSE keepalive) are 401 with `WWW-Authenticate` `resource_metadata` and `scope="mcp"`, so Streamable HTTP hosts including Grok Bot can start OAuth. `GET /mcp/tools` returns the tool list. See [MCP tools](/docs/reference/mcp-tools). ## OAuth (botpasses.com is the authorization server) PKCE S256 is required. Clients are public (`token_endpoint_auth_method` `none`; a registration that asks for a client secret is refused) and use the `code` response type. Access tokens are RS256 JWTs with audience `https://botpasses.com/mcp`, a 600 second lifetime, and an `org_id` claim naming the organisation the operator consented in. Refresh tokens rotate. | Path | Role | | --- | --- | | `/oauth/authorize` | Authorization code. The operator signs in and consents in the browser; the consent is bound to the organisation their session is acting in | | `/oauth/token` | Code or refresh token to JWT. A refresh for an agent that was revoked in the console is `invalid_grant`; only a new consent brings the agent back | | `/oauth/jwks` | RS256 public keys. Two are listed while a key rotation is in progress; new tokens are signed with the first | | `/oauth/register` | Dynamic client registration. `redirect_uris` may be `https`, RFC 8252 loopback `http` (`127.0.0.1`, `[::1]`, `localhost`), or a desktop scheme such as `cursor://` or `grokbot://`. `javascript:`, `data:`, and `file:` are rejected. 20 per IP per hour | | `/oauth/device/auth` | RFC 8628 device code. The operator finishes at `/device` (10 code attempts per 15 minutes per IP) | | `/oauth/revoke` | RFC 7009. Revoking a refresh token also revokes every access token issued with it; a JWT access token is revoked by its `jti`. Marked in the Access ledger | | `/consent`, `/device` | HTML pages for the two flows above. The consent page's script posts `{ "uid", "decision" }` with `X-CSRF-Token` and `Accept: application/json` and gets `200 { "location" }` to navigate to; a caller with the CSRF header and no such `Accept` gets a 303 to the same resume URL. A form submit without the header (scripts off) is 403 | ## Local vault serve (loopback) `npx vault serve` listens on `127.0.0.1:8788` and prints two loopback bearers. Send the operator bearer as `Authorization` on `/api/*` (the console stores it) and the model bearer on `POST /mcp` (`vault mcp --remote` sends it). Each is refused on the other surface, so an MCP client cannot approve its own requests. The server answers only to a loopback `Host`. Each MCP client gets its own session: `initialize` answers with an `Mcp-Session-Id` header, and every later frame must send it back (400 without it, 404 once the server no longer holds it, after 8 idle hours, so the client initializes again). Two clients on one server keep separate agent ids and separate approvals. `vault mcp --remote` handles the header for stdio clients. | Path | Role | | --- | --- | | `GET /` | Local operator console | | `GET /health` | `{ ok, product }` | | `GET /api/secrets` | Names, last-4, hosts, inject mode, username as `{ secrets }` | | `GET /api/items` | The same list as `{ items }`, the hosted key | | `POST /api/secrets` | Store `{ name, value, allowed_hosts?, inject?, username? }`, answered as `{ secret }`. `username` is the HTTP Basic user, OAuth client id, or AWS access key id | | `POST /api/items` | The same store, answered as `{ item }` | | `GET /api/grants` | Grant metadata | | `POST /api/grants/request` | Request a pending grant | | `POST /api/grants` | Approve (`scope` is `once` or `session`; `tool_id` is `http_request` for agent calls) | | `POST /api/grants/:id/revoke` | Revoke | | `GET /api/audit` | Events, no values | | `POST /mcp` | Local MCP JSON-RPC, the same six tools as hosted | There is no OAuth, provider connect, item edit or rotate route, or Access panel on the local plane; `/api/items` only lists and stores. ## Status codes | Status | When | | --- | --- | | 401 | Missing or invalid session or bearer | | 403 | CSRF, `mfa_required`, wrong channel (a model token on an operator route), foreign Origin on `/api`, `/console`, `/collect`, and auth | | 404 | Unknown id, or an id from another organisation | | 409 | Duplicate credential name, reused approval code | | 410 | Expired approval | | 413 | Body over 128 KiB | | 415 | A JSON route was sent a body that is not `application/json` | | 429 | Rate limit. See [Rate limits](/docs/reference/rate-limits) | # CLI Run `npx vault` from the cloned repository (or `npm run vault -- `) until the npm package is published. Process environment names are `VAULT_*`. The local server listens on **8788**. `vault --help` prints the usage for every command. ## Environment | Variable | Purpose | | --- | --- | | `VAULT_HOME` | Vault directory. Default `$HOME/.botpasses` | | `VAULT_MASTER_KEY` | 32 bytes as 64 hex characters (or standard base64). Generated by `vault init` into `master.key` when unset | | `VAULT_PUBLIC_URL` | Hosted origin for `vault login` and `vault mcp --user-jwt`. First-party botpasses.com, a self-host `https` origin, or loopback on plane `dev` | | `VAULT_USER_JWT` | Access token used by `vault mcp --user-jwt` | ## Commands | Command | Purpose | | --- | --- | | `vault init` | Create `$VAULT_HOME`, the SQLite schema, and a master key if needed | | `vault set NAME [--host api.example.com]... [--inject MODE] [--username USER]` | Read the value from stdin (or a no-echo prompt on a terminal), encrypt, and store. There is no `--value` flag: argv is visible to every process. Prints the name and last-4, never the value. `--host` allowlists the APIs `http_request` may send it to; `--inject` takes the same modes as hosted (`bearer`, `basic`, `client_credentials`, `refresh`, `sigv4`, `header:Name`, `query:param`, `cookie:name`, `hmac:...`); `--username` is the HTTP Basic user, OAuth client id, or AWS access key id | | `vault list` | Names and last-4 | | `vault grant --secret NAME --agent A --tool T [--once \| --session [--ttl 8h]]` | Approve a pending request. `--once` (the default) is spent by the first use and has no expiry. `--session` stays active until `--ttl` elapses (default 8h). `--ttl` applies to `--session` only, and the two scope flags exclude each other; either mistake is refused | | `vault revoke --id GRANT_ID` (or `--secret NAME --agent A --tool T`) | Stop later injects | | `vault audit` | Store, grant, revoke, and inject events. No values | | `vault run --with NAME --agent A --tool T -- CMD` | Inject the value into the child process environment and run the command. Nothing is printed | | `vault serve [--host 127.0.0.1] [--port 8788]` | Loopback HTTP, the local console, and `POST /mcp`. Prints two loopback bearers: the operator bearer for `/api` and the console, the model bearer for `POST /mcp`. Neither opens the other surface. `--port` must be a whole number from 1 to 65535 | | `vault mcp` | MCP over stdio against the local vault (the same six tools as hosted, `setup` and `http_request` included) | | `vault mcp --remote [URL]` | MCP over stdio forwarded to a running `vault serve` (default `http://127.0.0.1:8788`) with the model bearer, derived from the master key so nothing is pasted into a client config. Carries the `Mcp-Session-Id` that `initialize` returned, so the stdio client is one session on the server | | `vault mcp --user-jwt [TOKEN]` | MCP over stdio proxied to the hosted server. The token comes from `--user-jwt TOKEN`, `--user-jwt=TOKEN`, or `VAULT_USER_JWT` when the flag is bare; the flag alone selects hosted mode, so a missing token is an error, never the local vault | | `vault login` | Print the hosted `/sign-in`, `/console`, and `/device` URLs | | `vault kek-wrap`, `vault kek-rotate` | Self-hosting only. Wrap or rotate the platform key with AWS KMS. See [Self-hosting](/docs/self-hosting) | `list`, `grant`, and `audit` never print a secret value. ## Example ```bash export VAULT_HOME="$PWD/.botpasses" npx vault init printf '%s' 'sk_test_example_not_real' | npx vault set STRIPE_KEY npx vault list npx vault grant --secret STRIPE_KEY --agent invoicer --tool stripe --once npx vault run --with STRIPE_KEY --agent invoicer --tool stripe -- \ node -e 'console.log(Boolean(process.env.STRIPE_KEY))' npx vault audit npx vault revoke --secret STRIPE_KEY --agent invoicer --tool stripe ``` ## Hosted login and MCP over stdio ```bash export VAULT_PUBLIC_URL=https://botpasses.com npx vault login export VAULT_USER_JWT=eyJ... npx vault mcp --user-jwt ``` Device login is RFC 8628: after your authenticator, enter the code shown by the CLI at `/device`. ## Hosted server `VAULT_MODE=hosted vault serve` starts the multi-user process (Postgres, KMS, email). It refuses `VAULT_HOME` and exits with code 78 on configuration errors. Required secrets are listed in [Self-hosting](/docs/self-hosting). `npm run hosted:dev` starts the same kernel on loopback with sqlite-hosted (or `--postgres`). It writes `.botpasses-hosted/` and prints OTP codes in the terminal. Run one process per sqlite file (a second writer can get SQLITE_BUSY). The CLI vault callback stays `http://127.0.0.1:8888/callback`; hosted connect uses `{origin}/connect/callback`. See also: [MCP tools](/docs/reference/mcp-tools), [HTTP API](/docs/reference/http-api). # Rate limits and size caps Limits are fixed per organisation or per IP address. Hitting one returns HTTP 429 (or, inside an MCP tool result, an error the agent should show you). Waiting for the window to pass is the fix; there is no override. ## Requests | What | Limit | Scope | | --- | --- | --- | | New approval requests (`request_grant`, `http_request` needing an approval, `POST /api/grants/request`) plus new collect requests (`need_item`) | 30 per hour, combined | Organisation | | Email code sends (`POST /api/auth/otp/send`) | 5 per email per 15 minutes | Email address | | Wrong email codes | 5, then the code is void and a new one must be sent | Challenge | | Wrong approval codes (`POST /api/grants/approve-by-code`) | 5 per approval request, kept when the agent asks again and the code changes; approve from the Inbox instead. 20 attempts per 15 minutes across the organisation | Approval, then organisation | | OAuth dynamic client registration (`POST /oauth/register`) | 20 per hour | IP address | | Device sign-in code attempts (`POST /device`) | 10 per 15 minutes | IP address, and the browser session | | OAuth client metadata document fetches (an `https` `client_id`) | 30 per hour per document host, 10 per hour per requesting IP, 4 in flight at once | Host, IP address, server | | Approval code reuse | A code works once. Reuse is 409, expired is 410 | Code | ## Lifetimes | What | Lifetime | | --- | --- | | Email code | 10 minutes | | `prompt` approval | Until one successful API call | | `session` approval | 8 hours | | `item_standing`, `folder_standing` | Until revoked | | OAuth access token | 600 seconds (30 seconds of clock skew allowed). Refresh tokens rotate on use; revoking one, or replaying a rotated one, ends every access token issued with it | | Operator session | Until sign-out or revoke from the Access panel | ## Sizes and timeouts | What | Cap | | --- | --- | | JSON request body on any route | 128 KiB (413 above) | | Credential name | 128 characters, `[A-Z][A-Z0-9_]{0,127}` | | `task_description` shown in the Inbox | 500 characters (longer text is truncated) | | Origin response returned to the agent | 256 KiB | | Origin call timeout | 10 seconds | | OAuth client metadata document fetch | 1 MiB, 3 seconds | | Access ledger read (`GET /api/access/events`) | 200 newest rows | ## What an agent should do on 429 Tell the operator and stop retrying for the hour. Repeated `request_grant` calls for the same credential do not help; one pending approval is enough and the operator can approve it from the Inbox at any time. # Why the key stays out of the chat ## The problem with secrets in context Anything in a model's context window can end up anywhere the transcript goes: provider logs, a tool result echoed back to the user, a later turn that summarises the conversation, a prompt injection that asks the model to repeat what it knows, or a screenshot. Environment variables are not much better: an agent that can run `env` or read `process.env` can print them. Once a key has been in context, you have to assume it leaked. Rotating it is the only fix. ## What Botpasses does instead Botpasses is a **grant-vault**. Values stay inside the Botpasses process. The model only ever handles names. 1. The operator stores a credential once. It is encrypted at rest. 2. An agent asks for an API call by host, method, and path (`http_request`). It does not ask for the key. 3. Botpasses finds the credential by host, checks that this agent has an **approval** for it, and asks the operator if not. 4. Botpasses decrypts the value inside its own process, attaches it to the outbound HTTPS request (an `Authorization` header, HTTP Basic, or a raw header), and sends the request to the allowed host only. 5. The response comes back with the credential and any minted access tokens replaced by `[redacted]`. The point where the plaintext leaves the vault is called **inject**. On the hosted service the inject targets are the outbound connector call and `POST /runtime/resolve` for trusted (non-model) processes. On the local CLI the inject target is the child process environment of `vault run`. None of these is the model. ## There is no reveal path No MCP tool, HTTP route, CLI command, console screen, email, or audit row returns a stored value. `get_secret` does not exist. Support staff do not have a decrypt tool. Tests in the repository store a canary value and fail if it appears in any MCP result, REST model payload, Inbox JSON, email, collect page, or audit entry. ## Who can decrypt your keys Password managers for people encrypt in the client so the vendor cannot read what you store. Botpasses cannot make that claim: to attach a key to a request, the hosted process has to decrypt it. The parties that can decrypt are the hosted process at inject, and anyone who holds both the AWS KMS role and the database. The model, the chat transcript, logs, and staff without both of those cannot. The design decision is recorded in [ADR 0006](https://github.com/naffis/botpasses/blob/dev/docs/adr/0006-grant-vault-trust-model.md) and the table of which systems can access credentials is on the [Security](/security) page. ## What this does not protect against - An agent that is approved to use a credential can call any endpoint on the allowed host with it. Approvals are per credential and host, not per path or method. Use `prompt` approvals for anything sensitive and revoke standing approvals when a task is done. - The API you call sees the credential, as it must. - A compromised operator account. Protect it with your authenticator and keep the backup codes offline. # Troubleshooting ## host_mismatch **What you see.** The tool result is an MCP error with `status: "host_mismatch"`. The agent named a credential (`item_name`) that is not allowed for the host it called. **Fix.** Either let the agent call by host only (drop `item_name` so Botpasses picks the right credential), or add the host to the credential's **Allowed hosts** in the console (Vault, Edit). Allowed hosts are exact hostnames, so `api.stripe.com` does not cover `files.stripe.com`. ## mfa_required **What you see.** `403 { "error": "mfa_required", "enroll_url": "/enroll-totp" }` from an operator route, or the console sends you to `/enroll-totp`. **Why.** Your session verified an email code but has not completed the authenticator step. Operator actions need both. **Fix.** Open [/enroll-totp](/enroll-totp) and finish enrollment, or sign in again and enter your authenticator code when asked. Agents never see this error; it is operator-only. ## The connect card keeps appearing **Grok Bot Authorize spins, then Retry, no browser.** Unauthenticated `initialize` must be 401 with `WWW-Authenticate` `resource_metadata`. A 200 handshake leaves the host with `no_auth_link`. Remove the connector, add `https://botpasses.com/mcp` again with no header, and complete Authorize in the browser (sign-in on botpasses.com, then Allow). The host should return through `https://www.cursor.com/agents/mcp/oauth/callback`. If you would rather skip the card, put `Authorization: Bearer avm_...` on the connector instead. **Grok with a model token.** The `Authorization: Bearer avm_...` header is enough. `initialize` and `tools/list` succeed without a connect card. If vault calls still fail, the header is missing, revoked, or rotated (Access shows the last four characters). **OAuth clients (Claude, Cursor, ChatGPT).** A card that returns after every session usually means the client could not finish dynamic client registration or the redirect back. Botpasses accepts `https`, RFC 8252 loopback `http` (`127.0.0.1`, `[::1]`, `localhost`), and desktop schemes such as `cursor://` and `grokbot://`. It rejects `javascript:`, `data:`, and `file:`. Remove the server entry in the client, add it again, and complete the browser sign-in on botpasses.com in one go. If the agent was revoked in the Access panel, the client must connect again; that is expected. ## 409 on an approval code **What you see.** `409` from `POST /api/grants/approve-by-code`, or "code already used" in the console. **Why.** An 8-digit approval code works once. If the agent asked twice, the second request has its own code and the first is void. **Fix.** Use the newest code the agent showed, or approve the card in the Inbox instead. After a *failed* API call the agent does not need a new code: a `prompt` approval survives 401, 410, and 5xx responses, and the agent should retry with `next.arguments`. An expired code is 410; ask the agent again. ## need_item and collect_url **What you see.** The agent says nothing is stored for that API and gives you a `collect_url` such as `https://botpasses.com/collect/need_...`. The Inbox shows *Store the credential*. **Fix.** Open the URL (or the Inbox card), sign in, and type the value on botpasses.com. Botpasses stores it with the suggested name and host and gives the requesting agent a one-time approval. Tell the agent to retry. Never paste the value into the chat; the model does not need it and Botpasses never asks for it there. If you already stored the credential and still get `need_item`, the agent is probably in a different environment (`staging` vs `production`) or the stored allowed host does not match the host the agent called. ## 429 rate limit **What you see.** `429` from `POST /api/grants/request` or `POST /api/auth/otp/send`, or a tool error saying the limit was reached. **Why.** 30 new approval and collect requests per organisation per hour; 5 email codes per address per 15 minutes; 20 OAuth registrations per IP per hour. Full list: [Rate limits](/docs/reference/rate-limits). **Fix.** Wait for the window. Approve the pending request that already exists instead of asking the agent to request again. ## hosted:dev connect goes to port 8888 **What you see.** After `npm run hosted:dev`, a provider app redirects to `http://127.0.0.1:8888/callback` and nothing answers. **Why.** Port 8888 is the CLI vault (`vault serve`) callback. The hosted kernel, including the laptop plane, always uses `{origin}/connect/callback`. **Fix.** Add `http://127.0.0.1:8788/connect/callback` on the provider app (or the URI Collect shows). Point the MCP client at `http://127.0.0.1:8788/mcp`. Do not copy a CLI-vault snippet onto a hosted plane. ## SQLITE_BUSY on hosted:dev **What you see.** The laptop hosted kernel logs `SQLITE_BUSY` or refuses a second writer. **Why.** sqlite-hosted is one process per file. A leftover `hosted:dev` or a second `--postgres`-less start shares `.botpasses-hosted/hosted.sqlite`. **Fix.** Stop the other process. Or set `VAULT_HOSTED_SQLITE` to a different path. `npm run hosted:dev` is refused when `FLY_APP_NAME` is set. ## No email code on hosted:dev **What you see.** Sign-in says a code was sent, but nothing arrives in your inbox. **Why.** `npm run hosted:dev` does not send mail. The 8-digit code is printed on the terminal that started it. **Fix.** Read that terminal. Structured logs never contain the code. ## The agent asks me to paste the key Botpasses never asks for a value in chat and tells the model not to. If an agent asks anyway, refuse, and use the `collect_url` or the console instead. Consider revoking that agent from the Access panel if it keeps asking. ## Still stuck Email [support@botpasses.com](mailto:support@botpasses.com) with the tool result (it never contains a value) and the time of the call. Security issues go to [security@botpasses.com](mailto:security@botpasses.com); see [Security disclosure](/docs/security/disclosure). # FAQ ## What is Botpasses? Botpasses stores API keys and lets your agents use them with your approval. It attaches the key to each approved API call and removes secrets from the response. The model does not get the key. There is no `get_secret` tool. ## Is this a password manager? No. Password managers hold secrets for people and fill them into browsers. Botpasses holds credentials for agents and attaches them to API calls the agent asks for. There is no browser autofill, no sharing with other people, and no way to read a value back out. If you need a value on your screen, use a password manager; if you need an agent to call an API without ever holding the key, use Botpasses. ## What if the agent is prompt-injected? The agent still cannot read a value, because no tool returns one. What it can do is call APIs it has an approval for. Limit that: use `prompt` approvals (one call each) for anything sensitive, keep allowed hosts tight, and revoke standing approvals when a task is done. Each API call is in the Access activity log by agent name and credential name. ## What do you log? Actions and names: which agent requested or used which credential, when, and the outcome. Tokens are stored as hashes. Secret values are never written to the audit log, the Inbox, emails, error reports, or server logs. The API you call sees the request as normal. ## How do I revoke? Console **Access** panel. Revoke an agent (its tokens stop working on the next call), a single approval, or a session. See [Revoke access](/docs/how-to/revoke-access). There is no revoke over MCP, so a model cannot undo your revoke or approve itself. ## What is the difference between staging and production? Inside your account, each credential has an **environment** tag (`staging` or `production`) and each agent is bound to one of them; an agent only sees credentials in its own environment. Separately, `staging.botpasses.com` is the pre-release copy of the service with its own accounts and database; use `botpasses.com` unless you are testing the service itself. The laptop hosted kernel (`npm run hosted:dev`) is a third deploy plane. It is not a Fly app and it is refused when `FLY_APP_NAME` is set. ## Can Botpasses staff read my keys? Anyone with both the AWS KMS role and the database can decrypt your keys. The hosted process decrypts them in memory when making approved calls. Staff have no support tool that reveals a key. Botpasses is not zero-knowledge. See the [Security](/security) page. ## What does it cost? Botpasses is free to use and open source under the MIT license. Use [botpasses.com](/sign-up), [self-host](/docs/self-hosting) with Postgres 16 and your own domain, or run it on your laptop with `npm run hosted:dev`. ## Which agents work? Claude (web, desktop, and Claude Code), Cursor, ChatGPT, Grok, and any MCP client that supports remote servers over HTTP. See [Connect an agent](/docs#list-connect). ## How is this different from putting keys in .env or the system prompt? A key pasted into a prompt enters the conversation. A key in `.env` can be exposed if an agent reads the file or prints its environment. Botpasses does not return the stored key to the model. It checks the agent’s approval and attaches the key to the API request. ## How is this different from Composio or Arcade? Those products give the agent a catalog of tools and hold the tokens themselves. With Botpasses, you store the credential and the agent calls MCP `http_request`. Botpasses attaches the key after checking your approval. We do not wrap Stripe or GitHub as first-party tools. ## Where is the source? [github.com/naffis/botpasses](https://github.com/naffis/botpasses). Design decisions are in the `docs/adr` folder. ## Can I run Botpasses on my laptop? Yes. Choose a local console or a CLI vault: - **Hosted kernel on loopback.** From a clone, `npm run hosted:dev` starts the same console, email codes (printed in the terminal), and MCP as botpasses.com, on `http://127.0.0.1:8788`. Connect MCP to `http://127.0.0.1:8788/mcp`. Provider apps get `http://127.0.0.1:8788/connect/callback`, not port 8888. Run one process per sqlite file. - **CLI vault.** `vault init` under `VAULT_HOME`, then `vault mcp`. No account. The CLI vault callback stays `http://127.0.0.1:8888/callback`. Self-hosting a shared plane still needs Postgres 16. See [Install](/docs/install) and [Self-hosting](/docs/self-hosting). # Self-hosting The hosted process is one Node.js service. The runtime contract is a Postgres 16 URL, an `https` origin you control, and a platform key (raw on a laptop; KMS-wrapped in production). The reference deployment is one Fly Machine per plane, Neon Postgres, Cloudflare DNS and WAF in front, and AWS KMS holding the key that wraps the platform key. Any other Postgres 16 and any non-platform-default `https` origin work the same. The code is MIT licensed at [github.com/naffis/botpasses](https://github.com/naffis/botpasses). For a laptop hosted kernel (loopback, sqlite-hosted, OTP printed in the terminal) run `npm run hosted:dev`. That sets `VAULT_DEPLOY_PLANE=dev` and does not inherit a leftover `DATABASE_URL`. Run one process per sqlite file; a second writer can get SQLITE_BUSY. Do not set plane `dev` on Fly. The operational runbooks are in the repository under `docs/ops` ([cutover](https://github.com/naffis/botpasses/blob/dev/docs/ops/botpasses-cutover.md), [KEK rotation](https://github.com/naffis/botpasses/blob/dev/docs/ops/kek-rotation.md), [restore](https://github.com/naffis/botpasses/blob/dev/docs/ops/restore.md)). Paste the [self-host prompt](/docs/prompts#self-host) into an ops agent for help with the configuration on this page and the runbooks in `docs/ops`. Do not paste secret values into chat. ## Copy-paste prompt ``` Help me deploy a self-hosted Botpasses plane from https://github.com/naffis/botpasses. Goal: a private grant-vault my agents hit at our origin (not necessarily botpasses.com). Follow docs/self-hosting and docs/ops in the repo. Do not invent secret values; ask me to set them in the secret store. Reference shape (adapt to our cloud if we are not on Fly): - One Node process: VAULT_MODE=hosted vault serve (one Machine per plane; in-memory enroll/rate-limit state). - Postgres 16 (any vendor): separate database per plane; pooled DATABASE_URL + direct DATABASE_URL_DIRECT. - Edge DNS/WAF (Cloudflare or ours) with SSL full strict. - AWS KMS (or approved KMS) wrapping VAULT_KEK_WRAPPED; VAULT_KMS_APP_ID or FLY_APP_NAME; Fly OIDC or equivalent for AWS_ROLE_ARN. - Email provider for codes (Resend pattern: RESEND_API_KEY + VAULT_EMAIL_FROM). - Optional: R2/S3 encrypted backups, Sentry without values. - Laptop hosted kernel is npm run hosted:dev (VAULT_DEPLOY_PLANE=dev, loopback, sqlite). That plane is refused when FLY_APP_NAME is set. Required config checklist (confirm each is set in secrets, never paste into chat): DATABASE_URL (Postgres URL), DATABASE_URL_DIRECT, VAULT_PUBLIC_URL (our https origin), VAULT_DEPLOY_PLANE (staging|production), VAULT_KEK_WRAPPED, VAULT_KMS_KEY_ID, VAULT_KMS_APP_ID or FLY_APP_NAME, AWS_ROLE_ARN, VAULT_KEK_REQUIRE_KMS=1 after cutover, VAULT_SESSION_SECRET (>=32 bytes), VAULT_OIDC_PRIVATE_JWK, VAULT_APPROVAL_HMAC (64 hex), bootstrap token pair only for break-glass window, VAULT_TRUST_PROXY as appropriate. Build: npm ci npm --prefix site ci && npm --prefix site run build VAULT_MODE=hosted VAULT_BIND_HOST=0.0.0.0 PORT=8788 npx vault serve Exit 78 means config is wrong; fix from the self-hosting table. After /health and /ready pass: 1. Create the first operator account on our VAULT_PUBLIC_URL (email code + TOTP). 2. Issue a model/agent token or complete OAuth for our MCP clients against https:///mcp. 3. Give developers the hosted agent bootstrap prompt, with every botpasses.com URL replaced by our origin (including /connect/callback on provider apps). 4. Write a short internal runbook: who holds KMS, how to revoke an agent, how to rotate KEK (vault kek-rotate), backup restore pointer under docs/ops. Constraints: do not scale to two Machines without moving in-memory state to the database. Do not branch production Postgres from staging. Never log or return credential values. ``` ## Components | Component | Role | | --- | --- | | Fly.io app | Runs `VAULT_MODE=hosted vault serve`. One Machine. Health checks on `/health` and `/ready` | | Neon Postgres | All hosted data. Use a separate project per plane; do not branch production from staging. Pooled URL for the app, direct URL for backups | | Cloudflare | Orange-cloud DNS for the apex and `staging.` host, WAF, SSL Full (strict). `www` is a 301 to the apex | | AWS KMS | A customer managed key the Fly Machine can call through Fly OIDC. It unwraps the platform key at boot | | Resend | Email codes and approval notices from a verified sending domain | | Cloudflare R2 | Encrypted nightly `pg_dump` from a GitHub Actions job | | Sentry (optional) | Error reports. Values are never included | ## Build and run ```bash npm ci npm --prefix site ci && npm --prefix site run build # the marketing site must exist at boot VAULT_MODE=hosted VAULT_BIND_HOST=0.0.0.0 PORT=8788 npx vault serve ``` Hosted mode reads its listen address from `VAULT_BIND_HOST` and `PORT` (or `VAULT_PORT`); the `--host` and `--port` flags belong to the local `vault serve` only. The process exits with code 78 when configuration is wrong: `npm run hosted` without `VAULT_MODE=hosted`, a short session secret, a missing RS256 private JWK, a `VAULT_OIDC_PREVIOUS_JWK` that is not a private RS256 JWK, `RESEND_API_KEY` without `VAULT_EMAIL_FROM`, `VAULT_HOME` set in hosted mode, or a missing `site/dist/index.html`. The Dockerfile in the repository builds the site and installs the CLI in one image. ## Required secrets | Secret | Purpose | | --- | --- | | `DATABASE_URL` | Postgres 16 URL (any vendor). The reference stack uses a Neon pooled (`-pooler`) host | | `DATABASE_URL_DIRECT` | Direct Postgres URL for backups and migrations | | `VAULT_PUBLIC_URL` | The origin this plane serves. First-party hosts must match the plane. A custom `https` origin is allowed on self-hosted staging/production. Loopback is plane `dev` only | | `VAULT_DEPLOY_PLANE` | `staging`, `production`, or `dev`, required. Staging refuses vault environment `production`. Unset is exit 78. `dev` is the laptop hosted kernel and is refused when `FLY_APP_NAME` is set | | `VAULT_KEK_WRAPPED` | The platform key, wrapped by KMS. Produced by `vault kek-wrap` | | `VAULT_KMS_KEY_ID` | The KMS key id or ARN | | `VAULT_KMS_APP_ID` | Optional. KMS EncryptionContext `app` when `FLY_APP_NAME` is unset | | `AWS_ROLE_ARN` | Role the Machine assumes through Fly OIDC; it needs `kms:Decrypt` on that key | | `VAULT_KEK_REQUIRE_KMS` | Set to `1` after the wrapped key is confirmed. Refuses to boot on the raw key | | `VAULT_KEK` | Raw platform key. Pre-cutover fallback only; unset it after `VAULT_KEK_REQUIRE_KMS=1` | | `VAULT_SESSION_SECRET` | 32 bytes or more. Signs sessions and CSRF tokens | | `VAULT_OIDC_PRIVATE_JWK` | RS256 private JWK that signs OAuth access tokens | | `VAULT_OIDC_PREVIOUS_JWK` | Only during a key rotation: the JWK being retired. Published in JWKS and still verifies the tokens it signed; never signs new ones | | `VAULT_APPROVAL_HMAC` | Signs email approval links. 64 hex characters (32 bytes); any other shape is exit 78 | | `VAULT_BOOTSTRAP_TOKEN` | 32 characters or more. Break-glass operator token; keep it offline. Every use is logged as `auth_bootstrap_used` with a token hash | | `VAULT_BOOTSTRAP_ALLOW_PLANE` | Set to `1` only for the break-glass window. Staging and production ignore `VAULT_BOOTSTRAP_TOKEN` unless this is `1`; unset both afterwards | | `VAULT_TRUST_PROXY` | Set to `1` when a proxy you control (nginx, Caddy) sits in front and appends `X-Forwarded-For`; only that last hop is then trusted. `Fly-Client-IP` is trusted only on Fly (`FLY_APP_NAME`), which also implies this setting. Otherwise the socket peer is the caller's address for rate limits and logs | | `RESEND_API_KEY` | Sending-access key scoped to your domain | | `VAULT_EMAIL_FROM` | For example `Botpasses `. Required when `RESEND_API_KEY` is set | | `SENTRY_DSN` | Optional; a plane without it logs `sentry_dsn_missing` at boot | | `VAULT_TRUSTED_PROXY_CIDRS` | Optional. Comma-separated CIDRs of the CDN in front of the plane. `CF-Connecting-IP` is read for rate limits only when the connecting address is inside them. Unset means Cloudflare's published ranges; empty means the header is never read | | `VAULT_BIND_HOST` | Optional. Listen address. Default `127.0.0.1` on plane `dev`, `0.0.0.0` on staging/production | | `PORT` | Optional. Listen port, default `8788`; Fly sets it. `VAULT_PORT` is read when `PORT` is unset | | `VAULT_SITE_ROOT` | Optional. Directory of the built Astro site, default `./site/dist`; boot exits 78 when its `index.html` is missing | Backup job secrets (GitHub Actions, not Fly): `DATABASE_URL_DIRECT`, `BACKUP_KEY` (32-byte hex, must not be the vault key), `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`. The job fails closed when R2 is unset. ## Key hierarchy 1. The AWS KMS key (non-exportable) unwraps `VAULT_KEK_WRAPPED` once at boot. 2. The platform key in process memory wraps one data key per organisation. 3. The organisation key encrypts each credential with AES-256-GCM, bound to the organisation id. A database dump alone cannot decrypt anything. Rotation: `vault kek-rotate` re-wraps the organisation keys under a new platform key. ## DNS Orange-cloud `A` and `AAAA` records for the apex and the staging host, plus a grey-cloud `_fly-ownership` TXT record for Fly's certificate. `www` redirects to the apex at Cloudflare. ## Limits of the reference deployment Some state (pending authenticator enrollments, per-IP limiters) lives in process memory, so the design is one Machine per plane. Do not scale to two Machines without moving that state to the database. # Security disclosure If you find a security problem in Botpasses, the hosted service, or the repository, please tell us privately first. ## Contact Email **[security@botpasses.com](mailto:security@botpasses.com)**. The same address is published in [`/.well-known/security.txt`](/.well-known/security.txt) (RFC 9116). Do not open a public GitHub issue for a vulnerability. ## What to include - What you found and where (URL, route, tool, or file and line). - Steps to reproduce, or a proof of concept. A test that fails is ideal. - The impact you believe it has, in particular whether a secret value could reach a model, a transcript, a log, or a third party. Please do not store real third-party credentials on `botpasses.com` while testing; use `staging.botpasses.com` and throwaway keys. ## What to expect We reply from `security@botpasses.com` to confirm receipt and follow up with what we find. We will tell you when a fix ships and credit you in the [changelog](/changelog) if you want that. We ask that you give us time to fix the issue before publishing details. ## In scope - `botpasses.com` and `staging.botpasses.com`: the console, MCP endpoint, OAuth server, and APIs. - The `botpasses` repository: CLI, hosted server, connector, and crypto. Findings that would let a model, chat transcript, or log see a stored value are the highest priority. The full list of who can and cannot decrypt is on the [Security](/security) page. ## Out of scope - Reports against third-party services we use (Fly, Neon, Cloudflare, Resend, AWS). Report those to the vendor. - Denial of service by volume. - Findings that need a compromised operator device or account.