Skip to content

Syncing Secrets Locally

fnox sync fetches secrets from remote providers (1Password, AWS Secrets Manager, etc.) and re-encrypts them with a local encryption provider (age, YubiKey via age plugin, AWS KMS, etc.). The encrypted values are stored in fnox.local.toml (gitignored) so that subsequent access is instant and offline — no remote calls needed.

Why Sync?

A typical team setup stores secrets in a shared provider like 1Password:

toml
# fnox.toml (committed)
[providers.op]
type = "1password"
vault = "Engineering"

[secrets]
DATABASE_URL = { provider = "op", value = "Database/url" }
STRIPE_KEY = { provider = "op", value = "Stripe/secret-key" }
SENDGRID_KEY = { provider = "op", value = "SendGrid/api-key" }

This works, but every time you cd into the project (with shell integration), fnox calls 1Password to fetch each secret. This is slow and requires network access.

With fnox sync, you pull those values once and cache them locally with a fast, offline encryption provider:

bash
fnox sync --provider age --config fnox.local.toml

Now entering the directory is instant — secrets are decrypted locally from age without any remote calls.

How It Works

  1. fnox reads all secrets from your merged config
  2. It resolves each secret's plaintext value from the original remote provider
  3. It encrypts each value with the target provider (e.g., age)
  4. It writes the encrypted cache into your config as a sync field on each secret

When fnox resolves secrets, it checks for a sync field first and uses that instead of calling the original provider.

Basic Usage

bash
# Set up an age provider if you haven't already
fnox set --provider age  # or add [providers.age] to your config

# Sync everything to fnox.local.toml
fnox sync --provider age --config fnox.local.toml

Preview what would be synced

bash
fnox sync --provider age --config fnox.local.toml --dry-run

Sync specific secrets

bash
fnox sync --provider age --config fnox.local.toml DATABASE_URL STRIPE_KEY

Sync only secrets from a specific source

bash
fnox sync --provider age --config fnox.local.toml --source op

Filter by regex pattern

bash
fnox sync --provider age --config fnox.local.toml --filter "^DB_"

What It Looks Like

After syncing, your files look like this:

fnox.toml (committed — the source of truth):

toml
[providers.op]
type = "1password"
vault = "Engineering"

[providers.age]
type = "age"
recipients = ["age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"]

[secrets]
DATABASE_URL = { provider = "op", value = "Database/url" }
STRIPE_KEY = { provider = "op", value = "Stripe/secret-key" }
SENDGRID_KEY = { provider = "op", value = "SendGrid/api-key" }

fnox.local.toml (gitignored — your local cache):

toml
[secrets]
DATABASE_URL = { provider = "op", value = "Database/url", sync = { provider = "age", value = "YWdlLWVuY3J5cHRpb24..." } }
STRIPE_KEY = { provider = "op", value = "Stripe/secret-key", sync = { provider = "age", value = "YWdlLWVuY3J5cHRpb24..." } }
SENDGRID_KEY = { provider = "op", value = "SendGrid/api-key", sync = { provider = "age", value = "YWdlLWVuY3J5cHRpb24..." } }

When you cd into the project, fnox sees the sync field and decrypts with age locally — no 1Password calls.

Using a YubiKey

If you use a YubiKey with the age-plugin-yubikey, syncing works the same way. Your age provider just uses the YubiKey identity:

toml
[providers.age]
type = "age"
recipients = ["age1yubikey1q..."]  # YubiKey recipient
bash
fnox sync --provider age --config fnox.local.toml

Secrets are encrypted to your YubiKey's age identity. Decryption requires the YubiKey to be plugged in, adding hardware-based security to your local cache.

Refreshing the Cache

When secrets change in the remote provider, re-run sync to update the local cache:

bash
fnox sync --provider age --config fnox.local.toml --force

The --force flag skips the confirmation prompt. fnox re-fetches from the original provider and re-encrypts.

Full Workflow Example

bash
# 1. Clone a project with 1Password secrets in fnox.toml
git clone https://github.com/myorg/my-api && cd my-api

# 2. Make sure fnox.local.toml is gitignored
echo "fnox.local.toml" >> .gitignore

# 3. Set up your age key (one-time) — note the public key printed to your terminal
age-keygen -o ~/.config/fnox/age.txt
export FNOX_AGE_KEY=$(grep "AGE-SECRET-KEY" ~/.config/fnox/age.txt)

# 4. Add age provider to your config, replacing the recipient with your public key from step 3
cat >> fnox.toml << EOF
[providers.age]
type = "age"
recipients = ["$(grep 'public key:' ~/.config/fnox/age.txt | awk '{print $NF}')"]
EOF

# 5. Sync all 1Password secrets to local age encryption
fnox sync --provider age --config fnox.local.toml

# 6. Done — entering the directory is now instant
cd .. && cd my-api
# Secrets load from local age cache, no 1Password calls

Next Steps

Released under the MIT License.