> ## Documentation Index
> Fetch the complete documentation index at: https://allhandsai-add-cookbook-tab.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Per-Conversation Secrets

> Inject secrets scoped to a single conversation — as bash environment variables, or to authenticate an MCP server via placeholder expansion in a plugin's .mcp.json.

**Source:** [`per-conversation-secrets/`](https://github.com/jpshackelford/oh-examples/tree/main/per-conversation-secrets)

OpenHands lets users store secrets in their vault for long-lived credentials. This example covers a different need: injecting a secret that belongs to one conversation only — for example, a customer's OAuth token, a temporary CI credential, or a per-tenant API key.

## Two Patterns

### Pattern A — Secrets as Bash Environment Variables

Pass `secrets` at conversation-start time. The agent can then use `$MY_KEY` in any bash command it runs.

```python theme={null}
requests.post(
    f"{CLOUD}/api/v1/app-conversations",
    headers=cloud_headers,
    json={
        "secrets": {
            "DATABASE_URL": "postgres://user:pass@host/db",
            "API_TOKEN": "tok_abc123",
        },
        "initial_message": {
            "role": "user",
            "content": [{"type": "text", "text": "Run the migration using $DATABASE_URL"}],
        },
    },
)
```

The secrets are available as environment variables in every bash command the agent runs, but are never stored in the conversation transcript.

### Pattern B — Secrets as MCP Server Auth

The same `secrets` field also drives `${VARIABLE}` expansion inside a **plugin's** `.mcp.json`. When you ship a plugin with a placeholder like `${MCP_SECRET_TOKEN}` in its MCP server config, OpenHands fills it in from the conversation's secrets before dialing that MCP server.

```json .mcp.json inside the plugin theme={null}
{
  "mcpServers": {
    "my-api": {
      "url": "${MCP_SERVER_URL}/mcp",
      "transport": "sse",
      "headers": {
        "Authorization": "Bearer ${MCP_SECRET_TOKEN}"
      }
    }
  }
}
```

At conversation-start time, pass matching secrets:

```python theme={null}
requests.post(
    f"{CLOUD}/api/v1/app-conversations",
    headers=cloud_headers,
    json={
        "plugins": [{"source": "github:your-org/your-plugins", "ref": "main", "repo_path": "my-plugin"}],
        "secrets": {
            "MCP_SERVER_URL": "https://api.example.com",
            "MCP_SECRET_TOKEN": customer_oauth_token,
        },
        "initial_message": ...,
    },
)
```

OpenHands expands the placeholders before connecting to the MCP server. The token is never visible in the conversation transcript.

## What Is a Plugin?

A plugin is a directory in a git repo with three files:

```
my-plugin/
├── .mcp.json            # MCP server config with ${...} placeholders
├── .plugin/plugin.json  # Manifest: name, version, author
└── SKILL.md             # Documentation the agent reads at startup
```

<Note>
  This example ships a working `test-plugin/` in the repo — a ready-made plugin whose `.mcp.json` uses `${MCP_SERVER_URL}` and `${MCP_SECRET_TOKEN}`.
</Note>

## Quickstart

```bash theme={null}
export OH_API_KEY="your-api-key"
pip install requests

# Pattern A: secrets as bash env vars
python test_secrets_at_start.py

# Pattern B: secrets expand into plugin MCP config
python test_mcp_secrets_at_start.py
```

## Mid-Conversation Injection (Legacy)

You can also inject secrets into a running conversation via the agent server's `/secrets` endpoint. This is less common — most use cases are better served by passing secrets at conversation-start. See [`test_secrets.py`](https://github.com/jpshackelford/oh-examples/blob/main/per-conversation-secrets/test_secrets.py) for the implementation.

## See Also

* [Load a Plugin](/cookbook/load-plugin) — How to load a plugin into a conversation
* [Test MCP Config](/cookbook/test-mcp-config) — Validate MCP server configurations before use
* [Conversation Tags](/cookbook/conversation-tags) — Attach non-secret metadata to a conversation
