Skip to content
Latest from the blog Jul 16, 2026 SecretSpec 0.15: Provider credentials, Azure Key Vault / Gopass, and PHP SDK Authenticate providers from another secret store, use Azure Key Vault or Gopass, export secrets for CI, and resolve them from PHP.

Declarative secrets manager

Declare secrets once.
Store them anywhere.

Stop leaking .env files. Define what your application needs in secretspec.toml, then plug in keyring, 1Password, Vault, AWS, Azure, Gopass, or any of 13 providers — same code, every environment.

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 storage backend (one-time)
$ secretspec config init
? Select your preferred provider backend:
 keyring: Uses system keychain (Recommended)
  infisical: Infisical secret management (0.16+)
 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.

1

Declare

Generate secretspec.toml from your existing .env, or write it by hand. Names and descriptions only — never values.

$ secretspec init --from dotenv
2

Configure

Pick from 13 providers: system keyring, password managers, cloud secret stores, Gopass, dotenv, or environment variables.

$ secretspec config init
? Select your preferred provider backend:
> keyring: Uses system keychain (Recommended)
  infisical: Infisical secret management (0.16+)
3

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.

13

Providers

Keyring · 1Password · LastPass · Bitwarden · Vault · AWS · GCP · Azure · Gopass · Pass · Proton Pass · dotenv · env.

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.

Export for shells and CI

Resolve a complete profile as shell exports, dotenv, JSON, or masked GitHub Actions environment variables.

$ secretspec export --format json
Declaration vs storage

Declare a secret. Swap the source.

Your secretspec.toml never changes. The same code reads from Keychain, 1Password, Vault, AWS, Azure, or any of 13 providers.

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

[profiles.default]
DATABASE_URL      = { required = true }
STRIPE_SECRET_KEY = { required = true }
Source: System keyring
secretspec.toml declares DATABASE_URL
$ secretspec run — injected as env var at runtime
Your app reads DATABASE_URL from env
Profiles

One config. Every environment.

Profiles let the same secret be optional in development, required in production — without changing application code. How profiles work →

[profiles.default]
DATABASE_URL = { required = true }
[profiles.development]
DATABASE_URL = { default = "postgresql://localhost/dev" }
[profiles.production]
DATABASE_URL = { providers = ["vault", "keyring"] }
Same command. Every profile.
$ secretspec run --profile production -- npm start
--profile development --profile staging --profile production SECRETSPEC_PROFILE=ci
Per-secret providers

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.

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

Same code. Same secret. Different source per machine.

SecretSpec 0.15 · Provider credentials

Keep the key to your vault in another vault.

Authenticate a provider with credentials stored in another provider. SecretSpec fetches them in memory, hands them directly to the destination store, and never exposes them to your application environment. How provider credentials work →

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

Use convention paths for simple credentials, or a ref to source a specific 1Password field, Vault entry, or other provider-native address. Credential access is audited and cached for the invocation.

Secret references

Name it once. Point it anywhere.

Already managing a secret in 1Password, Vault, or a .env file? Point at it by the store's own coordinates with ref. No renaming, no copying it into SecretSpec's own layout. How references work →

secretspec.toml
[profiles.production]
# Point at a secret that already lives in your store
DATABASE_URL = {
  description = "Postgres DSN",
  ref = { item = "db", field = "password" }
}
A ref names the store's own coordinates: item plus an optional field
The same coordinates work in 1Password, Vault, keyring, or .env

Point at secrets you already have. Switch stores without editing the ref.

Audit & AI agents

Know who read a secret — and why.

Every access is appended to a local, append-only log — values never written, URI credentials redacted. And when an AI agent is driving, SecretSpec makes it state a reason first. How auditing works →

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

# State why — required for 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 1
2026-06-04T17:04Z  run  started  ./deploy.sh
  DATABASE_URL  (my-app/production via keyring://)
  reason: Deploy web frontend  [claude-code]

The default require_reason = "agents" policy is checked into secretspec.toml, so every clone, CI runner, and agent is held to it — humans running interactively are unaffected. Configure the policy →

SecretSpec 0.15 · Export

Resolve once. Hand secrets to anything.

secretspec export resolves the complete active profile without launching a command. It never prompts and exits non-zero when a required secret is missing, making it a clean boundary for shells, scripts, and CI. Export command reference →

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 Actions
# Make secrets available to later workflow steps
- name: Export secrets
  run: secretspec export --format gha

- name: Deploy
  run: ./deploy

# Values are added to $GITHUB_ENV and masked in logs

Choose shell, dotenv, json, or gha output without changing how secrets are declared or routed.

Language SDKs

Use it from any language.

Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, and C# (0.16+) all resolve from the same secretspec.toml through one Rust core — every provider, chain, and profile behaves identically, with no per-language logic. How the SDKs work →

Python
from secretspec import SecretSpec

s = SecretSpec.builder() \
    .with_provider("keyring://") \
    .with_reason("boot").load()
print(s.secrets["DATABASE_URL"].get)
Node.js
const { SecretSpec } = require("secretspec");

const s = SecretSpec.builder()
  .withProvider("keyring://")
  .withReason("boot").load();
console.log(s.secrets.DATABASE_URL.get());
Go
s, _ := secretspec.New().
    WithProvider("keyring://").
    WithReason("boot").Load()
fmt.Println(s.Secrets["DATABASE_URL"].Get())
Ruby
s = Secretspec::SecretSpec.builder
      .with_provider("keyring://")
      .with_reason("boot").load
puts s.secrets["DATABASE_URL"].get
Haskell
s <- S.load (S.builder
      & S.withProvider "keyring://"
      & S.withReason "boot")
print (S.get =<< Map.lookup "DATABASE_URL" (S.resolvedSecrets s))
PHP · Laravel · Symfony
use Secretspec\SecretSpec;

$s = SecretSpec::builder()
    ->withProvider('keyring://')
    ->withReason('boot web app')->load();
$s->setAsEnv();
C# / .NET (0.16+)
using Cachix.SecretSpec;

using var s = SecretSpec.Builder()
    .WithProvider("keyring://")
    .WithReason("boot").Load();
Console.WriteLine(s.Secrets["DATABASE_URL"].Get());

Every SDK is a thin client over one native core — the secretspec-ffi library, or a native extension that embeds it for Python, Node, and PHP — so a new provider is available everywhere at once. For typed access in the dynamic languages, secretspec schema feeds quicktype to generate typed classes. SDK overview →

Migration

Switch backends with one command.

Outgrowing dotenv? Standardizing on Vault? secretspec import moves every secret without touching a single line of application code.

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

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 →

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;
}

Switch providers per machine without touching devenv.nix: devenv --secretspec-provider dotenv --secretspec-profile dev shell.

Stop leaking secrets.

Declare what your application needs. Store the values anywhere. Onboard new developers in one command.