A declarative interface for every secret provider.
A declarative interface for every secret provider.
Stop committing secrets to git and putting them to .env files.
Secrets end up in .env files that get accidentally committed, shared over Slack, or copy pasted between machines. Each developer has their own version, nobody knows which secrets are actually needed, and onboarding means asking around for values.
SecretSpec fixes this by separating secret declaration from secret storage. You commit a secretspec.toml that declares what secrets your application needs, while the actual values live in a secure provider like your system keyring, 1Password, or any other backend. No secrets in git, no .env files to leak.
Documentation | Quick Start | Announcement Blog Post
secretspec.toml with descriptions and requirementsbws CLI required in SecretSpec 0.17+)secretspec.toml for compile-time safetyprompt = true to request a hidden missing value; writable providers save it, while null keeps it invocation-only${UPPERCASE_NAME} referencesextends featuregit send-email SMTP operations through any SecretSpec providersecretspec init to discover secrets from existing .env filesapiKeyHelper…See the Quick Start Guide for detailed instructions.
curl -sSL https://install.secretspec.dev | shSecretSpec 0.20+ provides static musl CLI binaries for x64 and arm64 Linux, so the installer works directly on Alpine without a glibc compatibility layer.
See the installation guide for more options including Nix and Devenv.
Each project has a secretspec.toml file that declares the required secrets:
…SecretSpec 0.19+ can map one logical secret to different native coordinates per provider alias, including independent import source and destination refs:
[providers]
remote = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } }
local = "keyring://"
[profiles.production]
API_KEY = { description = "API key", providers = ["local"], refs = { remote = { item = "legacy-api", field = "token" } } }See the configuration reference for all available options.
Profiles allow you to define different secret requirements for each environment (development, production, etc.):
$ secretspec run --profile development -- npm start
$ secretspec run --profile production -- npm start
# Set a user-global default profile (0.17+)
$ secretspec config global init
# SecretSpec 0.17+: set provider and profile defaults without prompting
$ secretspec config global init --provider env --profile defaultLearn more about profiles and profile selection.
SecretSpec supports multiple storage backends for secrets:
flyctl; Fly.io does not expose plaintext valuesdcli CLIopenbao:// through the Vault providerbw CLIbws CLI required in SecretSpec 0.17+)$ secretspec run --provider keyring -- npm start
$ secretspec run --provider dotenv -- npm start
# Configure a user-global default provider (0.17+)
$ secretspec config global initSee provider concepts and provider reference for details.
Generate strongly-typed Rust structs from your secretspec.toml:
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result> {
// Load secrets with type safety
let secrets = SecretSpec::load(Provider::Keyring)?;
// Access secrets as struct fields
println!("Database: {}", secrets.database_url);
// Optional secrets are Option
if let Some(redis) = &secrets.redis_url {
println!("Redis: {}", redis);
}
Ok(())
}See the Rust SDK documentation for advanced usage including profile-specific types.
Beyond Rust, SecretSpec ships SDKs for other languages. Each is a thin client over the same native core, so every provider, chain, profile, and generator works identically with no per-language resolution logic:
The shared embedded ABI is named libsecretspec in SecretSpec 0.20+; i
No open issues yet, or sync has not completed.