Skip to content
Latest from the blog Aug 3, 2026 SecretSpec 0.18: Secret lifecycle, Bitwarden, Keeper, AWS Parameter Store, and Swift

A declarative interface
for every secret provider.

Manage secrets without hard-wiring your app to a secrets provider or leaking them through environment variables.
One contract. Any of 27 providers. Development, CI, and production.

macOS Keychain
KeePass KDBX (0.17+)
1Password
Keeper Secrets Manager (0.18+)
LastPass
Dashlane (0.18+)
Bitwarden Password Manager (0.18+)
Vault
OpenBao (0.17+)
AWS Secrets Manager
AWS Parameter Store (0.18+)
Scaleway Secret Manager (0.17+)
Google Cloud Secret Manager
Azure Key Vault
Infisical
age (0.17+)
Gopass
Proton Pass
Passbolt (0.19+)
Pass (GPG)
.env files
Plaintext files (0.19+)
Environment variables
Defaults / ephemeral generation (null, 0.19+)
systemd credentials (0.17+)
Bitwarden Secrets Manager
Secret Service
Credential Manager
SOPS (0.17+)
secretspec.toml
[project]
name = "my-app"
revision = "1.0"

[profiles.default]
DATABASE_URL      = { required = true }
API_TOKEN         = { required = true }
HMAC_SECRET       = { required = true }

Eliminate secrets from environment variables and configuration files.

Read Where .env Went Wrong and Secrets Don’t Belong in Config.

secretspec.toml
[project]
name = "my-app"
revision = "1.0"

[profiles.default]
DATABASE_URL = { description = "PostgreSQL", required = true }
REDIS_URL    = { description = "Redis cache" }
TLS_CERT     = { as_path = true }
DB_PASSWORD  = { type = "password", generate = true }

[profiles.development]
# Inherits from default; override what changes
DATABASE_URL = { default = "postgresql://localhost/dev" }
terminal
# 1. Initialize from existing .env
$ secretspec init --from dotenv
 Created secretspec.toml with 5 secrets

# 2. Pick a user-global default (0.17+)
$ secretspec config global init
? Select your preferred provider backend:
keyring: Uses system keychain (Recommended)
null: Defaults or ephemeral generation, no storage (0.19+)
file: Plaintext files, one per secret (0.19+)
bw: Bitwarden Password Manager (0.18+)
kdbx: KeePass KDBX databases (0.17+)
keeper: Keeper Secrets Manager (0.18+) via official Rust SDK
passbolt: Passbolt self-hosted password manager (0.19+) via go-passbolt-cli
infisical: Infisical secret management
openbao: OpenBao secret management (0.17+)
age: age-encrypted file (0.17+)
sops: SOPS encrypted files (0.17+)
systemd-credential: Read-only systemd service credentials (0.17+)
awsps: AWS Systems Manager Parameter Store (0.18+)
scaleway: Scaleway Secret Manager (0.17+)
dashlane: Dashlane password manager, read-only (0.18+)
 Saved to ~/.config/secretspec/config.toml

# 3. Run your app with secrets injected
$ secretspec run --profile production -- npm start
 Loaded DATABASE_URL from keyring
 Loaded REDIS_URL    from keyring
 Generated DB_PASSWORD (32 chars)
 Wrote TLS_CERT to /tmp/secretspec-tls-cert
 npm start

Make the declaration your source of truth.

One manifest defines what your app needs. Profiles choose when, providers where, and scopes (0.17+) which consumer receives it.

27

Providers

Keep values in a local keyring, a team password manager, a cloud secret store, .env, or environment variables.

[defaults]
provider = "keyring"

Profiles

Use convenient local defaults while requiring managed values in production, without changing application code.

[profiles.production]
DATABASE_URL = { required = true }

Auto-generation

Create a password, token, or key automatically when a required value does not exist yet.

DB_PASSWORD = {
  type = "password",
  generate = true
}

Scopes (0.17+)

Resolve and expose only the declared subset of secrets that each service, command, or task needs.

[scopes.api]
secrets = ["DATABASE_URL", "API_KEY"]

Provider caching (0.17+)

Reuse a fresh local copy of a slow provider route for a configured window, with explicit invalidation.

fast_vault = {
  fallback = ["vault"],
  cache = {
    provider = "local",
    max_age = "8h"
  }
}

Credential alternatives (0.17+)

Require at least one credential—or exactly one—without treating every alternative as independently required.

PASSWORD = {
  required = { at_least_one = "auth" }
}
ACCESS_TOKEN = {
  required = { at_least_one = "auth" }
}

Composed secrets

Store credentials separately, then assemble connection strings and other application-ready values during resolution.

DATABASE_URL = {
  composed = "postgres://${USER}:${PASSWORD}@${HOST}/app"
}

Per-secret fallback chains

Look in the team store first, then fall back to a personal keyring or environment variable when needed.

API_KEY = { providers = ["vault", "keyring", "env"] }

Type-safe Rust SDK

Generate typed Rust fields from secretspec.toml so misspelled secret names fail at compile time.

secretspec_derive::declare_secrets!("secretspec.toml");
let s = Secrets::builder().load()?;
println!("{}", s.secrets.database_url);

Config inheritance

Share a base declaration across services, then add or override only what each service needs.

extends = ["../shared/common"]

File-path secrets

Materialize certificates and service account keys as temporary files for applications that expect a path.

TLS_CERT = { as_path = true }

Provider migration

Copy values to a new provider without renaming variables or changing application code.

$ secretspec import dotenv://.env

Export for shells and CI

Pass a resolved profile to shells, scripts, and CI as dotenv, JSON, shell exports, or GitHub Actions variables.

$ secretspec export --format json
Profiles

Change requirements by environment.

Share requirements. Use local defaults in development and managed providers in production. Profiles →

[profiles.default]
DATABASE_URL = { required = true }
[profiles.development]
DATABASE_URL = { default = "postgresql://localhost/dev" }
[profiles.production]
DATABASE_URL = { providers = ["vault", "keyring"] }
Resolved profile
$ secretspec run --profile development -- npm start
Local default DATABASE_URL = postgresql://localhost/dev
Managed providers DATABASE_URL resolves from vault → keyring
--profile development --profile production
Scopes · 0.17+

Give each service only the secrets it needs.

Allowlist secrets for each service, command, or task. Everything else stays out of the child process. Scopes →

secretspec.toml
[profiles.default]
DATABASE_URL = { description = "Database" }
API_KEY      = { description = "API key" }
QUEUE_TOKEN  = { description = "Queue token" }
[scopes.api]
secrets = ["DATABASE_URL", "API_KEY"]
[scopes.worker]
secrets = ["DATABASE_URL", "QUEUE_TOKEN"]
terminal
# The API receives its database and API credentials
$ secretspec run --profile production --scope api -- ./api
 Loaded DATABASE_URL
 Loaded API_KEY
 ./api

# The worker receives its database and queue credentials
$ secretspec run --profile production --scope worker -- ./worker
 Loaded DATABASE_URL
 Loaded QUEUE_TOKEN
 ./worker

Scopes minimize secret delivery; they are not an authorization boundary when the child process can access provider credentials.

Per-secret providers

Give each secret its own fallback.

Order providers per secret: team vault first, then a personal keyring or CI environment.

secretspec.toml
[providers]
team_vault = "onepassword://Shared"
keyring    = "keyring://"
env        = "env://"

[profiles.production]
API_KEY = {
  description = "Third-party API key",
  providers = ["team_vault", "keyring", "env"]
}
Look up API_KEY in team_vault (1Password)
Found in keyring

One declaration, with a source that can vary by machine.

Provider credentials

Keep provider credentials out of your app.

Keep provider credentials in a trusted store such as the OS keyring. Unlock remote providers without adding credentials to your app's environment. Provider credentials →

secretspec.toml
[providers]
keyring = "keyring://"

[providers.bws]
uri = "bws://project-uuid"
credentials = { access_token = "keyring" }

[profiles.production]
DATABASE_URL = {
  description = "Production database",
  providers = ["bws"]
}
terminal
# Store the Bitwarden token in the OS keyring
$ secretspec config provider login bws
Enter access_token for provider 'bws': ****
 stored access_token in keyring

# SecretSpec authenticates BWS without exporting its token
$ secretspec run --provider bws -- ./deploy
 Loaded DATABASE_URL from bws
 ./deploy

Provider credentials are resolved separately from application secrets, so your application receives DATABASE_URL but not the token used to fetch it.

Secret references

Use secrets where they already live.

Point a variable at an existing item or field in 1Password, Vault, or another provider. No renaming. No copying. References →

secretspec.toml
[profiles.production]
# Point at a secret that already lives in your store
DATABASE_URL = {
  description = "Postgres DSN",
  ref = { item = "db", field = "password" }
}
Application variable DATABASE_URL
Existing store location item: dbfield: password
Process environment DATABASE_URL

Adopt SecretSpec without moving or renaming existing secrets.

Audit & AI agents

See which secrets were accessed, by whom, and why.

Record access in a local, append-only log—never secret values. Require AI agents, or every caller, to provide a reason. Audit logs →

detected agent session
# A detected AI agent runs your app without a reason
$ secretspec run -- ./deploy.sh
Error: accessing secrets requires a reason.
       Provide one with --reason "<why...>"

# State why; required for detected agents by default
$ secretspec run --reason "Deploy web frontend" \
    -- ./deploy.sh
 Loaded DATABASE_URL from keyring
 ./deploy.sh
secretspec audit
# Review who accessed what, why, and the outcome
$ secretspec audit -n 2

2026-06-04T17:03Z blocked
  command: ./deploy.sh
  actor: [claude-code]
  reason: missing
2026-06-04T17:04Z started
  command: ./deploy.sh
  secret: DATABASE_URL (my-app/production via keyring://)
  reason: Deploy web frontend [claude-code]

Commit require_reason = "agents" to require reasons in sessions SecretSpec detects as agents. Detection is heuristic; use require_reason = true to require every SecretSpec caller to supply one. Configure the policy →

Export & Actions

Send secrets to shells, tools, and CI.

Export a profile to any shell or tool. Resolve it in GitHub or Forgejo Actions with one step. Required secrets are validated first. Export →

shell and JSON
# Load a profile into the current shell
$ eval "$(secretspec export --profile production)"

# Or pass a JSON object to another tool
$ secretspec export --format json
{"DATABASE_URL":"postgresql://…",
 "STRIPE_KEY":"sk_live_…"}
GitHub & Forgejo Actions (0.17+)
# Resolve only the API deployment scope
- uses: actions/checkout@v7
- uses: cachix/secretspec-action@main
  with:
    profile: production
    scope: api
- run: ./deploy.sh

# Later steps receive masked, validated secrets

The 0.17+ action masks every value, fails when a required secret is missing, and exposes the selected scope to later steps. Vault and OpenBao can use the runner's OIDC identity instead of a stored provider credential. Configure Actions →

Language SDKs

Resolve secrets directly from your language.

One resolver across 9 SDKs—Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, C#, and Swift (0.18+). Same profiles. Same providers. SDKs →

Rust
secretspec_derive::declare_secrets!("secretspec.toml"); let s = SecretSpec::builder()    .with_provider("keyring://")    .with_reason("boot").load()?;println!("{}", s.secrets.database_url);

Choose the API for your language without reimplementing secret resolution. Generate typed models from secretspec schema when you want typed access. SDK overview →

Remote secrets · 0.17+

Remote secrets, local speed.

Fetch from Azure once. Serve reads locally until the cache expires. Provider caching →

Your App requests DATABASE_URL from SecretSpec. Azure Key Vault returns it once, then SecretSpec sends one copy to your app and one to the local keyring cache. For the next eight hours, Azure remains inactive while repeat reads are served locally. When the cache expires, SecretSpec contacts Azure again.

Azure contacted once per freshness window Fresh reads stay on this machine
secretspec.toml
[providers]
local_cache = "keyring://secretspec/cache/{project}/{profile}/{key}"
azure = "akv://team-vault"

cached_azure = {
  fallback = ["azure"],
  cache = { provider = "local_cache", max_age = "8h" }
}

[profiles.default]
DATABASE_URL = { providers = ["cached_azure"] }
Credential bootstrapping · 0.15+

Start local. Unlock remote.

Store credentials locally. Unlock remote providers. Deliver only the secrets your app requests. Provider credentials →

Your App asks SecretSpec for DATABASE_URL. SecretSpec resolves a 1Password service account token from the local keyring provider named bootstrap and uses it to unlock the 1Password vault. 1Password returns DATABASE_URL through SecretSpec to your app. Your app receives DATABASE_URL but never receives the 1Password credential.

Your App receives DATABASE_URL Your App never receives the 1Password credential
secretspec.toml
[providers]
bootstrap = "keyring://"
onepassword = {
  uri = "onepassword://Production",
  credentials = { service_account_token = "bootstrap" }
}

[profiles.default]
DATABASE_URL = { providers = ["onepassword"] }
Migration

Move secrets without changing your application.

Move values from .env or any provider—without renaming secrets or changing your app.

1 · From
dotenv://.env.production
DATABASE_URL=postgres://…
STRIPE_SECRET_KEY=sk_live_…
REDIS_URL=redis://…
2 · To
keyring://
$ secretspec import dotenv://.env.production
 Imported 5 secrets to keyring://
 Application code unchanged
devenv & Nix

Use SecretSpec with devenv and Nix.

Enable the integration. Choose a provider and profile. Use resolved values in shells, services, and processes. devenv docs →

devenv.yaml
secretspec:
  enable: true
  provider: keyring   # keyring, dotenv, env, 1password, …
  profile: default
devenv.nix
{ config, ... }:
{
  # Wire any declared secret into the shell env
  env.DATABASE_URL = config.secretspec.secrets.DATABASE_URL;
}

Each machine can select its own provider and profile without changing the shared devenv.nix.

Stop leaking .env files.

Import .env. Commit the declaration. Let each environment choose its provider.