What the OS keyring provides
The OS keyring is a platform-managed, access-controlled credential store. It survives application reinstalls, is protected by the user’s login session, and is not included in simple file-system copies of the application data directory.- Windows
- macOS
- Linux
Windows Credential Manager (
wincred) stores credentials as “generic credentials” scoped to the current user. Credentials are encrypted at rest by the OS using DPAPI tied to the user’s login.Marking fields with keyring()
Callkeyring(typeCtor, { id }) in your schema to declare that a field is keyring-protected. The field will not appear in any written config file — only the config’s other fields are persisted to disk.
keyring() fields can appear at any depth in the schema, including inside nested objects and arrays. Each keyring() call must have a unique id across the entire schema.
KeyringOptions
Nearly every operation that involves keyring fields requires aKeyringOptions object:
service and account are used to construct the keyring lookup path. Use a consistent value throughout your app — changing either value makes previously stored secrets unreadable.
Keyring storage path
Eachkeyring() field is stored at the OS keyring path:
{ service: "my-app", account: "default" } with a field keyring(String, { id: "db-password" }) is stored with account default/db-password under service my-app.
For array keyring fields, the account is extended with the element’s dot-path index to keep each element distinct:
tokens array field with id: "token" stored under account default becomes default/token::tokens.1.
The
id must not contain / because / is used as a separator in the account string. defineConfig() validates this at runtime and throws if a / is found in any id.Locked vs unlocked states
When you load or save a config that haskeyring() fields, the plugin distinguishes between two states: locked and unlocked.
Locked state
In the locked state, keyring fields are represented asnull in the returned data. This is the default when you call .run() without chaining .lock() or .unlock().
LockedConfig<S> is the class wrapping this state. Its .data property is typed as InferLocked<S> — all keyring() fields are typed as null.
Unlocked state
In the unlocked state, keyring fields are populated with their actual values retrieved from the OS keyring. You reach this state by chaining.unlock(keyringOpts) instead of .run(), or by calling .unlock(keyringOpts) on an existing LockedConfig.
UnlockedConfig<S> is the class wrapping this state. Its .data property is typed as InferUnlocked<S> — keyring() fields have their real TypeScript types.
Revoking access
Once you are done with unlocked data, call.lock() on the UnlockedConfig instance to revoke API-level access to the decrypted values:
.lock() is an API-level access guard, not a cryptographic wipe. JavaScript’s garbage collector manages memory reclamation, so the underlying buffer is not guaranteed to be zeroed immediately. Do not rely on .lock() as a memory-security primitive..lock() and .unlock() on operations
The.lock() and .unlock() builder methods on operation entries (LazyConfigEntry, LazyPatchEntry, LazyResetEntry) control how the plugin handles keyring fields during write operations.
Writing with .lock()
Chain.lock(keyringOpts) before .run() to store keyring fields in the OS keyring when creating or saving:
keyring() fields and you call .run() without .lock(), the plugin throws an error:
Writing and reading back with .unlock()
Replace.lock(...).run() with .unlock(keyringOpts) to both write to the keyring and immediately read back the unlocked data in one step:
Deleting keyring entries
When you callconfig.delete(keyringOpts), the plugin deletes the config file and also removes all associated keyring entries from the OS keyring: