Summary
On macOS the keyring provider stores items in the legacy file-based keychain, whose default ACL binds each item to the code signature of the binary that created it. For secretspec installed via Nix (or any non-Team-ID-signed build), that identity is an adhoc cdhash that changes on every version bump, so after each upgrade macOS shows a GUI password prompt on every secretspec get until the secret is re-written with secretspec set.
This is not documented, and the provider is labelled "Recommended" for local development with no caveats.
Environment
- secretspec 0.20.0, installed via Nix (
/nix/store/<hash>-secretspec-0.20.0/bin/secretspec)
- macOS (Apple Silicon),
login.keychain-db, keychain confirmed unlocked (SecKeychainGetStatus = 0x7)
- Binary signature:
Signature=adhoc, linker-signed, TeamIdentifier=not set
- Designated requirement is purely cdhash-based:
$ codesign -d -r- $(readlink -f $(which secretspec))
# designated => cdhash H"6c98cf2f9dc022abfa749b2cd1bd8eeea847e911"
Evidence
I measured this rather than inferred it. Reads were timed, and accessibility was probed with
SecKeychainSetUserInteractionAllowed(false) so a restricted item returns an error instead of
silently blocking on a GUI dialog.
1. The ACL binds to cdhash, not to the filesystem path.
| Binary |
cdhash |
secretspec get |
| original |
6c98cf2f… |
0.02 s, silent |
| byte-identical copy at a different path |
6c98cf2f… |
0.94 s, silent |
same binary re-signed (codesign -f -s - --identifier …) |
6a5d05ef… |
blocked → GUI prompt |
So a Nix store path change alone is harmless; a content change (i.e. any real upgrade) is not.
2. Items are not interoperable with Apple's own security(1).
An item created by /usr/bin/security add-generic-password cannot be read by secretspec without a
prompt, and vice versa — security is Apple-signed, secretspec is adhoc-signed, and each item's
default ACL trusts only its creator. Re-creating the item with secretspec set restored silent
0.02 s reads.
3. The item's ACL partition list pins it to the creating binary's cdhash.
Dumping the ACLs with SecKeychainItemCopyAccess → SecAccessCopyACLList → SecACLCopyContents
and decoding the partition plist shows exactly what is happening:
| Item |
created by |
Partitions |
| control |
security add-generic-password |
['apple-tool:'] |
control + -A + set-generic-password-partition-list -S 'teamid:,apple:,apple-tool:' |
security |
['teamid:', 'apple:', 'apple-tool:'] |
| real secret |
secretspec set |
['cdhash:6c98cf2f9dc022abfa749b2cd1bd8eeea847e911'] |
That cdhash: value is byte-for-byte the codesign -d -r- designated requirement above. The item is
pinned, in macOS's own data structure, to one exact build of the binary.
There are therefore two independent gates, and -A only opens the first:
- Trusted-application list —
security add-generic-password -A does work here; the dump
confirms trustedApps = NULL ("any application").
- ACL partition list (macOS Sierra and later) — still enforced. An adhoc-signed binary has no
Team ID and is not Apple-signed, so it matches none of teamid: / apple: / apple-tool:.
Verified: even after -A and a successful partition-list update, a different adhoc-signed
reader still received errSecAuthFailed, against a positive control that returned rc=0
(a process reading an item it created itself).
So there is no upgrade-stable workaround available through security(1): the only partition that
admits an adhoc binary is its own cdhash:, which by definition changes on the next build. Adding
the new cdhash after each upgrade also requires the login keychain password
(SecKeychainItemSetAccessWithPassword), making it strictly worse than simply re-running
secretspec set, which needs no password.
Root cause
secretspec depends on keyring = "4.1.6" with default features, so the v1 compatibility layer
hardcodes the legacy store:
// keyring-4.1.6/src/v1.rs:110-111
#[cfg(target_os = "macos")]
let store = apple_native_keyring_store::keychain::Store::new()?;
which uses SecKeychainAddGenericPassword / SecKeychainFindGenericPassword
(security-framework/src/os/macos/passwords.rs). Apple documents the consequence directly:
"This function sets the initial access rights for the new keychain item so that the application
creating the item is given trusted access."
— SecKeychainAddGenericPassword (deprecated since 10.10)
No SecAccess / ACL / trusted-application object is constructed anywhere in the chain, and the
protected (data-protection keychain) backend is compiled out. KeyringConfig exposes only
folder_prefix, so none of this is reachable from secretspec configuration.
Upstream context
This is already understood one layer down, and is currently open and unfixed:
apple-native-keyring-store#24 — filed 2026-09-03 by a fellow Nix user: "after every rebuild, macOS prompts to re-allow access to keychain… Prod builds are created with nix build." Maintainer response: "This binding is by app signature, which changes every build in the case of non-codesigned apps." He notes an entry can be marked "unrestricted" at creation time, proposed adding that option, and is currently on leave.
keyring-rs#272 — same symptom; maintainer: "those settings aren't available via keyring" and "you really shouldn't be using the legacy keychain."
Suggested actions
- Document it now (cheap, unblocks users): note in
docs/.../providers/keyring.md that on macOS the item is bound to the creating binary's code signature, that unsigned/adhoc/Nix builds will re-prompt after upgrades, and that the fix is to re-run secretspec set.
- Adopt the "unrestricted" ACL option once
apple-native-keyring-store#24 lands, ideally opt-in
via KeyringConfig (e.g. unrestricted = true), since it trades ACL binding for "any process
running as this user".
⚠️ Important implementation note for that fix: clearing the trusted-application list alone
is not sufficient. As measured above, -A already yields trustedApps = NULL and access is
still denied, because the ACL partition list independently pins the item to
cdhash:<creating binary>. An "unrestricted" option must also clear or widen the partition list,
otherwise it will appear to work for the creating build and still re-prompt after every upgrade —
exactly the bug it is meant to fix.
- Consider the
protected (data-protection) store behind a feature flag — the upstream maintainer's own recommendation — noting it requires real code-signing/entitlements.
- Suggest
age/sops/dotenv for macOS local dev where upgrade churn is frequent.
Documentation nits spotted while investigating
docs/src/content/docs/concepts/providers.mdx:86 states macOS Keychain "uses Apple's Secure Enclave rather than a TPM" — misleading, since the legacy file-based keychain used here does not keep generic passwords in the Secure Enclave.
docs/src/content/docs/concepts/providers.mdx:44 links to Apple's keychain-data-protection page, i.e. the store secretspec does not use.
Summary
On macOS the
keyringprovider stores items in the legacy file-based keychain, whose default ACL binds each item to the code signature of the binary that created it. For secretspec installed via Nix (or any non-Team-ID-signed build), that identity is an adhoccdhashthat changes on every version bump, so after each upgrade macOS shows a GUI password prompt on everysecretspec getuntil the secret is re-written withsecretspec set.This is not documented, and the provider is labelled "Recommended" for local development with no caveats.
Environment
/nix/store/<hash>-secretspec-0.20.0/bin/secretspec)login.keychain-db, keychain confirmed unlocked (SecKeychainGetStatus=0x7)Signature=adhoc,linker-signed,TeamIdentifier=not setEvidence
I measured this rather than inferred it. Reads were timed, and accessibility was probed with
SecKeychainSetUserInteractionAllowed(false)so a restricted item returns an error instead ofsilently blocking on a GUI dialog.
1. The ACL binds to cdhash, not to the filesystem path.
secretspec get6c98cf2f…6c98cf2f…codesign -f -s - --identifier …)6a5d05ef…So a Nix store path change alone is harmless; a content change (i.e. any real upgrade) is not.
2. Items are not interoperable with Apple's own
security(1).An item created by
/usr/bin/security add-generic-passwordcannot be read by secretspec without aprompt, and vice versa —
securityis Apple-signed, secretspec is adhoc-signed, and each item'sdefault ACL trusts only its creator. Re-creating the item with
secretspec setrestored silent0.02 s reads.
3. The item's ACL partition list pins it to the creating binary's cdhash.
Dumping the ACLs with
SecKeychainItemCopyAccess→SecAccessCopyACLList→SecACLCopyContentsand decoding the partition plist shows exactly what is happening:
Partitionssecurity add-generic-password['apple-tool:']-A+set-generic-password-partition-list -S 'teamid:,apple:,apple-tool:'security['teamid:', 'apple:', 'apple-tool:']secretspec set['cdhash:6c98cf2f9dc022abfa749b2cd1bd8eeea847e911']That
cdhash:value is byte-for-byte thecodesign -d -r-designated requirement above. The item ispinned, in macOS's own data structure, to one exact build of the binary.
There are therefore two independent gates, and
-Aonly opens the first:security add-generic-password -Adoes work here; the dumpconfirms
trustedApps = NULL("any application").Team ID and is not Apple-signed, so it matches none of
teamid:/apple:/apple-tool:.Verified: even after
-Aand a successful partition-list update, a different adhoc-signedreader still received
errSecAuthFailed, against a positive control that returnedrc=0(a process reading an item it created itself).
So there is no upgrade-stable workaround available through
security(1): the only partition thatadmits an adhoc binary is its own
cdhash:, which by definition changes on the next build. Addingthe new cdhash after each upgrade also requires the login keychain password
(
SecKeychainItemSetAccessWithPassword), making it strictly worse than simply re-runningsecretspec set, which needs no password.Root cause
secretspecdepends onkeyring = "4.1.6"with default features, so thev1compatibility layerhardcodes the legacy store:
which uses
SecKeychainAddGenericPassword/SecKeychainFindGenericPassword(
security-framework/src/os/macos/passwords.rs). Apple documents the consequence directly:No
SecAccess/ ACL / trusted-application object is constructed anywhere in the chain, and theprotected(data-protection keychain) backend is compiled out.KeyringConfigexposes onlyfolder_prefix, so none of this is reachable from secretspec configuration.Upstream context
This is already understood one layer down, and is currently open and unfixed:
apple-native-keyring-store#24— filed 2026-09-03 by a fellow Nix user: "after every rebuild, macOS prompts to re-allow access to keychain… Prod builds are created withnix build." Maintainer response: "This binding is by app signature, which changes every build in the case of non-codesigned apps." He notes an entry can be marked "unrestricted" at creation time, proposed adding that option, and is currently on leave.keyring-rs#272— same symptom; maintainer: "those settings aren't available viakeyring" and "you really shouldn't be using the legacy keychain."Suggested actions
docs/.../providers/keyring.mdthat on macOS the item is bound to the creating binary's code signature, that unsigned/adhoc/Nix builds will re-prompt after upgrades, and that the fix is to re-runsecretspec set.apple-native-keyring-store#24lands, ideally opt-invia
KeyringConfig(e.g.unrestricted = true), since it trades ACL binding for "any processrunning as this user".
is not sufficient. As measured above,
-Aalready yieldstrustedApps = NULLand access isstill denied, because the ACL partition list independently pins the item to
cdhash:<creating binary>. An "unrestricted" option must also clear or widen the partition list,otherwise it will appear to work for the creating build and still re-prompt after every upgrade —
exactly the bug it is meant to fix.
protected(data-protection) store behind a feature flag — the upstream maintainer's own recommendation — noting it requires real code-signing/entitlements.age/sops/dotenvfor macOS local dev where upgrade churn is frequent.Documentation nits spotted while investigating
docs/src/content/docs/concepts/providers.mdx:86states macOS Keychain "uses Apple's Secure Enclave rather than a TPM" — misleading, since the legacy file-based keychain used here does not keep generic passwords in the Secure Enclave.docs/src/content/docs/concepts/providers.mdx:44links to Apple's keychain-data-protection page, i.e. the store secretspec does not use.