Providers are the modular source side of envkit. There are two ways to add one, and both are deliberately small.
Implement three methods in a new package under internal/provider/<name>/:
type Provider interface {
Name() string
// Called at most once per run (memoized). MUST return promptly and MUST
// NOT prompt or open a browser when opts.Interactive is false — a locked
// vault is a Status{Reason, Hint}, not a hang.
Probe(ctx context.Context, opts provider.ProbeOpts) provider.Status
// Resolve one ref (everything after "name:" in the manifest's `from`).
// You own the ref grammar. Return provider.ErrNotFound for a missing ref
// at a healthy provider, provider.ErrUnavailable when the provider
// itself is unusable.
Resolve(ctx context.Context, ref string) (secret.Value, error)
}Optional capabilities, detected by type assertion:
| Interface | Enables |
|---|---|
BulkProvider.ResolveMap(ctx, ref) |
feeding [[envfile]] sources (many vars from one ref) |
CheckProvider.Check(ctx, ref) |
device-bound presence checks (to = "check") |
LoginProvider.Login(ctx) |
envkit login <name> (the ONLY place prompting is allowed) |
Register it in internal/cli/root.go's provider.NewRegistry(...) call.
That's the whole contribution surface. Ground rules:
- Shell out through
execx.Runner(neveros/execdirectly) so tests can script your binary withexecx.NewFake()— seeinternal/provider/bwfor the pattern,internal/provider/awssmfor an SDK-based provider with a mockable API interface. - Never log secret material;
secret.Valueredacts itself under%v/%s, keep it that way by returning values only assecret.Value. - Configuration (profiles, folder allow-lists) comes in through your
constructor from the repo's committed
[providers.<name>]table (merged withenvkit.local.tomloverrides) — providers never read config files themselves.
Any executable named envkit-provider-<name> on PATH is a provider;
manifests use from = "<name>:<ref>" exactly like a built-in. This is the
right tier for org-internal providers that shouldn't live in this repo.
Protocol v1 (ENVKIT_PLUGIN_PROTOCOL=1):
stdin: one JSON line {"op":"probe"} or {"op":"resolve","ref":"<ref>"}
stdout: probe → JSON {"ready":bool,"reason":"…","hint":"…"}
resolve → raw secret bytes (nothing else)
stderr: diagnostics only — never secret material
exit: 0 ok · 40 ref not found · anything else = provider unavailable
A complete plugin in shell:
#!/usr/bin/env bash
read -r req
case $req in
*'"probe"'*) echo '{"ready":true}' ;;
*'"resolve"'*) ref=$(jq -r .ref <<<"$req")
pass show "$ref" | head -1 | tr -d '\n' || exit 40 ;;
esac| Provider | Ref grammar | Notes |
|---|---|---|
rbw |
folder/item[#field] |
Bitwarden via the rbw agent; [providers.rbw] folder-allow scoping |
bw |
item[#field] |
official Bitwarden CLI; field = password (default), username, uri, totp, notes, or a custom field; session via $BW_SESSION |
aws-sm |
secret-id[#json-key] |
AWS Secrets Manager; {repo} expands from git origin; bulk-capable for [[envfile]] sources; profile/region from [providers.aws-sm] |
generate |
hex32|hex64|b64-32|uuid |
per-instance, generated once, never regenerated |
detect |
tailscale-ip|git-name|git-email |
machine-local derivation |
check |
dir=<path> |
device-bound login presence |