~/.mibyan/.env loads, before Mibyan reads credentials. Bitwarden, 1Password, and a generic command-helper source ship in-tree; every other backend is a plugin. This guide covers building one.
First-process bootstrap timing
load_mibyan_dotenv() often runs at import time before plugins register.
Mibyan then re-pulls secrets after plugin discovery when any enabled
plugin secret source is configured. Enablement uses the source’s
is_enabled(cfg) contract; the standard form is
secrets.<name>.enabled: true, while custom activation remains supported.
That closes the “replace Bitwarden with my vault” first-process gap (#64177).
- Re-pull is idempotent and fail-open (never blocks startup).
mibyan updatenever resolves external sources — not in the updater process and not in the import-health probes it spawns. Nothing in the update path needs credentials, and a slow helper would otherwise be misreported as an import failure (#110823).- Sources only supply env vars through the orchestrator; there is no plugin API to dump other plugins’ or the user’s entire secret store beyond what your source’s own config allows.
- Reading
os.environafter load is possible for any in-process code — the trust boundary remains “enabled plugins run with agent privilege”.
What the framework owns vs. what you own
The orchestrator (agent.secret_sources.registry.apply_all) owns everything security- and precedence-sensitive, so a backend cannot get it wrong:
Directory structure
The SecretSource ABC
Implementagent.secret_sources.base.SecretSource. One method is required:
Contract rules (enforced, not suggestions)
fetch()never raises. Errors go inresult.error+result.error_kind. A raising fetch is contained by the orchestrator and reported asINTERNAL— a contract violation, not a feature.fetch()never prompts. Startup runs in non-TTY contexts (gateway, cron, Docker).run_secret_cli()closes stdin so a prompting helper fails fast. Interactive auth belongs in your CLI setup flow, never on the startup path.- Sync, within budget. The orchestrator enforces a wall-clock timeout (default 120s, user-tunable via
secrets.<name>.timeout_seconds). Exceeding it reportsTIMEOUTand your result is discarded. - You fetch; the orchestrator applies. Return the mapping you would contribute. Never write
os.environyourself — you’d bypass precedence, conflict detection, and provenance. - API versioning.
SecretSource.api_versiondefaults to the currentSECRET_SOURCE_API_VERSION. The registry skips (with a warning) sources built against a different version instead of crashing startup.
Choosing your shape
mapped— the user explicitly binds env-var names to references in config (like 1Password’senv:map). Strongest intent: mapped claims beat bulk claims on contested vars.bulk— you inject a whole project/folder of secrets implicitly (like Bitwarden BSM). Yields to mapped sources.
Optional hooks
Subprocess safety: use run_secret_cli()
If your backend shells out to a CLI, use the shared helper instead of subprocess.run directly. It gives you the audited posture for free: argv-only (no shell=True), a minimal allowlisted child environment (by the time sources run, os.environ holds every credential Mibyan knows — never hand that to a child process), NO_COLOR + ANSI-scrubbed stderr, stdin closed, timeout → clean RuntimeError. Pass user-supplied reference strings after a -- terminator in your argv so they can never parse as flags.
Registering
SecretSource instances, invalid/duplicate names, a scheme another source owns, wrong api_version, or a shape outside mapped/bulk.
TimingPlugin discovery runs later in startup than the first
load_mibyan_dotenv() call. Immediately after discovery, Mibyan re-pulls enabled plugin secret sources (reset_secret_source_cache() + load_mibyan_dotenv()), so the discovering process does pick them up — see First-process bootstrap timing above (#64177). The re-pull is fail-open and skipped when no plugin source is enabled. Any code that reads os.environ during the plugin module’s import or register(ctx) still runs before the re-pull and cannot depend on credentials supplied by that same source; keep credentialed work inside fetch(). Gateway, cron, and subagent processes perform the same discovery/re-pull sequence. The re-pull (and the per-fire cron re-pull) resets only the resolving home’s cache, so under a multiplex gateway sibling profiles keep their hydrated snapshots; and a re-pull whose keys already sit in the process environment (skipped_existing, e.g. the previous apply’s own write-back) still records the home’s effective values, so override_existing is never required just to survive a re-pull.Users configure it like any other source
(from My Vault) provenance labels all work automatically — see the user-facing secrets docs for the precedence ladder.
Validate with the conformance kit
Subclass the kit from the Mibyan repo (tests/secret_sources/conformance.py) in your plugin’s tests:
apply_all() round trip. Green conformance is the review bar for calling a backend contract-compliant.

