Declarative secrets manager
Declare secrets once.
Store them anywhere.
Stop committing .env files. Define what your application needs in secretspec.toml, then plug in keyring, 1Password, Vault, AWS, or any of 11 backends — same code, every environment.
[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 storage backend (one-time)
$ secretspec config init
✓ 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 One declaration · Any provider
Three commands. Done.
From a leaky .env to a portable, declarative secrets setup the whole team can use.
Declare
Generate secretspec.toml from your existing .env, or write it by hand. Names and descriptions only — never values.
$ secretspec init --from dotenv Configure
Pick a backend: system keyring, 1Password, Vault, AWS, GCP, Bitwarden, Pass, dotenv, or env vars.
$ secretspec config init
? Default provider: keyring Run
Secrets are loaded at runtime and injected as environment variables. Same command, every environment.
$ secretspec run -- npm start One file. Every secret. Every environment.
Built around the three questions every project answers: what secrets, how requirements differ per environment, and where values live.
Providers
Keyring · 1Password · LastPass · Bitwarden · Vault · AWS · GCP · Pass · Proton Pass · dotenv · env — pluggable trait-based architecture.
Profiles
Different requirements, defaults, and validation per environment. Optional locally, required in production — without touching app code.
[profiles.production]
DATABASE_URL = { required = true } Auto-generation
Passwords, tokens, and keys created automatically when missing — no manual setup, no copy paste.
DB_PASSWORD = { type = "password", generate = true } Per-secret fallback chains
Each secret can specify its own ordered provider list. Tries Vault first, falls back to keyring, then env — until the value is found.
API_KEY = { providers = ["vault", "keyring", "env"] } Type-safe Rust SDK
Proc macro reads secretspec.toml at compile time and generates strongly-typed structs. Typos fail to compile.
secretspec_derive::declare_secrets!("secretspec.toml");
let s = Secrets::builder().load()?;
println!("{}", s.secrets.database_url); Config inheritance
Extend shared configs across services with extends. Define once, reuse everywhere with proper precedence.
File-path secrets
Secrets with as_path = true get written to temp files — perfect for TLS certs and service account keys.
One-line migration
Move all secrets between providers without changing application code. secretspec import does the rest.
Declare a secret. Swap the source.
Your secretspec.toml never changes. The same code reads from Keychain, 1Password, Vault, AWS, or any of 11 backends.
[project]
name = "my-app"
[profiles.default]
DATABASE_URL = { required = true }
STRIPE_SECRET_KEY = { required = true } DATABASE_URL $ secretspec run — injected as env var at runtime DATABASE_URL from env One config. Every environment.
Profiles let the same secret be optional in development, required in production — without changing application code. How profiles work →
Fallback chains, per secret.
Specify an ordered list of providers for each secret. SecretSpec walks the chain until it finds the value — perfect for shared team vaults with personal overrides.
[providers]
team_vault = "onepassword://vault/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) Same code. Same secret. Different source per machine.
Compile-time secrets in Rust.
The proc macro reads secretspec.toml at compile time and generates strongly-typed structs. Misspelling a secret name fails the build, not your deploy. SDK reference →
// Generate typed structs from secretspec.toml
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let secrets = Secrets::builder()
.with_provider("keyring")
.with_profile(Profile::Production)
.load()?;
// DATABASE_URL → database_url (compile-time checked)
println!("Database: {}", secrets.secrets.database_url);
// Optional secrets are Option<String>
if let Some(redis) = &secrets.secrets.redis_url {
println!("Redis: {}", redis);
}
secrets.secrets.set_as_env_vars();
Ok(())
} Switch backends with one command.
Outgrowing dotenv? Standardizing on Vault? secretspec import moves every secret without touching a single line of application code.
DATABASE_URL=postgres://…
STRIPE_SECRET_KEY=sk_live_…
REDIS_URL=redis://… $ secretspec import dotenv://.env.production
✓ Imported 5 secrets to keyring://
✓ Application code unchanged Native devenv & Nix integration.
Enable SecretSpec in your devenv.yaml and every secret declared in secretspec.toml is loaded into config.secretspec.secrets — wire them into env vars, services, or processes from devenv.nix. 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;
}
Switch providers per machine without touching devenv.nix: devenv --secretspec-provider dotenv --secretspec-profile dev shell.
Provider trait and ship a new backend. Stop leaking secrets.
Declare what your application needs. Store the values anywhere. Onboard new developers in one command.