Repository navigation
Releases
v1.48.0: Touch ID-protected macOS Keychain secrets and streaming proxy responses
The macOS keychain provider can now require Touch ID for each read, and fnox proxy run streams rule-domain responses so SSE and LLM clients get events as they arrive. Secrets can record the URL where a new value is issued. This release also fixes several 1Password problems: secrets lost their trailing newline, and environment:// references failed on CLI 2.42.0.
Added
- Touch ID protection for macOS Keychain secrets (#1140, #1146) by @jdalton and @jdx. Set
touch_id = trueon akeychainprovider:[providers.keychain] type = "keychain" service = "fnox" touch_id = true
- Protected items are stored under a separate
<service>.touch-idKeychain service. Before each read, fnox shows the invoking command, project path and secret name, then asks macOS for Touch ID. Canceling or failing the check blocks access, and there is no password fallback. - fnox asks for Touch ID on every protected read. With
touch_id = true,fnox provider testcreates a temporary protected item and reads it, so it checks real protected access. - The protected Keychain operation runs through a signed
Fnox.apphelper. Official releases publish it as a separatefnox-touch-id-<target>.zipasset. Install it in/Applicationsor next to thefnoxbinary. Without the helper, Touch ID operations fail with an Apple missing-entitlement error. - Source builds on macOS need Node.js 22.18.0 or later to build the approval UI. They also need a Developer ID provisioning profile, and the CLI and helper must be signed with the same team and identifier. Builds made with an SDK older than macOS 26 use an opaque approval panel instead of the glass style. See the keychain docs for helper packaging.
- Protected items are stored under a separate
- Secret URLs and
fnox set --open(#1145) by @jdx. A secret can now include aurlfield for the page where you create a new value, such as a service's token settings page. The URL must usehttporhttps.fnox set KEY --openprints that URL, opens it in your browser, and then shows the usual hidden prompt for the value:fnox set GITHUB_TOKEN --url <URL> # record the URL only fnox set GITHUB_TOKEN --open # open the page, then enter the new value
--openneeds an interactive terminal and can't be combined withVALUE,--from-fileor--generate.fnox listshows aURLcolumn only when at least one secret has a URL, andfnox tuishows the URL in the secret details. - Find 1Password items by tag (#1138) by @jdalton.
fnox provider discoverlists item metadata (ID, title, tags and vault) without reading any secret fields, so you can look up references before adding them to your config. It uses the provider's configured account and vault. If no vault is configured, it searches every vault the account can access. Tags can be comma-separated or passed more than once, and archived items are skipped.fnox provider discover op --tags project/example --json
- Opt-in sudo Touch ID setup script (#1139) by @jdalton.
scripts/enable-sudo-touch-id.shcan check, preview or enable Touch ID forsudoon macOS, using the--check,--dry-runand--applymodes.--jsongives structured output. The script never elevates itself and only creates/etc/pam.d/sudo_localwhen that file doesn't exist. It refuses to change existing policies it doesn't recognize.
Changed
- Streaming proxy responses (#1113) by @jdx.
fnox proxy runno longer waits for a rule domain's full upstream response before forwarding it. It sends the headers right away and then sends the body in chunks as it arrives. Secret values are still redacted, including values split across two chunks. Before this change, streaming APIs such as SSE-based LLM endpoints delivered no events until the upstream finished, and agent clients could time out.- Responses no longer have a 10 MiB limit. Request bodies are still limited to 10 MiB.
- If the upstream fails partway through a response, the client receives an incomplete chunked body instead of a 502.
Fixed
fnox proxy runno longer exits when a client disconnects mid-response (#1112) by @jdx. Previously, the proxy was killed by SIGPIPE (exit 141). The child command then pointed at a closed port and the proxy's CA file was left in the temp directory.proxy runanddaemon servenow drop only the affected connection and keep running. Pipelines such asfnox get FOO | headbehave as before.- 1Password values keep their trailing newline (#1131) by @jdx. Values read from 1Password now match the stored value exactly. This fixes SSH private keys in
[files]that OpenSSH rejected withinvalid format. Batched reads also no longer change multi-line values or drop empty ones. As a result, a stored value that really ends in whitespace now keeps it in environment variables too. Sync cache entries written by earlier versions keep the trimmed value until you refresh them. - 1Password
environment://references work with CLI 2.42.0+ (#1144) by @jdx. fnox now runsop environment secret list. It falls back toop environment readonly when the CLI doesn't know the new command, which is the case for older betas. fnox getandfnox syncresolve provider tokens stored as secrets (#1125) by @jdx. A provider token kept as its own fnox secret, such asBWS_ACCESS_TOKEN, is now resolved forgetandsyncas it already was forexecandexport.syncstill writes only the secrets you selected.- Shell hook reports the actual config error (#1130) by @jdx. When
fnox.tomlhas a schema error,hook-envnow warns with the parse error. Previously it reported a missing.config/fnox.local.toml, or showed no warning at all when that file existed. - Vault leases send
ttlonly whendurationis set (#1129) by @jdx. This fixesunknown field: ttlerrors from strict engines such as the GitHub secrets plugin on OpenBao. If you don't setduration, engines that honorttlnow use their role's default TTL instead of the 15 minutes fnox used to request. Setdurationto keep the old behavior. - Daemon shell hooks pick up
FNOX_AGE_KEYchanges (#1137) by @Aschenbath. Adding, replacing or removing the inline age key in the calling shell now takes effect on the next hook request, without restarting the daemon. fnox sync --local-filewarns before removing stale local cache (#1135) by @jdx. When entries are marked for removal,--dry-runand the confirmation prompt now show a hint. Personal secrets synced by v1.36.0 or older lacklocal_source = trueand look the same as cache copies, so you need to add that flag to keep them. The sync guide now documentslocal_sourceand how to recover those entries.
New Contributors
- @jdalton made their first contribution in #1132
- @Aschenbath made their first contribution in #1137
Full Changelog: v1.47.0...v1.48.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.47.0: Named and passphrase-protected age identities, Azure Container Apps push target
The age provider can now pick one identity per machine from a shared config, read passphrase-protected identity files, and use a named table of recipients. fnox push adds an Azure Container Apps target that writes all of a target's variables in one request. This release also fixes op signin detection for 1Password and fnox sync caching for secrets that use a profile's default_provider.
Added
- Named age identities (#1099, #1100) by @jdx. One shared
fnox.tomlcan list an identity for each machine, for example one per YubiKey. Choose one withdefault_identity, or setFNOX_AGE_IDENTITYto override it on a given machine. An entry can be a key file path or a{ provider, value }reference, such as an age identity unlocked through thefido2provider:[providers.age] type = "age" recipients = ["age1...", "age1...", "age1..."] default_identity = "yk1" [providers.age.identities] yk1 = { provider = "fido2-yk1", value = "..." } yk2 = { provider = "fido2-yk2", value = "..." } recovery = "~/.config/fnox/recovery.txt"
- The selected identity replaces
key_file, oridentityfor provider-backed entries.FNOX_AGE_KEYstill takes precedence over everything. - fnox only calls the provider of the selected entry, so a machine never asks for another machine's key.
- An unknown name is an error that lists the available names. A table that mixes
secretwithprovider/valueis rejected. - Configs that don't set
identities,default_identityorFNOX_AGE_IDENTITYwork as before.
- The selected identity replaces
- Passphrase-encrypted age identity files (#1097) by @jdx.
key_filecan now point at an identity created withage-keygen | age -p -o identity.age, in binary or armored form. When fnox decrypts a secret, it prompts for the passphrase once per file in each run.fnox setonly uses the public recipients, so it never prompts. Without a terminal, fnox fails with an auth error that suggests decrypting the identity and settingFNOX_AGE_KEY. The daemon does not cache the passphrase, and no environment variable can supply it. - Named age recipients (#1104) by @jdx.
recipientscan now be a table, so you can see whose key each entry is:Secrets are encrypted to every key, in the order written. The names are only labels, so renaming one doesn't require[providers.age.recipients] desktop = "age1..." laptop = "age1..." recovery = "age1..."
fnox reencrypt. The array form still works. azure-container-appspush target (#1098) by @jdx.fnox pushcan now set environment variables on an Azure Container App through the ARM REST API. It uses the same credentials asazure-smandazure-ac: service principal environment variables, anaz/azdlogin, or a managed identity. Values never appear on a command line.[targets.api] type = "azure-container-apps" subscription = "00000000-0000-0000-0000-000000000000" resource_group = "rg-dev" app = "ca-api-dev" # container = "api" # required if the app has several containers
- Every env change creates a new revision, so fnox writes all of a target's variables in one request. If every value already matches, it writes nothing.
- Variables that fnox doesn't push, and the app's
secrets, are left alone. A pushed variable is stored as a plainvalueand replaces any existing variable with the same name, including one that used asecretRef. - When Azure provides an
ETag, the write is conditional, so a concurrent change to the app causes the push to fail instead of being overwritten. fnox polls long-running updates for up to five minutes but doesn't wait for the new revision to become ready. Only the public cloud is supported. fnox pushnow groups writes by target, so output lines are ordered by target. If a batch fails, every variable in it is reported as failed.
Fixed
- 1Password: When
opreports "You are not currently signed in", fnox now treats it as an authentication failure. In a terminal, it offers to runop signinand then retries. Otherwise, it shows theop signinhint. Item, vault or field names containing words like "authorization" no longer cause unrelated failures to be reported as sign-in errors. (#1102) by @djgoku - Sync:
fnox syncnow caches secrets that have avaluebut noproviderand are read through the profile'sdefault_provider. Before,--local-fileskipped them, so they kept calling the remote provider and failed offline. The same rule now applies to--source, the "from" labels, and the cleanup of stale cache entries. (#1106) by @jdx
New Contributors
Full Changelog: v1.46.0...v1.47.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.46.0: Push secrets to CI and hosting platforms with fnox push
fnox 1.46.0 adds fnox push, which copies secrets into CI and hosting secret stores such as GitHub Actions, Vercel and Fly. Proxy rules can now inject short-lived lease credentials, [files] entries can be cached locally with fnox sync, and allow_private entries can be limited to one port. This release also fixes a macOS sandbox escape through open.
Added
fnox pushand push targets (experimental) (#1079) by @jdx. CI secrets and hosting environment variables accept a value but never return it, so fnox can't read them as providers. Instead, a secret can now list the targets it should be sent to.fnox pushgets each value through the secret's own provider and writes it to every listed target, using the fnox key as the name.[targets.gh-prod] type = "github-actions" repo = "acme/api" environment = "production" [targets.vercel] type = "vercel" environments = ["production", "preview"] sensitive = true [secrets] STRIPE_KEY = { provider = "op", value = "Stripe/secret key", push = ["gh-prod", "vercel"] }
fnox push --dry-run # preview the writes fnox push STRIPE_KEY -t gh-prod fnox -P production push # use the production profile's targets
- Supported target types:
github-actions(viagh),gitlab-ci(viaglab),vercel,cloudflare-workers(viawrangler),fly,forgejo-actions(Forgejo/Gitea/Codeberg REST API, token fromFORGEJO_TOKEN/GITEA_TOKEN), andhcp-terraform(token fromTF_TOKEN_<host>,TFE_TOKEN, orterraform login). - CLI-based targets receive the value on stdin, not as an argument, and use whatever login that CLI already has.
fnox pushasks for confirmation unless you pass--force.--non-interactiveonly works together with--force. fnox checks every target name and target config before it resolves or writes anything. If one write fails, the remaining writes still run, and the command exits non-zero with a list of the failures.- A profile can define its own targets or override a top-level target that has the same name.
- Pushing keeps a value out of terminals, clipboards and chat transcripts. It doesn't hide the value from anyone who can change code that runs with the secret.
- Supported target types:
- Lease credentials in proxy rules (#1078) by @jdx. A
[[proxy.rules]]entry can now setleaseinstead ofsecret. The proxy then mints a short-lived credential from that lease (for examplegitlab,github-app,vaultorcloudflare) and injects it only into matching requests. The command sees a placeholder and never sees the minted credential or the parent credential it was minted from.[leases.forge] type = "gitlab" # url, project, scopes, duration ... [[proxy.rules]] lease = "forge" domain = "forge.example.com" header = "private-token" methods = ["POST"]
- Each rule sets exactly one of
secretandlease.envdefaults to the variable the lease produces. Leases that produce several variables, andcommandleases, needcredential = "NAME"to pick one.aws-stsleases are rejected. - The proxy mints a lease the first time a matching request needs it, and concurrent requests share that lease. A new lease is minted shortly before the old one expires. Old values stay redacted from responses.
- When the command exits, fnox revokes every lease it minted, if the backend supports revocation. Minted leases are recorded in the lease ledger, so
fnox lease listandfnox lease revokecan clean them up if fnox was killed. - If the parent credential is missing at startup,
fnox proxy runwon't start. If minting fails, that request is refused, and a later request tries again.
- Each rule sets exactly one of
- Sync cache for
[files](#1081) by @jdx.fnox syncnow caches[files]entries as well as secrets. A file stored in a slow remote provider such as 1Password can then be read from a local encrypted copy byfnox get --file,fnox files writeandfnox edit --file. The sync target must be an encryption provider defined in the global config, and the cache is always written to the global config.fnox set --file,fnox edit --fileandfnox reencryptdrop the old cache. Runfnox syncagain whenever the file changes at its source.fnox sync -p sync-age # or: fnox sync ~/.kube/config - Port-specific
allow_privateentries (#1088) by @jdx. Underfnox proxy run --sandbox, you can now give the command access to one local service without opening every port on that address. Entries take the formsip:port,[ipv6]:port, or a CIDR block followed by a port. A bare IP or CIDR still covers every port. Port0, hostnames and malformed entries are rejected when the config loads.[proxy] allow_private = ["127.0.0.1:4790", "[::1]:4790"]
Changed
- Signal handling in
fnox proxy run(#1078) by @jdx. Outside the sandbox,fnox proxy runno longer exits as soon as it gets a signal. It passesSIGTERMandSIGHUPon to the command. It also passes onSIGINT, unless fnox is in the terminal's foreground process group, because Ctrl-C has already reached the command in that case. fnox then waits for the command to exit, so it can shut down the proxy and revoke leases.
Fixed
- macOS sandbox escape through
open(#1090) by @jdx. A command run underfnox proxy run --sandboxon macOS could useopento launch an app through Launch Services. That app ran outside the sandbox, where it could read hidden paths and write outside the working directory (GHSA-38m4-h5j8-8fq6). The sandbox now blocks these launches. Linux is not affected.
Full Changelog: v1.45.0...v1.46.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.45.0: Agents can ask you to enter a missing secret, and fnox proxy run serves MCP tools with placeholders
With fnox 1.45.0, an AI agent can ask you for a missing secret through a one-time local page, so the value never goes through the chat. fnox proxy run can now start before every secret is set, and it serves fnox's MCP tools using placeholders instead of real values. fnox env --describe also reports which file defines each key, and symlinked global configs now work with [files].
Added
request_secretMCP tool (#1065, #1074, #1075) by @jdx. When an agent needs a credential you haven't stored yet, it can ask fnox for it by name. fnox serves a one-time page on127.0.0.1and sends the link to the MCP client through URL-mode elicitation. You type the value on that page and fnox stores it the same wayfnox setdoes. The agent is told only that the secret was stored, andexecinjects it from then on. The tool is off by default:default_provider = "age" [mcp] tools = ["exec", "request_secret"]
- If
mcp.secretsis set, the agent can only ask for names on that list. Theplainprovider is refused. If the secret already has a value, fnox doesn't ask for it again unless the agent passesreplace: true. - The link works once and expires after 10 minutes. The page rejects cross-site form posts. After you save, a reload shows the result for 60 seconds and doesn't store the value a second time.
- If the client doesn't support URL-mode elicitation, no page opens. Instead, the agent is told to ask you to run
fnox set NAMEin your own terminal. If the client rejects or cancels the prompt, or the request times out, the agent gets a message that is different from the one sent when you click Decline. That message also tells the agent not to reopen the prompt or ask for the secret in chat. - The input is a single line, so multi-line values such as PEM keys still need
fnox set --from-file. The page can't prove that you are the one filling it in. Another program running as your user could submit a value.
- If
- MCP tools under
fnox proxy run(#1072) by @jdx. When an agent runsfnox mcpinsidefnox proxy run(including--sandbox), the MCP server now relays to the proxy, and you don't need to change the agent's MCP config.get_secretreturns the secret's placeholder instead of its real value.request_secretopens its page from the proxy process, outside the sandbox. After you enter the value, the proxy starts injecting it into matching requests.execisn't offered in this mode.[mcp] toolsandsecretsstill apply. Only secrets that have a proxy rule can be used this way. Once the child starts,fnox proxy runno longer prompts for provider authentication, so authenticate before you start it. - Pending proxy secrets (#1071) by @jdx. Before,
fnox proxy runexited if any rule's secret had no value. Now, if a secret is declared but not set, fnox prints a warning, starts anyway, and still gives the command a placeholder. Requests that need the secret get a 403 with a "not set yet" message. When you set the secret, for example withfnox set STRIPE_KEYin another terminal, the next matching request picks it up without a restart. The proxy checks for the value at most once every 3 seconds per secret. A rule that names an undeclared secret is still an error.[secrets] STRIPE_KEY = { description = "Stripe test key" } [[proxy.rules]] secret = "STRIPE_KEY" domain = "api.stripe.com"
- Key source in
fnox env --json --describe(#1059) by @jdx. Each key now has asourcefield withpath, the file whose definition wins, andglobal, which is true for the global config and the files it imports. Tools such as mise can use this to tell a project's own keys from your personal global ones. The field is left out when fnox can't trace a key to a file. The describe schema version is still 1. Infnox-client,KeyInfogainssourceand awith_sourcebuilder."source": { "path": "/home/me/.config/fnox/config.toml", "global": true }
Fixed
- Symlinked global config and source files (#1063) by @jdx. fnox 1.44.0 refused
[files]when~/.config/fnox/config.tomlwas a symlink, as dotfiles managers such as stow create. Becausefnox set --filehad already stored the entry, every later command failed. fnox also rejected source files linked into~/.config/fnox/files/. Both now work.fnox setandfnox reencryptwrite through a source link instead of replacing it. They report an error, before storing anything, if the link is broken or points to the config file or to another entry's file. One case is now stricter: a projectfnox.tomlthat is a symlink into the config directory can no longer declare[files].
Full Changelog: v1.44.0...v1.45.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.44.0: Store whole files by path and edit a single secret
fnox 1.44.0 adds secret files: whole files such as ~/.kube/config stored in the global config under the path they belong at, and written there with fnox files write. fnox edit can now edit one secret, accepts editor commands that include arguments, and saves the change to the config file that defines the secret.
Added
- Secret files (#1056) by @jdx and @owpac. Some secrets need to exist as files at a fixed path, such as
~/.kube/config,~/.aws/credentialsor~/.npmrc. You can now declare them in a[files]table, using the path as the key. Files use the same providers, profiles andfnox reencryptas secrets, but fnox never turns them into environment variables.# ~/.config/fnox/config.toml [files] "~/.kube/config" = { provider = "age", value = "YWdl..." } [profiles.work.files] "~/.aws/credentials" = { provider = "age", source = "files/aws-credentials.age" }
fnox set --file ~/.kube/config # store the file at that path fnox get --file ~/.kube/config # print it, byte for byte fnox edit --file ~/.kube/config # edit it in $EDITOR fnox remove --file ~/.kube/config # remove the entry fnox files # list entries without decrypting them fnox files write # write every file of the active profiles
[files]is only allowed in the global config. If a projectfnox.tomldeclares it, fnox reports an error, so a cloned repository can't make fnox overwrite paths such as~/.ssh/authorized_keys.- A key must be an absolute path or start with
~/. Content is kept byte for byte, including the final newline, and must be UTF-8 text. - You can keep the encrypted content in a separate file with
sourceinstead ofvalue. The path is relative to the config directory and can't point outside it. Theplainprovider can't be used for files. fnox files writeresolves every file before it writes any of them. It writes each file with mode0600and replaces a symlink at the path instead of writing through it. Files whose content already matches are left unchanged.--dry-runlists what would be written, and path arguments limit the write to those files.- Files are written only when you run
fnox files write.fnox execand shell integration don't write them.
- Edit a single secret (#1045) by @jdx.
fnox edit KEYopens only that secret's resolved value in your editor, as raw text in a private temporary file. No other secrets are decrypted. When you save, fnox stores the new value with the secret's provider and keepsenv,descriptionand its other settings. If the only change is a final newline added by the editor, fnox ignores it. Secrets from read-only providers can't be edited this way. Runningfnox editwith no key works the same as before.fnox -P work edit KUBECONFIG
Fixed
- Editor commands with arguments (#1045) by @jdx. Setting
EDITOR="code --wait"orEDITOR="vim -n"used to fail withFailed to launch editor. fnox now splits the value using shell quoting rules. IfEDITORis empty, fnox usesVISUAL, thenvi(notepadon Windows). fnox now also reports an error when the editor exits with a nonzero status. fnox edit KEYsaving to the wrong file (#1048) by @jdx. Editing a secret defined in the global config or a parent project used to create a new./fnox.tomlin the current directory. The original definition kept the old value. The new value is now written to the file that defines the secret, encrypted with that file's providers. If afnox sync --local-filecache holds the secret, fnox updates the shared definition and removes the outdated cache entry.fnox seton synced and inherited secrets (#1045) by @jdx. Setting a new value now clears the secret'sfnox synccache, so an outdated cached value no longer takes precedence. Setting a value for a secret inherited from another profile or config layer now keeps itsas_file,env,descriptionand other settings.
Full Changelog: v1.43.0...v1.44.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.43.0: Proxy sandbox and egress hardening, exec output redaction, and new lease backends
fnox 1.43.0 makes fnox proxy run harder to misuse: it now has an OS sandbox, a per-session password and blocking of private network destinations. It also adds output redaction and secret filtering to fnox exec, four new lease backends, a local audit log, value-based secret scanning, generated secrets and KeePass field references. Some proxy setups need a config change; see Breaking Changes.
Highlights
- Credential proxy: the proxy can now confine the command it runs, refuses unauthenticated clients and private network destinations, and can inject secrets into query parameters on wildcard domains.
- Keeping secrets out of logs and repos:
fnox execcan redact secret values from piped output,fnox scancan search for your actual secret values (including in a pre-commit hook), and an opt-in audit log records which secrets were read, without recording their values. - Leases: new OAuth2, GitLab, Tailscale and Kubernetes backends, plus easier revocation and JSON listing.
Added
Proxy
fnox proxy run --sandbox(#1039) by @jdx. Runs the command in an OS sandbox where the proxy is the only network access. Inside the sandbox, the command can't read fnox config, state and data dirs,fnox.toml/profile files and their imports, age key files, or common credential dirs (~/.ssh,~/.aws,~/.kube,~/.gnupg, ...). On Linux it also can't reach local credential sockets. It can write only to the working directory and temp files, and it can't create or change.git/hooks,.git/configor fnox config files. The rest of the filesystem stays readable: fnox hides only the paths on its list.On Linux this requires[proxy] sandbox = true # project default; --no-sandbox overrides it for one run sandbox_allow_write = ["~/.cache"] sandbox_deny_read = ["~/.config/x"] sandbox_allow_read = ["fnox.toml"]
bwrap; on macOS fnox usessandbox-exec. Windows isn't supported. If the sandbox can't start, fnox stops before resolving any secrets and never runs the command unsandboxed. The macOS path hasn't been tested on real hardware yet. On macOS the sandbox can't hide other processes or the Keychain service.- Query-parameter injection and wildcard domains (#1026) by @jdx. A proxy rule can set
query = "<name>"instead ofheaderto substitute a secret into a URL query parameter. The value is percent-encoded, and fnox refuses the request (403) if the placeholder appears anywhere else or more than once.domainaccepts a leading*.that matches exactly one subdomain level. The apex domain and deeper subdomains don't match, and patterns like*.comare rejected.headeris now optional and still defaults toauthorization.[[proxy.rules]] secret = "MAPS_KEY" domain = "*.maps.example.com" query = "key" methods = ["GET"]
allow_domainsandallow_private(#1038) by @jdx.allow_domainslets the proxy tunnel the hosts you list without inspection or credentials while egress stays strict.allow_privatelists the internal networks the proxy may reach (see Breaking Changes). The proxy audit output now also logsrefused requestlines with the destination and the reason.
Exec
- Redact secrets from piped output (#1028) by @jdx. With
fnox exec --redact, secret values in the command's stdout and stderr are replaced with[REDACTED], but only for streams that are piped or redirected; terminal output is left alone. Redaction is off by default. To turn it on everywhere, setredact_exec = trueinfnox.tomlorFNOX_EXEC_REDACT=1.--no-redactturns it off for one run, and the flag takes precedence over the environment variable, which takes precedence over config. Values shorter than 3 characters aren't redacted, and you can't combine--redactwith--replace. Redaction matches exact values only, so it won't catch a secret that has been encoded or split before printing.fnox exec --redact -- sh -c 'echo "token=$API_TOKEN"' | cat # token=[REDACTED]
--onlyand--except(#1030) by @jdx. These flags limitfnox execto the secrets you select. Unselected secrets are never resolved, so their providers aren't called and can't prompt for login or fail the run. Patterns are exact names or case-sensitive globs, and you can repeat the flag or separate patterns with commas. fnox still resolves the secrets a selected secret depends on, but passes them to the command only if they're selected too. Leases follow the same filter. An unknown name, or an--onlyglob that matches nothing, is an error.fnox exec --only DATABASE_URL,'AWS_*' -- ./migrate.sh fnox exec --except LEGACY_API_KEY -- npm start
Leases
-
OAuth2, GitLab, Tailscale and Kubernetes backends (#1037) by @jdx:
oauth2-client-credentials: access tokens from any OAuth2 token endpoint. They're revoked only if you setrevocation_url.gitlab: project or group access tokens on gitlab.com or self-hosted GitLab, deleted on revoke. GitLab expiry is date-only, so a token can stay valid for up to a day after its lease ends unless you revoke it.tailscale: tagged, ephemeral, pre-authorized auth keys. With the defaultreusable = false, every command gets a new key and fnox doesn't cache it.kubernetes: ServiceAccount tokens from the TokenRequest API, without needingkubectl. These can't be revoked and expire on their own. The minimum duration is 10 minutes.
Credential URLs must use https; plain http is accepted only for localhost.
fnox lease revokeandfnox lease cleanupnow load the parent credentials a backend needs to revoke a lease, so revocation also works when the parent token is stored in[secrets].[leases.gl] type = "gitlab" project = "platform/api" scopes = ["read_api"] duration = "1h"
-
Easier lease revocation and JSON listing (#1036) by @jdx.
fnox lease revokeaccepts a full lease ID, a unique ID prefix (including the shortened ID thatlease listprints) or a backend name, which revokes all of that backend's active leases.--allrevokes every active lease in the project. If a prefix matches more than one lease, fnox lists the matches and revokes nothing.fnox lease listgains aLABELcolumn and a--jsonflag that prints full IDs and timestamps but never cached credentials.fnox lease revoke aws-1a2b3c fnox lease revoke aws fnox lease revoke --all fnox lease list --json --active
Other additions
- Local access audit log (#1031) by @jdx. The log is opt-in. Each time a secret is read, fnox appends a JSON line to a
0600file with the time, command, profile, config file, secret name, provider and whether the value came from the daemon cache. Values and command lines are never recorded. The log coversexec,get,export,env --json,list --values,check, shell integration,mcp,proxy runand the TUI. It doesn't coverlease,sync,editorreencrypt. Usefnox auditto view entries, with--secret,--command,--provider,-n/--limit,--jsonand--path.FNOX_AUDIT=on|offandFNOX_AUDIT_PATHoverride the config. Any process running as your user can edit the log, so it isn't tamper-proof. Set[audit]in your global config so a projectfnox.tomlcan't choose the log path.# ~/.config/fnox/config.toml [audit] enabled = true
- Find real secret values with
fnox scan(#1029) by @jdx.fnox scan --valuesresolves the active profile and searches for the actual secret values instead of text that merely looks like a secret, so every match is a real leak. Output names the secret but never prints its value.--stagedscans what's staged in the git index.--install-hookadds a pre-commit hook that runsfnox scan --values --staged. It won't overwrite an existing hook unless you pass--append. fnox skips secrets shorter than 8 characters, binary files, files over 5 MiB and gitignored files (unless you pass--include-ignored). Iffnox.tomlitself contains plain values, it will match; use--ignore fnox.tomlif that's intended. - Generate secret values with
fnox set --generate(#1032) by @jdx. fnox creates the value with the OS random number generator and stores it through the selected provider.--generate-typecan bepassword(the default),hex,base64(URL-safe) oruuid.--lengthdefaults to 32 and accepts 1 to 4096. fnox doesn't print the value unless you pass--show, and it won't replace an existing secret without--force.fnox set API_TOKEN --generate fnox set SESSION_KEY --generate --generate-type hex --length 64
- KeePass field references and custom fields (#1022, #1025) by @jdx. The
keepassprovider now resolves{REF:...}references such as{REF:P@I:<uuid>}instead of returning the raw text. References can be embedded in longer strings and nested up to 10 levels deep. A path such aswork/github/ApiTokencan now read a custom field (ApiToken) of an entry (work/github). If an entry is titled with the full path, that entry takes precedence. Custom field names are case-sensitive.fnox setdoesn't write custom fields.
Changed
v1.42.1: Shell hooks survive mise upgrades and work with starship in PowerShell
fnox 1.42.1 fixes two shell-integration problems: hooks that broke after a mise upgrade, and PowerShell hooks that didn't work correctly alongside starship.
Fixed
- Shell hooks breaking after a mise upgrade (#1016) by @jdx.
fnox activateused to put the fully resolved binary path, such as.../installs/fnox/1.42.0/fnox, into the shell hook. Whenmise upgradedeleted that directory, every prompt printed_fnox_hook: no such file or directory, andfnox deactivatefailed too. These changes apply:- fnox now writes the path you ran it from into the hook, keeping symlinks. To get hooks that keep working after upgrades, activate through a stable path:
A plain
eval "$(~/.local/share/mise/installs/fnox/latest/fnox activate zsh)"
eval "$(fnox activate zsh)"that finds fnox through a versionedPATHentry still writes that versioned path into the hook. - In bash, zsh and fish,
fnox deactivatenow removes the hooks and thefnoxfunction even if the binary is gone. To recover, runfnox deactivateand then activate again. When you activate again, fnox still unsets the secrets that were loaded before. - Binary paths in the generated bash, zsh and fish hooks are now quoted, so paths containing spaces work.
- Nushell and PowerShell hooks still use the old behavior.
- fnox now writes the path you ran it from into the hook, keeping symlinks. To get hooks that keep working after upgrades, activate through a stable path:
- PowerShell hooks with starship (#1020) by @jdx.
fnox activate pwshnow works whether it's loaded before or after starship:- If fnox loaded first, starship replaced the prompt function and
cdstopped loading and unloading secrets. The hook now also runs when the current location changes. It passes the event on to any existing handler, andfnox deactivateputs the previous handler back. - If starship loaded first, the prompt showed failed commands as successful. The hook now keeps
$?and$LASTEXITCODEintact. - Each change summary (for example
fnox: +1 KEY) is now printed once instead of twice.
- If fnox loaded first, starship replaced the prompt function and
Full Changelog: v1.42.0...v1.42.1
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.42.0: Export selected secrets by name, managed identity for azure-token leases
In fnox 1.42.0, fnox export can export only the secrets you name, and the azure-token lease can authenticate with an Azure managed identity. This release also fixes the AWS SSO login prompt for templated profiles, stops fnox sync --local-file from deleting personal secrets, and updates the bundled TLS root certificates after the eMudhra CA compromise.
Added
- Export only named secrets (#1002) by @jdx.
fnox exportnow takes optionalKEYarguments. When you pass keys, fnox resolves and exports only those secrets, so it doesn't need to authenticate to providers for the rest of the profile. It still resolves the secrets they depend on for interpolation, but doesn't export them. A named key is exported whatever itsenvsetting, so you don't need--all. An unknown key fails with the same "did you mean" suggestions asfnox get. Format,--output,--dry-runandas_filework the same as before, and runningfnox exportwithout keys behaves as it did.fnox export API_KEY fnox export --format json DATABASE_URL API_KEY
- Managed identity for the
azure-tokenlease (#1003) by @jdx. The lease now accepts the sameauthandmanaged_identity_client_idsettings asazure-sm,azure-kmsandazure-ac. CI jobs on Azure-hosted runners no longer need to install the Azure CLI and runaz login --identityjust for the lease. You have to turn managed identity on withauth = "managed-identity"; fnox never tries it on its own. Without it, fnox still uses service principal environment variables and thenaz/azd. In managed identity mode, fnox doesn't check forazonPATH. Settingmanaged_identity_client_idwithoutauth = "managed-identity", or using an unknownauthvalue, is a config error.[leases.azure-openai-access-token] type = "azure-token" scope = "https://ai.azure.com/.default" env_var = "AI_ACCESS_TOKEN" auth = "managed-identity" managed_identity_client_id = "00000000-0000-0000-0000-000000000000" # omit for system-assigned
Fixed
- AWS SSO login prompt with templated or secret-based profiles (#998) by @balintant. When
profilewas a template such as"{{ fnox.profile }}", theaws-kms,aws-psandaws-smproviders offered to runaws sso login --profile '{{ fnox.profile }}', and that command failed. The prompt now uses the provider's resolved profile, for exampleaws sso login --profile production. A profile set withprofile = { secret = "..." }also gets its resolved name now; before, fnox ran a bareaws sso login. Anauth_commandyou configure still takes precedence. fnox sync --local-filedeleting personal secrets (#1004) by @jdx. When you ran the sync a second time, fnox mistook secrets defined only infnox.local.tomlfor cached copies of shared secrets and deleted their source references. Synced entries for these secrets now recordlocal_source = truein theirsyncblock. fnox keeps and refreshes those entries on every sync. Cached copies of sharedfnox.tomlsecrets are still removed when the shared secret is deleted. Existing files work without changes. If this bug already removed one of your secrets, add it back tofnox.local.tomlonce and it will be kept from then on.- Revoked eMudhra intermediate certificates (#1005) by @jdx. The private keys of eight eMudhra intermediate CA certificates were compromised. fnox now uses
rustls-webpki0.103.16 andwebpki-root-certs/webpki-roots1.0.10, which stop trusting those certificates for HTTPS connections.
Full Changelog: v1.41.0...v1.42.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.41.0: gopass provider and profile-aware config templates
fnox 1.41.0 adds a gopass provider and lets secret defaults and provider fields use the active profile through Tera templates. fnox exec no longer decrypts env = false secrets that nothing uses, and the AWS SSO login prompt now logs in to the profile your provider is configured with.
Added
- gopass provider (#980) by @jdx. Teams that already keep secrets in gopass can now read them through fnox. fnox reads values with
gopass cat, writes them withgopass insert -f(fnox set --provider gopass), and tests the connection withgopass ls --flat. The optionalmountsetting is added to the front of every secret path. fnox removes one trailing newline from each value and leaves other whitespace alone. Errors tell you whether gopass is missing, the GPG key is locked or unavailable, or the secret path doesn't exist. You can also add the provider withfnox provider add <name> gopass.[providers] gopass = { type = "gopass", mount = "company" } # mount is optional [secrets] # read with: gopass cat company/apps/myapp/database_url DATABASE_URL = { provider = "gopass", value = "apps/myapp/database_url" }
- Profile-aware templates in config (#993) by @jdx. A secret
defaultand literal string provider fields are now rendered as Tera templates if they contain{{,{%or{#. Two variables are available:fnox.profile(the selected profile with the highest priority, ordefaultif none is selected) andfnox.profiles(all selected profiles, lowest priority first). This means you can write values that follow the profile, such as AWS profiles or SST stages, once instead of repeating them for every profile.FNOX_PROFILEis still not exported to child processes.[providers.aws] type = "aws-ps" region = "eu-central-1" profile = "{{ fnox.profile }}" [secrets] SST_STAGE = { default = "{{ fnox.profile }}" }
- Templates are rendered before
${NAME}interpolation. A default is only rendered when it's actually used, and only the${NAME}references left after rendering must exist. So a branch of an{% if %}that isn't taken requires nothing. - An undefined variable or a syntax error fails with a message that names the field. To keep a marker as literal text, wrap it in
{% raw %}...{% endraw %}. - These fields are not templated:
value,description, list fields such as agerecipients, andcredential_command, which keeps its own template context. - Check existing configs: a default or provider field that already contains
{{,{%or{#as plain text will now be treated as a template. Wrap it in{% raw %}to keep the old value.
- Templates are rendered before
- More detailed
--help(#990) by @jdx. Every public command now shows a description, examples and links to the relevant guides when you runfnox <command> --help. This is the same content as the CLI reference at fnox.jdx.dev/cli. Flags and behavior are unchanged.
Fixed
fnox execno longer resolves unusedenv = falsesecrets (#984) by @andersonkrs. Previously,fnox execresolved every secret in the profile and then removed theenv = falseones from the child's environment. With a PIN-protected age identity, each extra decrypt asked for the PIN again.fnox execnow selects secrets the same way asfnox env --json. It resolves only the secrets it injects plus what they depend on:${NAME}references in defaults, provider secret references such astoken = { secret = ... }, and the inputs of selected leases. If acommandlease is selected, it still resolves the whole profile. Theaws-stslease's list of inputs now also includes the environment variables for web identity, container credentials and EC2 instance metadata, so those can still be supplied asenv = falsesecrets. The child's environment is the same as before.- AWS SSO login prompt uses the provider's profile (#991) by @balintant. When authentication fails for an
aws-ps,aws-smoraws-kmsprovider that setsprofile, the default prompt now runsaws sso login --profile <name>instead of a bareaws sso login. Before, the bare command logged in to thedefaultprofile and failed if your AWS config had none. The profile is only added when it's a literal value and noauth_commandis set. On Windows, it's only added when the name uses ASCII letters, digits,-,_and..
Documentation
- The guides, landing page, README, provider pages, lease pages and configuration and environment variable references were reorganized and rewritten (#987, #988, #990) by @jdx. This fixes some wrong statements, for example: an earlier import takes precedence over a later one, and imports are not recursive. The sync docs now cover TPM PIN protection and hardware-backed service tokens (#986) by @andersonkrs.
New Contributors
- @balintant made their first contribution in #991
Full Changelog: v1.40.0...v1.41.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.
v1.40.0: Lua plugin providers, Kubernetes Secrets, and Windows daemon support
fnox 1.40.0 adds custom providers written as Lua plugins, a read-only Kubernetes Secrets provider, 1Password Environments, file-backed plain providers and Bitwarden folder scoping. The daemon now runs on Windows. This release also fixes Azure service principal authentication, repeated hardware PIN prompts for age identities, and fnox set dropping piped values.
Added
- Custom providers as Lua plugins (#968) by @jdx. You can now write a secrets backend that fnox doesn't support as a Lua 5.4 plugin and share it as a git repository. Declare the plugin under
[plugins], then use it withtype = "lua":[plugins] corp = "acme/fnox-plugin-corp#v1.2.0" # git URL or owner/repo, optional #ref [providers.corp] type = "lua" plugin = "corp" # or a path such as ./plugins/corp.lua [secrets] DATABASE_URL = { provider = "corp", value = "prod/database-url" }
- A plugin is a
provider.luathat returns hooks.getis required.get_batch,put,encryptandtestare optional, and the hooks you define decide whetherfnox setandfnox importcan write to the provider. A provider'soptionstable (string values, which can be secret references) is passed to the plugin asctx.options. Plugins can use thefnox.http,fnox.json,fnox.cmd,fnox.logandfnox.base64modules.fnox.auth_error(msg)triggers the provider'sauth_commandflow. - New commands:
fnox plugin install,update,link,lsanduninstall, plusfnox provider add NAME lua --plugin .... - fnox installs a declared plugin the first time a provider uses it (
git clone). Plugins go in$FNOX_DATA_DIR/plugins, which defaults to$XDG_DATA_HOME/fnoxor~/.local/share/fnox. SetFNOX_PLUGINS_DIRto use a different location. If a project declares a different source or ref than the installed one, fnox won't load the plugin until you runfnox plugin update <name>. - Plugins run with your user's permissions. Only install plugins from sources you trust, and pin a ref. See
docs/providers/lua.mdfor details.
- A plugin is a
- Kubernetes Secrets provider (#953) by @jdx. A read-only
kubernetesprovider reads existing Secrets through the Kubernetes API. It only makes namedGETrequests and never lists, watches or writes Secrets. A reference can besecret,secret/keyornamespace/secret/key. The optionalcontext,namespace,kubeconfigandprefixsettings control where it looks, and theFNOX_K8S_CONTEXTenvironment variable can also select the context. Daemon caching is always disabled for this provider, even withdaemon_cache = true.[providers.kubernetes] type = "kubernetes" context = "production" namespace = "payments" [secrets] DATABASE_URL = { provider = "kubernetes", value = "database/url" }
- 1Password Environments (#955) by @jdx. The
1passwordprovider can read variables from a 1Password Environment usingenvironment://<environment-id>/<VARIABLE>references. You can mix these with vault item references in the same config. fnox reads each Environment once per provider instance, and a variable missing from the Environment is an error. This needs the betaopCLI with theop environmentcommand. - Read values from a plaintext file with the
plainprovider (#954) by @joshbode. The newfileandformatsettings make each secret'svaluea key to look up in that file.formatcan beenv,shell,json,yamlortoml, and you must set it wheneverfileis set. A relative path resolves against the config file that declares the provider. The file is read-only, and a missing key counts as a missing secret, sodefaultandif_missingstill apply.[providers.dotenv] type = "plain" file = "secrets.env" format = "env" [secrets] API_TOKEN = { provider = "dotenv", value = "SERVICE_TOKEN" }
- Bitwarden folder scoping (#965) by @andersonkrs. The new optional
foldersetting limits lookups to one Bitwarden folder. For nested folders, use the full path, such asfolder = "MyApp/Production". This lets items with the same name exist in different folders. It works with bothbwandrbw. - Azure managed identity (#964) by @jdx.
azure-sm,azure-kmsandazure-acacceptauth = "managed-identity", with an optionalmanaged_identity_client_idfor a user-assigned identity. fnox never tries managed identity on its own; you have to setauth. - Daemon on Windows (#974) by @jdx. On Windows, the daemon now uses a per-user named pipe where other platforms use a Unix socket, so Windows gets cached secrets too. Only the current user can access the pipe, and both the client and the daemon check the user on the other end. The wire protocol and the
fnox-clientAPI are unchanged.
Fixed
- Azure service principal environment variables (#964) by @jdx. Azure providers authenticate as a service principal again when
AZURE_TENANT_ID,AZURE_CLIENT_IDandAZURE_CLIENT_SECRETare all set. Without them, fnox falls back to theaz/azdlogin. For sovereign clouds, setAZURE_AUTHORITY_HOST. These variables can also be fnox secrets resolved byfnox exec. - Hardware PIN prompts for age identities (#978) by @andersonkrs and @jdx. With PIN-protected age identities (
age-plugin-tpm --pin,age-plugin-yubikey), a value used by several providers made one command ask for the PIN up to three times. Now a successful decrypt is reused for the rest of the request, so you get one prompt perfnox get,execorexport, or per daemon request. Nothing is kept between requests, and a failed decrypt can be retried. fnox setwith piped stdin and metadata flags (#976) by @jdx.echo val | fnox set A -d "desc"(or with--defaultor--if-missing) said it worked but saved no value. fnox now stores the piped value along with the metadata. If stdin is empty, only the metadata is updated, as before.
Changed
fnox importparsing (#954) by @joshbode.fnox importnow accepts the{secrets, metadata}document thatfnox export --format json|yaml|tomlproduces. Theenvparser now follows Docker Compose dotenv rules for inline comments. For example,PORT=8080 # defaultnow imports as8080, where it used to keep the comment as part of the value.#without whitespace before it, as inID=val#ue, and#inside quotes are still part of the value.
New Contributors
- @joshbode made their first contribution in #954
- @andersonkrs made their first contribution in #965
Full Changelog: v1.39.0...v1.40.0
💚 Sponsor fnox
fnox is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If fnox handles secrets or config for you or your team, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep fnox secure, free, and independent.