Context: the basecamp-cli installer hang (basecamp/basecamp-cli#568, fixed CLI-side in basecamp/basecamp-cli#578).
credstore.NewStore probes OS keyring availability eagerly with a write (keyring.Set). On macOS that shells out to /usr/bin/security -i via zalando/go-keyring (keyring_darwin.go:76 in v0.2.8), whose Wait() is uncancellable. On a locked keychain with no TTY or GUI, the probe blocks forever and the child process cannot be reclaimed — abandoning the goroutine would leak both.
basecamp-cli now defers credstore.NewStore to first credential use, which fixes every credential-free command, but credential-touching commands still block unbounded on a locked headless keychain (escape hatch: the DisableEnvVar). Making that bounded needs support at two layers here, since the CLI cannot do it soundly:
-
Explicit file-only construction — StoreOptions.ForceFile (or Disabled), a programmatic equivalent of the disable env var. Today the only way to force file mode is to mutate the process environment before construction, which isn't a real contract (Store fields are private, so the CLI can't build a file-only store directly either). This is what a caller would fall back to after a probe timeout.
-
A cancellable/bounded probe — either a context-aware operation upstream in go-keyring (the exec.Command process handle lives there, so only it can kill the hung security child), or a platform-specific probe owned by credstore that manages its own child process and can actually reclaim it on timeout. A timeout wrapper outside these layers can only abandon the goroutine: it hides the hang and keeps the leaked security child, which is why basecamp-cli deliberately didn't ship one.
With both in place, callers can do: bounded probe → on timeout, construct file-only with a warning. basecamp-cli would adopt that via a dependency bump.
Context: the basecamp-cli installer hang (basecamp/basecamp-cli#568, fixed CLI-side in basecamp/basecamp-cli#578).
credstore.NewStoreprobes OS keyring availability eagerly with a write (keyring.Set). On macOS that shells out to/usr/bin/security -ivia zalando/go-keyring (keyring_darwin.go:76in v0.2.8), whoseWait()is uncancellable. On a locked keychain with no TTY or GUI, the probe blocks forever and the child process cannot be reclaimed — abandoning the goroutine would leak both.basecamp-cli now defers
credstore.NewStoreto first credential use, which fixes every credential-free command, but credential-touching commands still block unbounded on a locked headless keychain (escape hatch: theDisableEnvVar). Making that bounded needs support at two layers here, since the CLI cannot do it soundly:Explicit file-only construction —
StoreOptions.ForceFile(orDisabled), a programmatic equivalent of the disable env var. Today the only way to force file mode is to mutate the process environment before construction, which isn't a real contract (Storefields are private, so the CLI can't build a file-only store directly either). This is what a caller would fall back to after a probe timeout.A cancellable/bounded probe — either a context-aware operation upstream in go-keyring (the
exec.Commandprocess handle lives there, so only it can kill the hungsecuritychild), or a platform-specific probe owned by credstore that manages its own child process and can actually reclaim it on timeout. A timeout wrapper outside these layers can only abandon the goroutine: it hides the hang and keeps the leakedsecuritychild, which is why basecamp-cli deliberately didn't ship one.With both in place, callers can do: bounded probe → on timeout, construct file-only with a warning. basecamp-cli would adopt that via a dependency bump.