Credential Management
The hybrid-memory plugin includes a credential store for structured storage of API keys, tokens, passwords, and other authentication data. It’s on by default under the local and minimal config presets (local is the plugin’s own default mode, so most installs have it on out of the box) and opt-in under enhanced/complete (enable it explicitly with credentials.enabled: true, or set a valid credentials.encryptionKey). Encryption is optional: you can use the vault with or without an encryption key; without a key, values are stored in plaintext and you may secure the data by other means (e.g. filesystem permissions, full-disk encryption).
Dual-mode: Vault vs memory
- Vault disabled/unavailable (e.g.
enhanced/completepresets with no encryption key andcredentials.enablednot explicitly set totrue): Credential-like content is blocked from ordinary memory storage.memory_store,openclaw hybrid-mem store, session distillation, and extract-daily will skip credential-like inputs and instruct operators to enable the credential vault. - Vault enabled: When the secure credential vault is enabled (see below), credential-like content is not written into memory or the database. Instead:
- The secret is stored only in the encrypted vault.
- A pointer fact is stored in memory (e.g. “Credential for home-assistant (token) — stored in secure vault. Use credential_get(service="home-assistant") to retrieve.”) so the agent knows the credential exists and how to retrieve it.
- Recall and search return the pointer, not the secret; the agent uses
credential_getwhen it needs the value.
So: no vault → credentials are not stored in ordinary memory. Vault on → credentials live only in the vault; memory holds pointers only.
Dual-mode applies to: memory_store tool, openclaw hybrid-mem store (CLI and scripts such as session distillation), and openclaw hybrid-mem extract-daily.
Migration: existing credentials into vault
When the vault is enabled, the plugin ensures that any credentials that were previously stored in memory (facts) are moved into the vault and redacted from their old locations:
- On first load (once per install): The plugin looks for facts with entity
Credentialsthat contain real secrets (not already pointer text). For each one it: stores the secret in the encrypted vault, deletes the original fact from SQLite and LanceDB, and adds a new pointer fact so the agent still knows the credential exists and can usecredential_get. A flag file (.credential-redaction-migratedin the memory directory) is written so this runs only once. -
Manual run: You can run the migration again anytime (e.g. after adding more credential facts with vault off, then enabling vault):
openclaw hybrid-mem credentials migrate-to-vaultThis is idempotent: facts that are already pointers are skipped.
So when vault is enabled, all stored credentials end up only in the vault, with pointers in memory — including ones that were originally stored in memory before the vault was turned on.
Enable
The vault is already on by default under the local and minimal config presets — no action needed there. Under enhanced/complete, set "enabled": true in your memory-hybrid config to use the credential vault. Optionally set encryptionKey to encrypt values at rest; if omitted, the vault stores values in plaintext (you may secure the file by other means).
With encryption (recommended when storing secrets):
{
"credentials": {
"enabled": true,
"store": "sqlite",
"encryptionKey": "env:OPENCLAW_CRED_KEY",
"autoDetect": true,
"autoCapture": {
"toolCalls": true,
"logCaptures": true
},
"expiryWarningDays": 7
}
}
Set the OPENCLAW_CRED_KEY environment variable to a secret of at least 16 characters:
export OPENCLAW_CRED_KEY="your-secret-key-min-16-chars"
Without encryption (plaintext vault): Omit encryptionKey (or set enabled: true only). The vault is fully functional; values are stored in plaintext. Secure the credentials file (e.g. ~/.openclaw/memory/credentials.db) via filesystem permissions or full-disk encryption if desired.
- encryptionKey (optional):
env:VAR_NAME,file:/absolute/path/to/key(reads file contents), or a 16+ character secret. When set and valid, the vault is encrypted at rest. When omitted, the vault is plaintext.
file: SecretRef semantics
file:/path/to/key follows the same SecretRef pattern as other hybrid-mem config secrets: the file contents (trimmed) are used as the vault passphrase.
Legacy behavior (pre-2026.6): Some vaults were encrypted using the literal configured string file:/path/to/key as the passphrase (the path text itself, not the file contents). On open, the plugin tries file contents first, then falls back to the legacy literal ref so existing vaults keep working. When the legacy fallback is used, a one-time warning is logged with migration guidance.
To migrate to file-content semantics: put your desired passphrase in the key file, ensure encryptionKey is file:/path/to/key, then re-encrypt:
openclaw hybrid-mem credentials encrypt-vault --backup --verify --yes
- enabled: Set to
trueto enable the vault; set tofalseto disable. - autoDetect (optional): When true, detects credential patterns in conversation and prompts the agent to offer storing them.
- autoCapture (optional): Auto-capture credentials from tool call inputs (see Auto-Capture from Tool Calls below). You can set requirePatternMatch to
trueso that only values matching a known credential pattern (e.g. JWT,sk-...,ghp_...) are stored; narrative or value-only text is then rejected. - expiryWarningDays (optional): Days before expiry to warn (default: 7).
Check vault encryption status
Use vault-status to see the current state of your credentials vault at any time:
openclaw hybrid-mem credentials vault-status
Example output (plaintext vault with key configured):
Vault path: /home/user/.openclaw/memory/credentials.db
Status: plaintext (kdf_version=0)
Entries: 3
Key present: true
⚠ Migration required: vault is plaintext but an encryption key is configured.
Run: openclaw hybrid-mem credentials encrypt-vault --backup --verify --yes
Pass --json for machine-readable output. The migrationRequired field is also surfaced in the audit-health report under the credentials key.
Encrypt an existing plaintext vault
If your credentials vault was initialized in plaintext mode (kdf_version=0) and you later configure an encryption key, the key is ignored until you encrypt the existing vault at rest. This prevents the plugin from accidentally treating an existing plaintext database as encrypted (or vice versa).
A warning is logged once per process at startup when migration is required, and the audit-health report will include a credentials.migrationRequired: true field.
Recommended safe migration workflow
The recommended approach uses --backup (consistent snapshot via VACUUM INTO) and --verify (decrypt every entry after encryption to confirm integrity):
openclaw hybrid-mem credentials encrypt-vault --backup --verify --yes
This will:
- Create a consistent plaintext backup at
<vault-path>.bak.<timestamp>-<pid>(VACUUM INTO — safe on a live open connection). - Encrypt the vault in-place via an atomic SQLite transaction.
- Decrypt every entry to confirm values are readable after encryption.
- Print the backup path and a verification confirmation.
Options
| Option | Description |
|---|---|
--yes | Apply changes (default is dry-run) |
--backup | Auto-generate a .bak.<timestamp>-<pid> backup before encrypting |
--backup-path <path> | Custom backup path (implies backup) |
--verify | Verify all entries are readable after encryption |
Safety note: If --verify fails (which would indicate a corrupt DB), the command throws with the backup path so you can manually restore. The in-place encryption is already committed at that point — do NOT re-run encrypt-vault. Restore from the backup instead:
cp /path/to/credentials.db.bak.<timestamp>-<pid> /path/to/credentials.db
After running, restart the gateway (or run openclaw hybrid-mem verify) to confirm the vault now reports encrypted (kdf_version=2).
API (Tools)
When enabled, nine tools are registered:
| Tool | Description |
|---|---|
credential_store | Store a credential (service, type, value, url?, notes?, expires?) |
credential_get | Retrieve by service (optional type for disambiguation) |
credential_list | List stored credentials (service/type/url only — no values) |
credential_delete | Delete by service (optional type) |
credential_revision_list | List historical revisions for a service/type (metadata only) |
credential_revision_get | Retrieve a specific revision’s value intentionally |
credential_revision_restore | Restore/promote a revision back to current |
credential_revision_purge | Hard-delete one revision, or every revision for a service/type |
credential_revision_pin | Pin a revision so it never expires, or unpin it |
Credential Types
token,password,api_key,ssh,bearer,otherurlis not a credential type. Endpoint/base URLs belong in the optionalurlparameter (ornotes), not intype. Legacy vault rows withtype=urlare migrated automatically on vault open (merged into a siblingbearer/token/… entry when present, otherwise converted totype=otherwith the URL in theurlcolumn).
Examples
Store:
credential_store(service="home-assistant", type="token", value="eyJ...", url="http://localhost:8123")
credential_store(service="my-api", type="bearer", value="secret-token", url="https://api.example.com/v1")
credential_store(service="github", type="api_key", value="ghp_...", notes="Personal access token")
Get (exact lookup, no fuzzy search):
credential_get(service="home-assistant")
credential_get(service="github", type="api_key")
Historical Revisions (issue #2104)
Overwriting an existing credential (same service + type) no longer discards the previous value outright — it’s kept as a historical revision for a retention window, so a migration or rotation mistake can be reversed instead of reconstructed from memory/logs.
credential_store(service="doris-gateway", type="other", value="10.0.0.70")
# → "Stored credential for doris-gateway (other). Previous value retained as revision
# <id> until <date> (30-day retention) unless accessed/pinned/purged."
credential_revision_list(service="doris-gateway", type="other")
credential_revision_get(service="doris-gateway", type="other", revision="<id>")
credential_revision_restore(service="doris-gateway", type="other", revision="<id>")
credential_revision_purge(service="doris-gateway", type="other", revision="<id>") # or all=true
credential_revision_pin(service="doris-gateway", type="other", revision="<id>", pinned=true)
Equivalent CLI commands: hybrid-mem credentials revisions list|get|restore|purge|pin (restore and purge are dry-run by default — pass --yes to apply; get masks the value by default — pass --show-value or --value-only).
Retention model:
- Replaced values are kept for 30 days by default (
credentials.revisionTtlDays). - Retrieving a specific revision (
credential_revision_get) refreshes/extends its retention window from the moment of access, unlesscredentials.revisionAccessRefreshis set tofalse. - Pinned revisions (
credential_revision_pin) never expire until unpinned. - Expired, unpinned revisions are swept lazily the next time revisions for that service/type are listed or accessed.
credential_revision_purgehard-deletes immediately and cannot be undone.- Set
credentials.revisionsEnabled: falseto disable revision history entirely (a plain overwrite, matching pre-#2104 behavior).
Restore is non-destructive: promoting an old revision back to current snapshots the value it replaces as a new revision first, so restoring never discards data outright.
Security: revisions are encrypted with the same key/mode as current values (re-keying/encrypting the vault also re-encrypts every revision). credential_revision_list never includes values — only credential_revision_get does, and only for a revision id you explicitly pass. Every retrieve, restore, purge, and pin/unpin is recorded in a revision-specific audit trail inside the vault itself, so the trail exists even when the cross-agent audit log isn’t wired up (e.g. the credentials-only CLI fast path). Normal credential_get is completely unaffected — it only ever returns the current value.
{
"credentials": {
"enabled": true,
"revisionsEnabled": true,
"revisionTtlDays": 30,
"revisionAccessRefresh": true
}
}
Storage
- Credentials are stored in
credentials.dbnext to your facts database (e.g.~/.openclaw/memory/credentials.db) - Values are encrypted with AES-256-GCM
- Only service, type, url, notes, and expiry metadata are stored in plaintext
- Historical revisions live in the same database (
credential_revisionstable) with the same encryption, plus acredential_revision_audittable for the retrieve/restore/purge/pin trail
Key Derivation (KDF)
The encryption key is derived from your encryptionKey config using a key derivation function:
- v2 (current, scrypt): New vaults use scrypt (N=16384, r=8, p=1) with a random 32-byte salt. The KDF version and salt are stored in the
vault_metatable. - v1 (legacy, SHA-256): Earlier versions derived the key using a single SHA-256 hash (no salt). This is weaker and is automatically migrated.
Automatic KDF migration (v1 → v2)
If you have an existing vault created before scrypt support was added, it will be automatically migrated to scrypt on the first successful credential_get call:
- The plugin detects the vault has no
vault_metaentries (legacy vault). - On the first successful decryption (proving the password is correct), all credentials are re-encrypted with a new scrypt-derived key and random salt.
- The new KDF version and salt are written to
vault_meta.
This migration is transparent — no action required. After migration, all subsequent operations use the stronger scrypt KDF.
Redaction
- credential_get: The credential value is returned only in
details.valuewithsensitiveFields: ["value"]. Thecontenttext does not include the value, so it can be safely logged and persisted in transcripts, dashboards, or memory-extraction pipelines. Platforms should redact fields listed insensitiveFieldswhen persisting session transcripts or exporting.
credentials.revealInContent (default: false)
For explicit local debugging only, you may set revealInContent: true to have credential_get include the raw value in the plain-text content field:
{
"credentials": {
"enabled": true,
"revealInContent": true
}
}
⚠️ Do not enable this in production.
contentis commonly persisted in transcripts, logs, dashboards, and memory-extraction pipelines, defeating sensitive-field protection. Keep the default (false) in all non-debugging environments.
Auto-Capture from Tool Calls
When autoCapture.toolCalls is enabled, the plugin scans tool call inputs (what the agent sends to tools) for credential patterns and stores them in the vault immediately — no prompting required, since the agent already used the credential openly.
This solves the problem of credentials getting lost between sessions: the agent uses them in tool calls (exec commands, API calls, etc.) but never explicitly mentions them in conversation text.
Enable
{
"credentials": {
"encryptionKey": "env:OPENCLAW_CRED_KEY",
"autoCapture": {
"toolCalls": true,
"logCaptures": true
}
}
}
Config options:
toolCalls: Enable scanning of tool call inputs (default:false, opt-in)logCaptures: Emit an info-level log on each capture (default:true)
Pattern matching currently uses a built-in pattern set and is not configurable via credentials.autoCapture.
Detection Patterns
The following tool input patterns are detected and stored automatically:
| Pattern | Type | Service |
|---|---|---|
sshpass -p <pass> ssh user@host | password | ssh://user@host |
curl -H "Authorization: Bearer <token>" <url> | bearer | hostname from URL |
curl -u user:pass <url> | password | hostname from URL |
-H "X-API-Key: <key>" | api_key | hostname from URL |
postgres://user:pass@host/db (also mysql, mongodb, redis, mssql) | password | proto://host/db |
export VAR_KEY=value, export VAR_TOKEN=value, export VAR_PASSWORD=value, export VAR_SECRET=value | api_key / token / password / other | derived from var name |
.env-style VAR_KEY=value assignments | api_key / token / password / other | derived from var name |
Behavior
- The plugin registers an
agent_endhook that scans alltool_calls[*].function.argumentsin assistant messages. - When a credential pattern is found, the value is extracted along with service name (hostname, connection string, or variable-derived slug) and type.
- The credential is stored in the vault via
credentialsDb.store()— an upsert on(service, type), so repeated captures update the value without creating duplicates. - A confirmation log is emitted:
memory-hybrid: auto-captured credential for ssh://root@192.168.1.19 (password). - Credential values are never written to the facts DB or vector DB — vault only.
Security Notes
- Only tool inputs are scanned — never tool outputs (which may contain unrelated secrets in command logs, etc.).
- Respects the
credentials.enabledgate — if vault is disabled, no capture occurs. - Credential values are encrypted with AES-256-GCM in the vault.
Auto-Detection
When autoDetect is enabled, the plugin scans conversation messages for patterns such as:
- Bearer/JWT tokens (
eyJ...) - OpenAI-style API keys (
sk-...) - GitHub tokens (
ghp_...,gho_...) - Slack tokens (
xoxb-..., etc.) - SSH connection strings (
ssh user@host)
When detected, a hint is stored and injected at the start of the next turn, prompting the agent to offer storing the credential with credential_store.
Validation and cleanup
Auto-captured credentials are validated before storage so that narrative text, debug notes, and descriptions are not stored as secrets:
- Value validation: Values that look like natural language (e.g. sentences), paths, or long descriptions are rejected. Values that match a known pattern (JWT,
sk-...,ghp_..., etc.) are accepted. - Service name validation: Service names longer than 50 characters or that look like sentences (many dash-separated tokens) are rejected. Hostnames (e.g.
api.example.com) and URL-style names (e.g.postgres://host/db) are preserved; other names are normalized (e.g.anthropic_api_key→anthropic). - Deduplication: If the vault already has the same (service, type) with the same value, the store is skipped (no-op).
The vault stores one entry per (service, type). If you store the same service+type again (e.g. UniFi API key multiple times), the previous value is overwritten — you will only ever see one row per (service, type) in the list.
To inspect and clean an existing vault:
- List:
openclaw hybrid-mem credentials list— lists all entries (service, type, url; no values). Use--service <pattern>to filter by service name (case-insensitive substring, e.g.--service unifito find unifi, unifi-controller, etc.). - Get:
openclaw hybrid-mem credentials get --service <name>— retrieve the secret value for a service. Use--type <type>when multiple credential types exist for that service (e.g.--type api_key). Use--value-onlyto print only the value (for scripting or piping). - Audit:
openclaw hybrid-mem credentials audit— flags suspicious entries (e.g. natural language, long service names, duplicates). Use--jsonfor machine-readable output. - Prune:
openclaw hybrid-mem credentials prune— by default runs a dry-run showing what would be removed. Use--yesto actually remove flagged entries. Optionally use--only-flags natural_language,service_too_longto prune only those reasons.
Expiry Warnings
- credential_get: When a credential has
expiresset and it’s withinexpiryWarningDays, a warning is appended to the response. - credential_list: Items expiring soon are flagged with an expiry date and a ⚠️ marker. A summary block lists how many credentials need rotation.
Related docs
- README — Project overview and all docs
- CONFIGURATION.md — Credentials config settings
- CLI-REFERENCE.md —
credentials migrate-to-vaultcommand - SESSION-DISTILLATION.md — How distillation routes credentials to vault
- TROUBLESHOOTING.md — Common issues