agalwood/Motrix · error · ConfigError
plugin.lifecycle.secrets_seed_missing
plugin.lifecycle.secrets_seed_missing
Error message
plugin "${this.opts.pluginId}" config key "${key}" is secret but no decryptSecret function was injected What it means
Thrown by ConfigCapabilityHost's single-key getter when a config key is in `secretFields`, the resolved value is a non-empty string (stored value or schema default), but no `decryptSecret` function was supplied to the capability. The capability refuses to return ciphertext as plaintext, so a secret field without a decryption seed is treated as a misconfigured host. Code is `plugin.lifecycle.secrets_seed_missing`.
Source
Thrown at src/core/plugin/capabilities/config.ts:91
* If the resolved value is a string and the key is secret, it is decrypted
* via `decryptSecret`. If no decryptSecret is injected for a stored secret
* string, throws ConfigError('plugin.lifecycle.secrets_seed_missing', ...).
*/
async get(key: string): Promise<unknown> {
const stored = this.opts.readValues()
const storedValue = Object.hasOwn(stored, key) ? stored[key] : undefined
const resolved =
storedValue !== undefined ? storedValue : this.opts.schemaDefaults[key]
// Only decrypt if the resolved value is a string and the key is secret.
if (
resolved !== undefined &&
typeof resolved === 'string' &&
this.opts.secretFields.has(key)
) {
if (!this.opts.decryptSecret) {
throw new ConfigError(
'plugin.lifecycle.secrets_seed_missing',
`plugin "${this.opts.pluginId}" config key "${key}" is secret but no decryptSecret function was injected`
)
}
return this.opts.decryptSecret(resolved)
}
return resolved
}
/**
* Returns the stored value verbatim — does NOT fall back to schema defaults
* and does NOT decrypt. Intended for the renderer's "is this overridden?" check.
*/
async getRaw(key: string): Promise<unknown> {
const stored = this.opts.readValues()
return Object.hasOwn(stored, key) ? stored[key] : undefined
}View on GitHub (pinned to 1a708ee577)
Solutions
- Inject a decryptSecret function into the Config capability options at host construction.
- If the field should not actually be secret, remove it from secretFields in the plugin's config schema.
- In dev/test, supply a no-op decryptor (returns input unchanged) only when values are already plaintext — never do this in production.
- Verify the secrets seed/bootstrap runs before plugin activate() and that the decryptor is bound per-plugin, not globally absent.
Example fix
// before
const config = new ConfigCapabilityHost({
pluginId: 'p', secretFields: new Set(['apiToken']),
readValues: () => ({ apiToken: cipherText }),
// decryptSecret missing
})
await config.get('apiToken') // throws
// after
const config = new ConfigCapabilityHost({
pluginId: 'p', secretFields: new Set(['apiToken']),
readValues: () => ({ apiToken: cipherText }),
decryptSecret: (ct) => kms.decrypt(ct),
}) Defensive patterns
Strategy: validation
Validate before calling
function assertDecryptor(opts: ConfigCapabilityHostOptions): void {
if (opts.secretFields && opts.secretFields.size > 0 && !opts.decryptSecret) {
throw new Error('decryptSecret required because secretFields is non-empty')
}
} Type guard
function isSecretsSeedMissing(e: unknown): e is ConfigError {
return e instanceof Error && (e as ConfigError).code === 'plugin.lifecycle.secrets_seed_missing'
} Try / catch
try {
await config.get(key)
} catch (e) {
if (isSecretsSeedMissing(e)) {
// host misconfiguration: fail loudly in dev, alert ops in prod
} else throw e
} Prevention
- Treat secretFields as requiring decryptSecret by construction — assert at host build.
- Never ship plaintext values in schemaDefaults for secret fields.
- In tests, supply a deterministic decryptor stub rather than omitting it.
When it happens
Trigger: Calling `config.get('apiToken')` where `apiToken` is declared secret in the schema and has a value, but the host built the Config capability without injecting `decryptSecret`. Also happens if the host secrets pipeline (e.g. a KMS/seed provider) was disabled in the current environment.
Common situations: Local dev environment where the secrets decryptor was never wired; CI runs with a stripped-down host config; a plugin manifest marks a field secret but the host hasn't provisioned the decryption seed for this plugin.
Related errors
- plugin.commands.access_denied
- plugin.lifecycle.deactivate_timeout
- PluginActivationCapExceeded
- plugin.fs.task.not_available_outside_hook
- plugin.metadata.not_available_outside_hook
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/ad4213011f1e4901.
Report an issue: GitHub.