Skip to content

SecretSpec 0.20: Git, Docker, inline specs, and five new providers

SecretSpec 0.20 brings SecretSpec-managed credentials to Git and Docker, lets applications declare secrets directly in code, adds five providers, and expands support for JVM and Alpine applications.

This release includes:

Git and Docker can now read credentials directly from any SecretSpec provider. Configure each helper once, then keep using the usual Git and Docker commands.

The Git integration registers a helper for a host, then stores its token through your normal SecretSpec provider:

Terminal window
$ secretspec git configure \
--url https://github.com \
--username YOUR_USERNAME
$ secretspec git login https://github.com

After that, normal commands invoke git-credential-secretspec automatically:

Terminal window
$ git clone https://github.com/OWNER/PRIVATE_REPOSITORY.git
$ git push

The helper works before a repository has been cloned and can scope credentials by host or URL path. It also supports SMTP credentials for git send-email, so the password does not need to live in sendemail.smtpPass.

The Docker integration uses one helper per registry. Configure the registry and store its token separately:

Terminal window
$ secretspec docker configure \
--registry ghcr.io \
--username YOUR_USERNAME
$ secretspec docker login ghcr.io

docker pull, docker push, docker build, and Docker Compose can now retrieve the value through docker-credential-secretspec:

Terminal window
$ docker pull ghcr.io/OWNER/IMAGE:TAG

Docker’s config.json contains only the helper configuration, not the credential value. Separate DOCKER_CONFIG directories can use different credentials for the same registry.

The helpers only read credentials. Use secretspec git login / logout and secretspec docker login / logout to change stored values. Use configure and unconfigure to add or remove the helpers themselves.

secretspec.toml remains the portable contract shared by the CLI and every SDK. Applications can now define that contract directly when keeping a separate manifest is inconvenient.

Rust applications can use the new public Spec, Profile, and Secret types to build a specification in code:

use secretspec::{Profile, Secret, Secrets, Spec};
let spec = Spec::builder("checkout")
.provider("env", "env://")
.secret(
"DATABASE_URL",
Secret::required("PostgreSQL connection URL").providers(["env"]),
)
.profile(
"production",
Profile::new().secret(
"SENTRY_DSN",
Secret::required("Production Sentry endpoint"),
),
)
.build()?;
let mut secrets = Secrets::from_spec(spec)?;
secrets.set_profile("production");
let resolved = secrets.resolve()?;

Rust-built specs use the same validation as secretspec.toml. Spec::schema_json generates the same JSON Schema as secretspec schema, and SpecBuilder can update a file-backed spec without losing comments or formatting.

See Describing secrets in Rust for the complete builder and format-preserving editing API.

Typed Rust loaders generated by declare_secrets! can now call prompt_missing() to ask for and store missing required values. Prompting remains opt-in.

SecretSpec 0.20 gives the SDKs the same new capabilities across languages. It also renames secretspec-ffi to libsecretspec. Packaged SDKs handle the rename automatically; update your build only if you link or load libsecretspec directly.

The SDKs for Go, Python, Node.js, Ruby, Haskell, PHP, C#, and Swift can now accept an inline specification. This is useful when the application already owns its configuration or cannot rely on a manifest on disk. For example, in Go:

spec := map[string]any{
"project": map[string]any{"name": "checkout"},
"profiles": map[string]any{"default": map[string]any{
"secrets": map[string]any{
"API_TOKEN": map[string]any{"description": "API token"},
},
}},
}
resolved, err := secretspec.New().
WithInlineSpec(spec, "/logical/project").
Load()

The base directory tells SecretSpec where to resolve relative provider paths and inherited manifests. Invalid fields and unsupported spec versions fail with an error.

Git and Docker set caller context automatically, recording which tool and operation requested a secret. CLI and SDK callers can provide the same context:

Terminal window
$ secretspec get GITHUB_TOKEN \
--caller git \
--caller-version 2.51.0 \
--caller-operation credential_get \
--caller-resource github.com \
--reason "push release tag"

Audit records include the caller name, version, operation, and resource. This is separate from require_reason, so policies can record both which tool accessed a secret and why.

The new JVM SDK lets Java, Kotlin, and other JVM applications load SecretSpec secrets directly. Add it with Gradle:

dependencies {
implementation("org.cachix:secretspec-jvm:0.20.0")
}

Then load secrets with the same builder pattern as the other SDKs:

import org.cachix.secretspec.SecretSpec;
try (var resolved = SecretSpec.builder()
.withProvider("keyring://")
.withProfile("production")
.withReason("boot web app")
.load()) {
System.out.println(resolved.secret("DATABASE_URL").get());
resolved.setAsSystemProperties();
}

The package supports JDK 11 or newer on Linux, macOS, and Windows, including x64 and Arm64 systems. It includes everything needed to resolve secrets, so the application does not need a separate SecretSpec CLI or Rust toolchain.

The standalone installer and secretspec-update now work on Alpine Linux. The Node.js SDK also works directly in images such as node:alpine, without a glibc compatibility layer.

SecretSpec 0.20 adds five providers, bringing the total to 33.

Use Azure App Configuration to read and manage ordinary key-values or follow references to Azure Key Vault secrets.

Terminal window
$ az login
$ secretspec set DATABASE_URL --provider aac://payments-production
$ secretspec run --provider aac://payments-production -- ./payments

It works with service principals, Azure CLI sessions, managed identity, workload identity, and connection strings. Provider URLs can select labels, prefixes, and tags.

Kubernetes stores values in a ConfigMap or Secret using the current kubeconfig context:

Terminal window
$ secretspec set DATABASE_URL \
--provider k8s+secret://app-credentials@production
$ secretspec run \
--provider k8s+secret://app-credentials@production \
-- ./application

You can use convention keys or point declarations at existing .data entries. The provider supports reads, writes, deletion, and declaration discovery.

Shopify’s EJSON lets teams keep encrypted secrets alongside application source. Secret values are encrypted with a public key and decrypted with the matching private key, while the JSON structure stays visible.

SecretSpec’s EJSON provider reads string values from those files. A ref.item selects any RFC 6901 JSON Pointer, while convention addresses use /{project}/{profile}/{key}.

Supply the EJSON private key as a SecretSpec provider credential. It can come from Google Cloud Secret Manager, the system keyring, or any other readable provider. The EJSON provider is read-only.

Use Fly.io to publish and delete application secrets with flyctl. Secret values stay out of process arguments, and app-scoped deploy tokens can come from provider credentials. Fly.io does not return plaintext, so keep the original value in a readable provider and use fly as the deployment destination.

Use Cloudflare Secrets Store to publish and delete account-level secrets through Cloudflare’s API. Authenticate with a scoped API token from another provider or an existing Wrangler session. Cloudflare does not return plaintext, so keep the original value in a readable provider and use cloudflare as the deployment destination.

Shell completions now come directly from the CLI definition:

Terminal window
$ source <(secretspec completions bash)

Bash, Elvish, Fish, Nushell, PowerShell, and Zsh now complete commands, profiles, scopes, secret names, providers, files, and executables. Completion does not read secret values. See secretspec completions for persistent installation instructions (#330).

0.20 also includes these CLI and automation changes:

  • On Unix, secretspec run forwards SIGTERM, SIGINT, and SIGHUP to its child, so containers can shut down gracefully (#391).
  • check --json, check --explain, and SDK reports now mark an unprovisioned required generate declaration as missing_required instead of resolved. Run secretspec check or secretspec run once to generate and store it (#394).
  • Human-readable secretspec check output moves to stdout, matching its JSON and explain modes. Diagnostics remain on stderr (#372).
  • Dotenv files, age-encrypted dotenv blobs, and export --format dotenv now use dotenv-ng. Dollar signs and bcrypt-style values now round-trip correctly (#73).
  • extract supports INI documents, selecting unsectioned keys with /key and named-section keys with /section/key (#386).
  • The age provider can now delete secrets, enabling secretspec delete, import --delete-source, and age-backed provider caches (#328).
  • Closing a stdout pipe is quiet on Unix, so commands such as secretspec export | head behave like other Unix tools (#377).

Provider-specific fixes keep 1Password batches fast when optional items are missing (#401), prevent Bitwarden convention names from colliding across projects and profiles (#390), make Infisical Universal Auth sessions more reliable (#402), and let Node.js applications exit cleanly after AWS resolution (#365).

Terminal window
$ cargo install secretspec

For most users, 0.20 is a drop-in upgrade. The new integrations, providers, and inline declarations are opt-in. Check these cases before upgrading:

  • scripts that captured human-readable secretspec check output from stderr should capture stdout;
  • Google Cloud Secret Manager writes now use the collision-safe secretspec2--{project}--{profile}--{key} convention. Reads fall back to the matching 0.19 name until a write moves the value, leaving the old secret in place for rollback;
  • Bitwarden Password Manager convention items now use secretspec/{project}/{profile}/{key} titles. init --from bw:// preserves references to legacy bare items so they can keep working or be migrated deliberately.

Rust applications using secretspec-derive can remove direct serde or secrecy dependencies if they were needed only for generated types. Code using the old raw configuration or code-generation APIs should move to Spec, SpecBuilder, Profile, and Secret.

See the full changelog for every change and fix in this release.

The resolver and provider IPC protocols are currently RFCs in PR #362. The resolver protocol would let applications request individual secrets from secretspec serve. The provider protocol would let external executables act as SecretSpec providers. Both are targeted for 0.21 and may change during review.

Work on the Nix integration identified that SecretSpec first needs the IPC layer above, so the concrete integration is blocked until 0.21 is released. In the meantime, the underlying refactoring has been split into a generic secret-provider interface for Nix.

Separately, devenv’s experimental machines interface aims to handle NixOS installation, NixOS and nix-darwin deployment, and home-manager activation in one workflow. Its SecretSpec integration can supply deployment credentials without putting their values in Nix evaluation or store paths. This work follows devenv’s own release schedule and is not part of SecretSpec 0.21.

Questions or feedback? Join us on Discord.

In partnership with SecretSpec, Fencer has expanded its offering to open source. Public repositories now get free static analysis, dependency scanning, secret scanning, and GitHub configuration scanning, with no credit card and no expiry.

We recommend Fencer for code security scanning, and Fencer recommends SecretSpec for secrets management. Fencer finds credentials and other risks already in a repository, while SecretSpec helps keep the next secret in the right provider instead of source code or a dotenv file.