Skip to content
Latest from the blog Sep 2, 2026 Claude Code Stores OAuth Tokens in Plaintext On Linux, Claude Code protects MCP OAuth credentials with file permissions—not encryption. It should let users choose a real secret store.

Declarative secrets manager

Declare secrets once.
Store them anywhere.

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

monosecret.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
$ monosecret init --from dotenv
 Created monosecret.toml with 5 secrets

# 2. Pick a storage backend (one-time)
$ monosecret config init
? Default provider: keyring, dotenv, env, onepassword, lastpass, bws, vault, awssm, gcsm, protonpass, pass, age (0.2+), akv (0.2+), awsps (0.2+), bw (0.2+), dashlane (0.2+), file (0.2+), gopass (0.2+), infisical (0.2+), kdbx (0.2+), keeper (0.2+), null (0.2+), openbao (0.2+), passbolt (0.2+), scaleway (0.2+), sops (0.2+), systemd-credential (0.2+), aac (0.20+), fly (0.20+), cloudflare (0.20+), kubernetes (0.20+)
 Saved to ~/.config/monosecret/config.toml

# 3. Run your app with secrets injected
$ monosecret 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/monosecret-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 monosecret.toml from your existing .env, or write it by hand. Names and descriptions only — never values.

$ monosecret init --from dotenv
2

Configure

Pick any supported provider, from a local keyring or encrypted file to a team password manager or cloud secret store.

$ monosecret config init
? Default provider: keyring, dotenv, env, onepassword, lastpass, bws, vault, awssm, gcsm, protonpass, pass, age (0.2+), akv (0.2+), awsps (0.2+), bw (0.2+), dashlane (0.2+), file (0.2+), gopass (0.2+), infisical (0.2+), kdbx (0.2+), keeper (0.2+), null (0.2+), openbao (0.2+), passbolt (0.2+), scaleway (0.2+), sops (0.2+), systemd-credential (0.2+), aac (0.20+), fly (0.20+), cloudflare (0.20+), kubernetes (0.20+)
3

Run

Secrets are loaded at runtime and injected as environment variables. Same command, every environment.

$ monosecret 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.

Declaration vs storage

Declare a secret. Swap the source.

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

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

[profiles.default]
DATABASE_URL      = { required = true }
STRIPE_SECRET_KEY = { required = true }
Source: System keyring
monosecret.toml declares DATABASE_URL
$ monosecret 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.
$ monosecret run --profile production -- npm start
--profile development --profile staging --profile production MONOSECRET_PROFILE=ci
Per-secret providers

Fallback chains, per secret.

Specify an ordered list of providers for each secret. Monosecret walks the chain until it finds the value — perfect for shared team vaults with personal overrides.

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

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, Monosecret makes it state a reason first. How auditing works →

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

# State why — required for agents by default
$ monosecret run --reason "Deploy web frontend" \
    -- ./deploy.sh
 Loaded DATABASE_URL from keyring
 ./deploy.sh
monosecret audit
# Review who accessed what, why, and the outcome
$ monosecret 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 monosecret.toml, so every clone, CI runner, and agent is held to it — humans running interactively are unaffected. Configure the policy →

Type-safe SDK

Compile-time secrets in Rust.

The proc macro reads monosecret.toml at compile time and generates strongly-typed structs. Misspelling a secret name fails the build, not your deploy. SDK reference →

main.rs
// Generate typed structs from monosecret.toml
monosecret_derive::declare_secrets!("monosecret.toml");

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let secrets = Secrets::builder()
        .with_provider("keyring")
        .with_profile(Profile::Production)
        .load()?;

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

    // Optional secrets are Option<String>
    if let Some(redis) = &secrets.secrets.redis_url {
        println!("Redis: {}", redis);
    }

    secrets.secrets.set_as_env_vars();
    Ok(())
}
Migration

Switch backends with one command.

Outgrowing dotenv? Standardizing on Vault? monosecret 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://
$ monosecret import dotenv://.env.production
✓ Imported 5 secrets to keyring://
✓ Application code unchanged
devenv & Nix

Native devenv & Nix integration.

Enable Monosecret in your devenv.yaml and every secret declared in monosecret.toml is loaded into config.monosecret.secrets — wire them into env vars, services, or processes from devenv.nix.

devenv.yaml
monosecret:
  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.monosecret.secrets.DATABASE_URL;
}

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

Stop leaking secrets.

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

Partner spotlight

Fencer for Open Source

In partnership with SecretSpec, Fencer has expanded its security offering to open-source projects. Public repositories get free static analysis, dependency scanning, secret scanning, and GitHub configuration scanning, with no credit card and no expiry.

We recommend Fencer for code security scanning, and Fencer recommends SecretSpec for secrets management.

Learn about the partnership