User Secrets Plugin
Stores per-user API keys and tokens encrypted at rest and lets plugins and the shell tool consume them without exposing the value to the LLM. Use it for GitHub PATs, Redmine/Gitea keys, database passwords, Telegram bot tokens, and similar credentials in a multi-user deployment.
Manage secrets in Settings → Extensions → User Secrets. That requires the user_secrets:manage permission, which the built-in power_user and admin roles have.
How secrets are used
Three delivery modes, all independent of workspace isolation:
${secret:<name>}in YAML. Any config field that supports it (LLM profileapi_key, MCP server headers, web-search provider keys, a databasepassword:) can embed${secret:my_token}. The value is resolved from the signed-in user's store at request time.- Shell env binding (default). Bind a secret to an env var name (e.g.
gh_pat→GITHUB_TOKEN). When the agent runs a shell command, the value is injected into the subprocess environment; the transcript never sees it. - Visible to the agent (opt-in). A binding can also be flagged visible to agent, in which case the value is written to the agent's own environment for that turn. Needed when in-process libraries (boto3, google-auth, etc.) read env vars directly. Only turn this on when you accept that the agent process can see the value.
${VAR} env-var substitution in YAML continues to work; the syntax is disambiguated by the colon (${secret:…} vs ${VAR}).
| Consumer | Where the placeholder lives | When it resolves |
|---|---|---|
LLM profile api_key | models.profiles.<name>.api_key (YAML) or a per-user profile | Each LLM call |
| MCP remote headers | MCP server headers in plugin args | Each outbound MCP request |
Web-search provider api_key | Web-search providers.<name>.api_key | Each search |
Database password | Database connection password: ${secret:…} | Each SQL tool call |
| Shell tool env var | A binding you create in Settings | Each shell command |
Needed by plugins
Because secrets are referenced by name, a user otherwise has to know the exact name a plugin expects (e.g. redmine_api_key). Plugins that need a secret declare it, and the User Secrets pane shows a Needed by plugins section — each flagged set or not set, with Add this secret to pre-fill the create form.
Plugins that declare requirements today:
- Redmine — configured
api_key_secret(defaultredmine_api_key) - Gitea PR — configured
api_token_secret(defaultgitea_api_token) - Telegram — shared or per-user bot token (and a speech key if
speech.api_keyis${secret:…}) - Database — each connection's
password_secret - MCP — every
${secret:...}found in a server's headers/env/args
Host-only secrets
When creating a secret you can mark it Host-only (e.g. SCM push token). A host-only secret is never given to the agent and cannot be bound to the shell tool. Features such as SCM Push read it only on the host. Tick this for credentials that must never reach a sandbox.
Rows the assistant manages itself
Some host-only rows are not created by hand. An MCP server configured
with oauth: true keeps each user's authorization here, as a host-only row named
mcp_oauth_<host>_<digest> whose description says which server it belongs to.
There is nothing to edit — the value is a token Codumentor mints and refreshes —
but deleting the row disconnects you from that server, which is a supported
way to do it. The server's tools go quiet from your next turn.
Configuration
plugins:
- module: codumentor.plugins.user_secrets
class: UserSecretsPlugin
priority: 3
args:
enabled: true
max_secrets_per_user: 50 # default: 50
previous_jwt_secrets: [] # for key-rotation grace period
Load this plugin before consumers that resolve ${secret:…} at startup (keep priority: 3 unless you have a reason to change it).
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Master on/off. |
max_secrets_per_user | int | 50 | Cap shown in the Settings pane (used / max). |
previous_jwt_secrets | list | [] | Previous auth.jwt_secret values, so secrets encrypted under an older key can still be decrypted after rotation. |
data_dir | string | agent.storage_dir | Override only if the store should not live under the agent storage directory. |
Set auth.jwt_secret so secrets are encrypted at rest (keyed off that secret). Rotation: put the old value in this plugin's previous_jwt_secrets, switch auth.jwt_secret, restart; secrets are re-encrypted with the current key the next time they are read.
Using the Settings pane
Settings → Extensions → User Secrets:
- Needed by plugins (when any plugin declares requirements) — create the missing ones with the exact name shown.
- Secrets — add / edit / rename / delete. Values are write-only: they cannot be read back after save.
- Shell-tool env bindings — link a secret to an env var. Optionally mark it visible to the agent. Host-only secrets do not appear in the binding dropdown.
Renaming a secret updates its bindings. Deleting a secret deletes its bindings.