Declarative secrets, every environment, any provider

Stop committing secrets to git and putting them to .env files

Section titled “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.

SecretSpec answers three questions for every project:

  • WHAT secrets does the application need?
  • HOW do requirements change per environment?
  • WHERE are the actual values stored?

Read the concepts overview →

WHAT - Declaring Your Secrets

Section titled “WHAT - Declaring Your Secrets”

Applications declare their secret requirements in a secretspec.toml file.

Each secret is defined with its name and description, creating a single source of truth that’s version controlled alongside your code. This standardized format enables ecosystem-wide tooling and ensures every developer knows exactly what secrets the application needs.

Important: The secretspec.toml file only declares which secrets your application needs and their requirements - it never contains actual secret values. Secret values are always retrieved at runtime from your configured provider.

[project]

name = "my-app"

revision = "1.0"

[profiles.default]

DATABASE_URL = { description = "PostgreSQL connection string", required = true }

REDIS_URL = { description = "Redis connection string", required = false }

TLS_CERT = { description = "TLS certificate", as_path = true }

Secrets with as_path = true are written to temporary files - useful for credentials that must be passed as file paths (TLS certs, service account keys).

Secrets with type and generate are automatically created when missing — no manual setup needed for passwords, tokens, and keys:

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

# Initialize secretspec.toml, possibly from `.env`

$ secretspec init --from dotenv

Learn more about declarative configuration →

HOW - Managing Requirements with Profiles

Section titled “HOW - Managing Requirements with Profiles”

SecretSpec’s profile system allows you to specify different requirements, defaults, and validation rules for development, staging, production, or any custom environment.

A secret might be optional with a local default in development but required in production - all without changing your application code.

[project]

name = "my-app"

revision = "1.0"

[profiles.default]

DATABASE_URL = { description = "PostgreSQL connection string", required = true }

REDIS_URL = { description = "Redis connection string", required = false }

[profiles.development]

# Inherits from default profile - only override what changes

DATABASE_URL = { default = "postgresql://localhost/myapp_dev" }

REDIS_URL = { default = "redis://localhost:6379" }

# Run with a specific profile

$ secretspec run --profile development -- npm start

$ secretspec run --profile production -- npm start

# Or use environment variables

$ SECRETSPEC_PROFILE=development secretspec run -- npm start

$ SECRETSPEC_PROFILE=production secretspec run -- npm start

Learn more about profiles →

WHERE - Flexible provisioning with Providers

Section titled “WHERE - Flexible provisioning with Providers”

The same application works across different secret storage backends without any code changes.

# Configure your default provider interactively

$ secretspec config init

? Select your preferred provider backend:

> keyring: Uses system keychain (Recommended)

onepassword: OnePassword password manager

dotenv: Traditional .env files

env: Read-only environment variables

lastpass: LastPass password manager

? Select your default profile:

> development

default

none

Configuration saved to ~/.config/secretspec/config.toml

Supported providers:

Learn how to add a new provider →

Per-Secret Providers

Section titled “Per-Secret Providers”

Individual secrets can specify their own providers with fallback chains:

[profiles.production.defaults]

providers = ["vault", "keyring"] # Default for all secrets in this profile

[profiles.production]

DATABASE_URL = { description = "Production DB" } # Uses profile defaults

API_KEY = { description = "API key", providers = ["env"] } # Override: env only

SHARED_SECRET = { description = "Shared", providers = ["team_vault", "keyring"] } # Override

Provider aliases are configured in ~/.config/secretspec/config.toml:

$ secretspec config provider add vault "onepassword://Production"

$ secretspec config provider add team_vault "onepassword://Shared"

# Check all secrets are available and set them if not

$ secretspec check

$ secretspec set DATABASE_URL

# Override provider for specific commands

$ secretspec run --provider env -- npm test

$ secretspec run --provider onepassword://vault -- npm start

# Or use environment variables

$ SECRETSPEC_PROVIDER=env secretspec run -- npm test

$ SECRETSPEC_PROVIDER=onepassword://vault secretspec run -- npm start

Learn more about providers →

Migrating Between Providers

Section titled “Migrating Between Providers”

SecretSpec makes it easy to migrate your secrets between different providers without changing your application code.

# Import all secrets from one provider to another

$ secretspec import dotenv://.env.production

Imported 5 secrets from dotenv://.env.production to keyring://

This separation enables portable applications with lower operational overhead when switching providers.

Type-Safe Rust SDK

Section titled “Type-Safe Rust SDK”

While the CLI is great for development workflows, integrating SecretSpec directly into your application provides better type safety and error handling.

The Rust SDK generates strongly-typed structs from your secretspec.toml, ensuring compile-time verification of your secret access.

// Generate typed structs from secretspec.toml

secretspec_derive::declare_secrets!("secretspec.toml");

fn main() -> Result<(), Box<dyn std::error::Error>> {

// Load secrets using the builder pattern

let secrets = Secrets::builder()

.with_provider("keyring") // Can use provider name or URI like "dotenv:/path/to/.env"

.with_profile("production") // Can use string or Profile enum

.load()?; // All conversions and errors are handled here

// Access secrets (field names are lowercased)

println!("Database: {}", secrets.secrets.database_url); // DATABASE_URL → database_url

// Optional secrets are Option<String>

if let Some(redis_url) = &secrets.secrets.redis_url {

println!("Redis: {}", redis_url);

}

// Set all secrets as environment variables

secrets.secrets.set_as_env_vars();

Ok(())

}

Learn more about the Rust SDK →

SDKs for other languages are welcome! Please see our contribution guide if you’d like to help.


SecretSpec was designed by Cachix for devenv.sh. See the announcement post.