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.
[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.
[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" } # 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.
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 Change requirements by environment.
Share requirements. Use local defaults in development and managed providers in production. Profiles →
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 →
[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"] # 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.
Give each secret its own fallback.
Order providers per secret: team vault first, then a personal keyring or CI environment.
[providers]
team_vault = "onepassword://Shared"
keyring = "keyring://"
env = "env://"
[profiles.production]
API_KEY = {
description = "Third-party API key",
providers = ["team_vault", "keyring", "env"]
} API_KEY in team_vault (1Password) One declaration, with a source that can vary by machine.
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 →
[providers]
keyring = "keyring://"
[providers.bws]
uri = "bws://project-uuid"
credentials = { access_token = "keyring" }
[profiles.production]
DATABASE_URL = {
description = "Production database",
providers = ["bws"]
} # 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.
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 →
[profiles.production]
# Point at a secret that already lives in your store
DATABASE_URL = {
description = "Postgres DSN",
ref = { item = "db", field = "password" }
} Adopt SecretSpec without moving or renaming existing secrets.
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 →
# 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 # 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 →
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 →
# 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_…"} # 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 →
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 →
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, 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.
[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"] } 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.
[providers]
bootstrap = "keyring://"
onepassword = {
uri = "onepassword://Production",
credentials = { service_account_token = "bootstrap" }
}
[profiles.default]
DATABASE_URL = { providers = ["onepassword"] } Move secrets without changing your application.
Move values from .env or any provider—without renaming secrets or changing your app.
DATABASE_URL=postgres://…
STRIPE_SECRET_KEY=sk_live_…
REDIS_URL=redis://… $ secretspec import dotenv://.env.production
✓ Imported 5 secrets to keyring://
✓ Application code unchanged Use SecretSpec with devenv and Nix.
Enable the integration. Choose a provider and profile. Use resolved values in shells, services, and processes. devenv docs →
secretspec:
enable: true
provider: keyring # keyring, dotenv, env, 1password, …
profile: default { 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.
Provider trait and ship a new backend. Stop leaking .env files.
Import .env. Commit the declaration. Let each environment choose its provider.