View source on GitHub
Why you’d want this
OpenHands already lets users define secrets at the user level (stored in the vault). That’s fine for stable, long-lived credentials. But there are cases where you want a secret that is:- Scoped to a single conversation — e.g., a customer’s OAuth token, a temporary CI credential, or a one-off API key the calling system minted for this run.
- Used to authenticate an MCP server, not just exported as a bash variable —
e.g., wire the agent into a customer’s Linear / GitHub / internal API by
passing the right
Authorization: Bearer …header on every MCP call.
The two patterns shown here
A singlesecrets field on the conversation-start request powers two related but
distinct patterns. Read this section before anything else — most of the
confusion in earlier versions of this README came from blurring them together.
Pattern A — Secrets as bash environment variables
You pass{"MY_KEY": "value"} at conversation start (or inject it later via the
agent server’s /secrets endpoint), and $MY_KEY becomes available to every
bash command the agent runs.
- Demonstrated by:
test_secrets_at_start.py(recommended) andtest_secrets.py(legacy / mid-conversation injection). - Use when: the agent needs a credential to run a CLI command, hit an HTTP
API via
curl, or otherwise consume a secret from the shell.
Pattern B — Secrets as ${VARIABLE} placeholders inside a plugin’s MCP config
The same secrets field also feeds variable expansion in an OpenHands
plugin’s .mcp.json. A plugin is a small bundle of files (described below)
that OpenHands fetches from GitHub at conversation start; if its .mcp.json
contains ${MCP_SERVER_URL} or ${MCP_SECRET_TOKEN}, those placeholders are
filled in from the conversation’s secrets before the MCP transport is dialed.
- Demonstrated by:
test_mcp_secrets_at_start.py(recommended) andtest_mcp_secrets.py(legacy). - Use when: you want the agent to talk to an MCP server (yours or a customer’s) using a token that isn’t in the user’s vault — for example, a per-tenant token chosen by your application for this conversation only.
What is a plugin (in this example)?
A plugin is a directory in a git repo with three files. The wholetest-plugin/
folder in this repo is a working example:
test-plugin/.mcp.json — note the ${VARIABLE} placeholders:
test-plugin/.plugin/plugin.json — minimal manifest:
test-plugin/SKILL.md — short markdown explaining the plugin’s tools and
required secrets. The agent reads this so it knows what the plugin exposes.
You point a conversation at this plugin by adding a plugins entry alongside
secrets in the start request:
source resolves to a GitHub repo, repo_path is the directory within it.
OpenHands fetches the directory, reads .mcp.json, and expands ${...}
references against the secrets you passed in the same request before
establishing the MCP transport. That expansion step is the whole point of
Pattern B.
APIs used
These tests exercise two separate OpenHands APIs:1. App Server API
- Purpose: Manages sandboxes, conversations, and user resources.
- Base URL:
https://app.all-hands.dev/api(or your deployment). - Auth header:
X-Access-Token: <your_api_key> - OpenAPI spec:
https://app.all-hands.dev/openapi.json
2. Agent Server API
- Purpose: Direct agent interaction inside a running sandbox.
- Base URL: From the sandbox’s
exposed_urlsarray (entry withname="AGENT_SERVER"). - Auth header:
X-Session-API-Key: <session_api_key>(from sandbox creation). - OpenAPI spec:
{agent_server_url}/openapi.json
Tip: To explore the Agent Server API, first create a sandbox via the App Server, wait for it to reachRUNNINGstatus, then fetch{agent_server_url}/openapi.json.
Two ways to deliver the secrets
Independent of A vs. B above, there are two timings for getting secrets into the conversation:1. At conversation start (recommended)
Pass secrets directly in thePOST /v1/app-conversations request body:
- Single request — simpler API.
- Secrets available immediately when the agent starts.
- No race condition — guaranteed to be set before the agent runs.
- Secrets are merged with vault secrets (request secrets take precedence).
- Required for Pattern B —
${...}expansion in a plugin’s.mcp.jsonneeds the secrets to be present before the MCP transport is opened.
2. After conversation start (original)
Inject secrets via the Agent Server’s/secrets endpoint after the conversation
already exists:
- You need to add secrets mid-conversation.
- You’re on an older OpenHands version without the at-start
secretsfield.
Quick comparison
Architecture
Key findings
- Two different conversation IDs. The App Server and the Agent Server use different IDs for the same conversation. You must query the Agent Server to find the correct ID for its endpoints.
- Two authentication schemes.
- App Server:
X-Access-Token: {api_key} - Agent Server:
X-Session-API-Key: {session_api_key}
- App Server:
- Secrets endpoint (Agent Server):
POST /api/conversations/{id}/secretswith body{"secrets": {"KEY": "value"}}. Secrets become environment variables ($KEY) for bash commands. ${...}expansion only works when secrets are passed at start. A plugin loaded viaplugins: [...]has its.mcp.jsonexpanded against thesecretsfield of the samePOST /v1/app-conversationsrequest. Post-hoc injection via the Agent Server’s/secretsendpoint is too late to influence MCP transport setup.
Files
Usage
Pattern A — secrets at conversation start (recommended)
Pattern A — secrets after conversation start (legacy)
Pattern B — secrets + plugin at conversation start (recommended)
This test verifies that secrets passed at conversation start are available for MCP config variable expansion inside the plugin’s.mcp.json.
- The test started a sandbox and called
POST /v1/app-conversationswithsecrets={"MCP_SERVER_URL": ..., "MCP_SECRET_TOKEN": "per-conv-secret-xyz-123"}andplugins=[{source: github:OpenHands/enterprise-cookbook, repo_path: per-conversation-secrets/test-plugin}]. - OpenHands fetched
test-plugin/from GitHub, read.mcp.json, and substituted both${MCP_SERVER_URL}and${MCP_SECRET_TOKEN}from the secrets above. - The agent dialed the resulting URL with header
Authorization: Bearer per-conv-secret-xyz-123. mcp_server.pycompared the token to its--expected-token, matched, and returned a success result that the test then asserts on.
API workflow
API reference
App Server API
Agent Server API
Getting the OpenAPI specs
Related
service-account-github-pat
Use OpenHands as a service account with per-user PATs
gpg-commit-signing
Configure GPG signing with SessionStart hooks
OpenHands API Reference
Full API documentation

