The clearest difference between fnox and SecretSpec is what each configuration treats as its conceptual center.
fnox records where each value lives:
[providers]
age = { type = "age", recipients = ["age1..."] }
[secrets]
DATABASE_URL = {
provider = "age",
value = "AGE_ENCRYPTED_VALUE..."
}
[profiles.production.providers]
aws = { type = "aws-sm", region = "us-east-1" }
[profiles.production.secrets]
DATABASE_URL = {
provider = "aws",
value = "database-url"
}The model is:
environment variable -> provider -> provider-native location/value
Changing storage normally means overriding the secret definition, including both provider and value. Mixing concrete providers is a feature: one value can come from AWS, another from 1Password, and another from an age-encrypted value.
A provider-independent SecretSpec manifest can describe only what the application needs:
[project]
name = "my-app"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "Application database" }The storage backend can then be selected separately:
# Developer's ~/.config/secretspec/config.toml
[defaults]
provider = "keyring"# CI
secretspec run --provider env -- npm test
# Another environment
secretspec run --provider "onepassword://Production" -- npm startThe logical secret remains DATABASE_URL; SecretSpec can derive its storage address from the project, profile, and key. The whole resolution can be routed elsewhere without changing the declaration.
SecretSpec can also bind individual secrets explicitly:
[providers]
prod_vault = "onepassword://Production"
shared_vault = "onepassword://Shared"
[profiles.production]
DATABASE_URL = {
description = "Production database"
providers = ["prod_vault"]
}
SENTRY_DSN = {
description = "Error reporting"
providers = ["shared_vault"]
}At that point the two configurations look similar. The remaining differences are:
- fnox uses one
provider; SecretSpec supports an orderedprovidersfallback chain. - fnox normally specifies a provider-native
valuefor each binding. - SecretSpec normally derives the location by convention, though
refcan override it. - fnox profiles can replace the concrete provider binding.
- SecretSpec treats the declaration as the application contract and provider routing as an optional layer.
In short:
# fnox: concrete source mapping
DATABASE_URL = { provider = "aws", value = "database-url" }
# SecretSpec: logical requirement
DATABASE_URL = { description = "Application database" }fnox centers the source mapping; SecretSpec centers the provider-independent declaration.
AI-assisted — Tool: Codex; model: unavailable/unavailable; version: unavailable.