Skip to content

Instantly share code, notes, and snippets.

@jdx
Created August 16, 2026 20:06
Show Gist options
  • Select an option

  • Save jdx/8aae42239973b296fb0e48847fd1b6da to your computer and use it in GitHub Desktop.

Select an option

Save jdx/8aae42239973b296fb0e48847fd1b6da to your computer and use it in GitHub Desktop.

fnox and SecretSpec: provider binding

The clearest difference between fnox and SecretSpec is what each configuration treats as its conceptual center.

fnox: concrete source mapping

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.

SecretSpec: logical requirement

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 start

The 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.

Where they converge

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 ordered providers fallback chain.
  • fnox normally specifies a provider-native value for each binding.
  • SecretSpec normally derives the location by convention, though ref can 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment