Skip to content

Commit a0019b2

Browse files
docs(adr): adopt fnox for secrets (ADR 32)
Primary provider is macOS Keychain to avoid the Touch ID lifecycle pain of op:// resolution for always-set env vars. Establishes the convention of naming fnox providers by scope (not by type) to prevent silent shadowing across nested fnox.toml files. Drops unused artifacts: - config/fish/conf.d/secrets.fish: op:// literals that nothing resolved - fnox.toml: empty provider block violating the new convention Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8b2700f commit a0019b2

4 files changed

Lines changed: 90 additions & 8 deletions

File tree

config/fish/conf.d/secrets.fish

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# 32. Use fnox for Secrets
2+
3+
Date: 2026-04-24
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
Environment variables that hold secrets (API keys, tokens) had been a loose mess. The previous approach lived in `config/fish/conf.d/secrets.fish` and looked like this:
12+
13+
```fish
14+
set -gx OPENAI_API_KEY "op://Personal/OpenAI chatblade/credential"
15+
set -gx WPSCAN_API_TOKEN "op://Personal/Wpscan/Token/Token"
16+
```
17+
18+
The problem: those aren't resolved values, they're literal strings. Only tools that natively understand `op://` syntax (or commands wrapped in `op run`) can do anything useful with them. For everything else, `$OPENAI_API_KEY` was just the text `op://Personal/OpenAI...`.
19+
20+
The obvious fix is to wrap shells in `op run` or have direnv call `op read`, but the 1Password CLI has a lifecycle problem on macOS: every resolution wants Touch ID. That's fine for one-shot CI runs or scripted secret retrieval. It's painful for env vars that get re-read constantly by shell hooks and child processes.
21+
22+
What's needed is something that can fetch a secret once, cache it at the OS level, and hand it to the shell without prompting every time.
23+
24+
## Decision
25+
26+
Adopt [fnox](https://fnox.jdx.dev/) for shell-scoped secrets, with macOS Keychain as the default provider.
27+
28+
Keychain is the right primary because once a secret is stored and the keychain is unlocked (which happens at login), reads are free. No Touch ID, no menu-bar prompts, no lifecycle pain.
29+
30+
fnox also supports 1Password natively, so when a secret legitimately needs to live in 1Password (shared vault, rotation policy, etc.) it can reference `op://` paths through fnox instead of through raw env strings. That's available, just not the default.
31+
32+
### Convention: Unique provider names across nested configs
33+
34+
fnox walks up from the current directory and merges every `fnox.toml` it finds. Inner configs override outer configs at the key level, which means if two `fnox.toml` files both declare `[providers.keychain]`, the inner one wins silently.
35+
36+
This bit us on 2026-04-24. `~/pickleton/fnox.toml` had `[providers.keychain]` with `service = "pickleton"`, and a nested `fnox.toml` declared the same provider name with `service = "dotfiles"`. From inside the nested directory, `RUNLAYER_API_KEY` resolution failed because fnox was now looking it up in the wrong keychain service. Warning logged, env var unset, confusing afternoon.
37+
38+
Rule: name providers by their scope, not their type. `[providers.pickleton]` and `[providers.dotfiles]`, not `[providers.keychain]` in every config.
39+
40+
### Scope
41+
42+
fnox configs currently live per-project (one at `~/pickleton/fnox.toml`). `RUNLAYER_API_KEY` is the first secret managed this way. A global config for truly personal secrets is a reasonable future addition, but hasn't been set up yet.
43+
44+
### Alternatives Considered
45+
46+
1. **`op run --env-file=...`**
47+
- Pros: Resolves op:// paths at command-launch time
48+
- Cons: Touch ID per run, doesn't fit the "env var that's always set" shape
49+
- Rejected: the prompting cadence
50+
51+
2. **direnv + `op read`**
52+
- Pros: Per-directory env, good ecosystem
53+
- Cons: Same Touch ID problem, plus direnv reloads are frequent
54+
- Rejected: same reason
55+
56+
3. **Raw `op://` strings in fish (the old way)**
57+
- Pros: No moving parts
58+
- Cons: Nothing actually resolves them; tools see literal strings
59+
- Rejected: this is what got us here
60+
61+
4. **sops + age for git-committed encrypted secrets**
62+
- Pros: Version-controlled, reviewable
63+
- Cons: Wrong shape for personal env vars; designed for team-shared config that lives in repos
64+
- Rejected: different use case, might still make sense elsewhere
65+
66+
## Consequences
67+
68+
### Positive
69+
70+
- Secrets resolve to real values in shells, no prompting in the hot path
71+
- Keychain unlock state handles auth once per login session
72+
- Provider is swappable; 1Password stays available for secrets that belong there
73+
- Shell integration reacts to `cd`, so per-project secrets come and go with the directory
74+
75+
### Negative
76+
77+
- First-time keychain reads may still prompt once (acceptable)
78+
- Provider-naming collisions are a silent footgun until you know to watch for them
79+
- No global config for personal secrets yet, so anything truly global has to wait or live per-project
80+
- Another tool in the chain; if fnox's shell hook breaks, env vars go with it
81+
82+
### Migration
83+
84+
`config/fish/conf.d/secrets.fish` was removed as part of this decision. It held three `op://` literals (`OPENAI_API_KEY`, `WPSCAN_API_TOKEN`, `NPM_TOKEN`) that weren't actually in use.
85+
86+
## Links
87+
88+
- [fnox docs](https://fnox.jdx.dev/)
89+
- [fnox 1Password provider](https://fnox.jdx.dev/providers/1password)

doc/adr/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@
3131
- [29. tmux-local-config-overrides](0029-tmux-local-config-overrides.md)
3232
- [30. ssh-keychain-loading-at-login](0030-ssh-keychain-loading-at-login.md)
3333
- [31. role-scoped-agent-git-identity](0031-role-scoped-agent-git-identity.md)
34+
- [32. use-fnox-for-secrets](0032-use-fnox-for-secrets.md)

fnox.toml

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)