SecretSpec 0.13: SDKs for Python, Node.js, Go, Ruby, and Haskell
SecretSpec separates what secrets an application needs, declared in
secretspec.toml, from where the values live, a provider like your system
keyring, 1Password, or Vault. Until now, reading those resolved secrets at
runtime meant the CLI or the Rust SDK. If your service was written in Python or
Go, you shelled out to secretspec run or reimplemented resolution yourself.
SecretSpec 0.13 closes that gap. It ships native SDKs for five languages: Python, Node.js / TypeScript, Go, Ruby, and Haskell. Each resolves the exact secrets your manifest declares, through the same providers, profiles, fallback chains, and generators as the CLI, with no per-language configuration.
Native bindings over one resolver
Section titled “Native bindings over one resolver”Every SDK is a thin client over the same Rust core that powers the CLI. No
provider logic, profile resolution, chain fallback, as_path materialization, or
secret generation lives in the binding. A provider added to SecretSpec works in
every language the day it lands, and every SDK behaves identically.
The binding strategy is chosen per ecosystem:
- Python: a pyo3 extension, statically linked, shipped as a self-contained
cp39-abi3wheel. - Node.js: a napi-rs addon with prebuilt per-platform packages.
- Ruby: a native C extension (mkmf) with the resolver statically linked into a platform gem.
- Go: the
secretspec-ffiC ABI loaded at runtime via purego (no cgo). - Haskell: the same C ABI, linked at build time through the Haskell FFI.
The same three steps, in your language
Section titled “The same three steps, in your language”Each SDK mirrors the vocabulary of the Rust derive crate: a builder that takes a
provider, a profile, and an access reason, then load() to resolve, then a map
of secrets you can read or export into the environment.
# Pythonfrom secretspec import SecretSpec
resolved = ( SecretSpec.builder() .with_provider("keyring://") .with_profile("production") .with_reason("boot web app") .load())print(resolved.secrets["DATABASE_URL"].get) # value, or file path for as_pathresolved.set_as_env() # export into os.environ// Node.js / TypeScriptconst { SecretSpec } = require('secretspec');
const resolved = SecretSpec.builder() .withProvider('keyring://') .withProfile('production') .withReason('boot web app') .load();console.log(resolved.secrets.DATABASE_URL.get()); // value, or as_path file pathresolved.setAsEnv(); // export into process.env// Goresolved, err := secretspec.New(). WithProvider("keyring://"). WithProfile("production"). WithReason("boot web app"). Load()fmt.Println(resolved.Secrets["DATABASE_URL"].Get()) // value, or as_path file pathresolved.SetAsEnv() // export into the environment# Rubyresolved = Secretspec::SecretSpec.builder .with_provider("keyring://") .with_profile("production") .with_reason("boot web app") .loadputs resolved.secrets["DATABASE_URL"].get # value, or as_path file pathresolved.set_as_env! # export into ENV-- Haskellresolved <- S.load ( S.builder & S.withProvider "keyring://" & S.withProfile "production" & S.withReason "boot web app" )S.setAsEnv resolved -- export into the environmentAcross all of them, load() resolves every declared secret, a missing required
secret raises a typed MissingRequiredError, and as_path secrets come back as
a readable file path with a cleanup that removes the backing temp file. The
access reason feeds the same audit log and require_reason policy from
0.12, so a Go service is
as accountable as the CLI.
Write your own binding
Section titled “Write your own binding”Under all five SDKs sits a new crate, secretspec-ffi: a small, versioned C ABI
for resolving secrets. If we do not ship your language yet, you can bind to it
directly. It also exposes the public Rust building blocks the SDKs share,
Secrets::resolve() and Secrets::report(), so a Rust program reaches the same
value-carrying and value-free entry points.
Typed secrets, one schema for every language
Section titled “Typed secrets, one schema for every language”secretspec.toml already knows the shape of your secrets, so 0.13 can hand that
shape to your type system. secretspec schema emits a JSON Schema for your
manifest, the union of all profiles or one profile with --profile. Pipe it
through quicktype to generate idiomatic typed classes in
any language, then populate them from each SDK’s fields() map:
secretspec schema | quicktype -s schema --top-level SecretSpec --lang python -o secrets_gen.pytyped = Secrets.from_dict(resolved.fields())print(typed.database_url) # typed strOne schema drives every language’s type system, with no hand-written emitter per language.
Install
Section titled “Install”pip install secretspec # Pythonnpm install secretspec # Node.js / TypeScriptgem install secretspec # Rubygo get github.com/cachix/secretspec/secretspec-go # GoFor Haskell, add secretspec from Hackage to your build-depends. The CLI and
Rust SDK upgrade as usual:
cargo install secretspecSee the SDK overview for the per-language guides. Questions or feedback? Join us on Discord.