nunciod boots correctly (keys minted, gRPC listening on 127.0.0.1:9420, port confirmed), but nuncio-cli system statushangs indefinitely and prints nothing. No error, no timeout, no output.
Diagnosis (verified, not guessed)
It is a macOS keychain ACL problem, not a broken keyring:
a direct security find-generic-password returns immediately, and the nuncio keychain entries exist;
nunciodcreated the grpc-bearer-token item, so the item's ACL trusts nunciod;
nuncio-cli is a different binary, so reading that item raises a "nuncio-cli wants to access key … in your keychain" consent prompt.
Interactively a human clicks Allow. In any non-interactive context — CI, ssh, a headless box, a background shell — nothing can answer the prompt, so the blocking keyring read never returns and the CLI stalls with no output and no error.
Impact
The reference client is unusable headlessly, which also means the CLI-driven offline E2E story cannot run anywhere without a logged-in desktop session.
Fix
An explicit, opt-in NUNCIO_GRPC_TOKEN override: when set and non-empty the CLI uses it and never touches the keyring. The keyring stays the default.
The keyring path must never hang silently: bound the read and surface an honest, actionable error instead of blocking forever.
Document how to drive the CLI headlessly, and why the prompt appears interactively.
The token is a secret: it must never be logged, echoed, or included in an error message, and never written to disk or SQLite.
Note
nunciod itself is not affected: it creates the item rather than reading one another binary owns, so its own startup raises no consent prompt.
## Symptom
`nunciod` boots correctly (keys minted, gRPC listening on `127.0.0.1:9420`, port confirmed), but `nuncio-cli system status` **hangs indefinitely and prints nothing**. No error, no timeout, no output.
## Diagnosis (verified, not guessed)
It is a macOS keychain **ACL** problem, not a broken keyring:
- a direct `security find-generic-password` returns immediately, and the `nuncio` keychain entries exist;
- `nunciod` **created** the `grpc-bearer-token` item, so the item's ACL trusts `nunciod`;
- `nuncio-cli` is a **different binary**, so reading that item raises a *"nuncio-cli wants to access key … in your keychain"* consent prompt.
Interactively a human clicks Allow. In **any non-interactive context** — CI, ssh, a headless box, a background shell — nothing can answer the prompt, so the blocking keyring read never returns and the CLI stalls with no output and no error.
## Impact
The reference client is unusable headlessly, which also means the CLI-driven offline E2E story cannot run anywhere without a logged-in desktop session.
## Fix
1. An explicit, opt-in `NUNCIO_GRPC_TOKEN` override: when set and non-empty the CLI uses it and never touches the keyring. **The keyring stays the default.**
2. The keyring path must never hang silently: bound the read and surface an honest, actionable error instead of blocking forever.
3. Document how to drive the CLI headlessly, and why the prompt appears interactively.
The token is a secret: it must never be logged, echoed, or included in an error message, and never written to disk or SQLite.
## Note
`nunciod` itself is not affected: it *creates* the item rather than reading one another binary owns, so its own startup raises no consent prompt.
NUNCIO_GRPC_TOKEN short-circuits the keyring in the CLI only — the daemon never accepts a token from its environment. The keyring remains the default when the variable is unset.
The keyring read is also bounded now, because the failure mode was not an error but a call that never returns: a macOS keychain ACL prompt that no one can answer in a headless context. It runs on a detached std::thread behind a 20s timeout rather than spawn_blocking, since Tokio joins its blocking pool at shutdown and that would merely relocate the hang to process exit.
Reported but not fixed: nunciod's own keyring read is not timeout-bounded. It creates the item rather than reading a foreign one, so it does not hit the ACL prompt, and a keyring failure already surfaces as an honest startup error.
Fixed in #420 (`4732d6e`), merged to `dev`.
`NUNCIO_GRPC_TOKEN` short-circuits the keyring in the **CLI only** — the daemon never accepts a token from its environment. The keyring remains the default when the variable is unset.
The keyring read is also bounded now, because the failure mode was not an error but a call that never returns: a macOS keychain ACL prompt that no one can answer in a headless context. It runs on a detached `std::thread` behind a 20s timeout rather than `spawn_blocking`, since Tokio joins its blocking pool at shutdown and that would merely relocate the hang to process exit.
Reported but not fixed: `nunciod`'s own keyring read is not timeout-bounded. It creates the item rather than reading a foreign one, so it does not hit the ACL prompt, and a keyring failure already surfaces as an honest startup error.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Symptom
nunciodboots correctly (keys minted, gRPC listening on127.0.0.1:9420, port confirmed), butnuncio-cli system statushangs indefinitely and prints nothing. No error, no timeout, no output.Diagnosis (verified, not guessed)
It is a macOS keychain ACL problem, not a broken keyring:
security find-generic-passwordreturns immediately, and thenunciokeychain entries exist;nunciodcreated thegrpc-bearer-tokenitem, so the item's ACL trustsnunciod;nuncio-cliis a different binary, so reading that item raises a "nuncio-cli wants to access key … in your keychain" consent prompt.Interactively a human clicks Allow. In any non-interactive context — CI, ssh, a headless box, a background shell — nothing can answer the prompt, so the blocking keyring read never returns and the CLI stalls with no output and no error.
Impact
The reference client is unusable headlessly, which also means the CLI-driven offline E2E story cannot run anywhere without a logged-in desktop session.
Fix
NUNCIO_GRPC_TOKENoverride: when set and non-empty the CLI uses it and never touches the keyring. The keyring stays the default.The token is a secret: it must never be logged, echoed, or included in an error message, and never written to disk or SQLite.
Note
nuncioditself is not affected: it creates the item rather than reading one another binary owns, so its own startup raises no consent prompt.Fixed in #420 (
4732d6e), merged todev.NUNCIO_GRPC_TOKENshort-circuits the keyring in the CLI only — the daemon never accepts a token from its environment. The keyring remains the default when the variable is unset.The keyring read is also bounded now, because the failure mode was not an error but a call that never returns: a macOS keychain ACL prompt that no one can answer in a headless context. It runs on a detached
std::threadbehind a 20s timeout rather thanspawn_blocking, since Tokio joins its blocking pool at shutdown and that would merely relocate the hang to process exit.Reported but not fixed:
nunciod's own keyring read is not timeout-bounded. It creates the item rather than reading a foreign one, so it does not hit the ACL prompt, and a keyring failure already surfaces as an honest startup error.