Review of how bbx stores the API token today, the gaps, and seven options. No code was changed; this is the write-up. Review date: 2026-08-06.
Pick what goes in the PR
Tick the options to implement. My recommendation is 1 + 2 now, 3 next; see Recommendation for why.
What the app does today
The token comes in through bbx auth login. The flow, in order of preference:
- Interactive prompt.
SecretInput.ReadSecret() intercepts keystrokes, so the
token never echoes to the terminal (src/Bbx/Auth/SecretInput.cs).
- Piped stdin. If input is redirected it reads one line, so
pbpaste | bbx auth login --email x keeps the token out of history.
- A
--token flag. The help text warns against it because it lands in shell
history.
AuthGate runs before every non-auth command and triggers the same prompt on
first use, on a TTY only (src/Bbx/Auth/AuthGate.cs). LoginApiTokenHandler
verifies the token against /2.0/user before saving anything, so a bad paste
never gets persisted.
Storage is FileCredentialStore (src/Bbx/Auth/FileCredentialStore.cs):
- Plaintext JSON at
~/.config/bbx/config.json holding email, token and
default workspace.
- On Unix the directory is chmod 700 and the file 600, set at create time via
UnixCreateMode, so there is no window where the file is world-readable.
- Writes are atomic: temp file with
FileMode.CreateNew, flush to disk, then
File.Move over the target. A crash mid-write cannot leave a torn or
world-readable config.
bbx auth token prints email:token to stdout on purpose, for curl and
scripts. Same footgun as gh auth token; fine, but worth remembering.
There is no environment-variable override. BBX_NO_INTERACTIVE and
BBX_JSON_COMPACT exist; a BBX_API_TOKEN does not.
How secure is that?
For a plaintext-file design, this is close to as good as it gets. Permissions
are set at create time, writes are atomic, the token is never logged, and the
prompt does not echo. That matches what gh did for years and what many CLIs
still do.
The honest gaps:
- Plaintext at rest. Anything that reads the disk gets the token: full
backups, disk images, a stolen unencrypted laptop, malware running as the
user. FileVault/BitLocker covers the stolen-laptop case on modern machines.
- Windows gets no explicit protection. The
UnixCreateMode branch is
skipped and the file relies on default profile ACLs. Usually fine, but
nothing enforces it, and ~/.config is a non-standard spot on Windows.
- No permission check on load. If the user or a sync tool later chmods the
file to 644, bbx reads it without complaint. ssh refuses a loose key file;
bbx could at least warn.
- Scope of the credential is wide. An Atlassian API token is close to a
password. Whatever the storage, the blast radius on leak is the whole
account, which raises the value of doing better than plaintext.
Also worth being honest about the ceiling: on Linux and Windows, any process
running as the same user can read the OS credential store too (Secret Service
answers same-user callers; DPAPI decrypts for the same user). macOS is the
exception, because Keychain prompts per application. So a keychain mostly buys
protection at rest (backups, disk reads, sync accidents), not protection
from a compromised user account.
Options
1. Harden the current file (small, do regardless)
- Warn on load when the file is group/world readable on Unix, like ssh does.
- On Windows, either set an explicit DACL or, better, see option 5.
- Keep everything else as is.
Effort: hours. Cross-platform: yes. User setup: none.
2. Environment-variable override: BBX_API_TOKEN + BBX_EMAIL
Read env vars before the config file, the way GH_TOKEN works. This is the
single cheapest change with the biggest reach:
- CI and Pipelines get a clean path with no config file on disk at all.
- It unlocks every external secret store via a wrapper, with zero native code
in bbx. This is exactly the orchat pattern (solrevdev.orchat README): the
app only reads OPENROUTER_API_KEY, and thin wrappers fetch it from the
macOS Keychain (security find-generic-password), Linux libsecret
(secret-tool), or PowerShell SecretManagement on Windows.
Caveats: an exported variable is visible to every child process, and inline
use can land in shell history. Documented as "for CI and wrappers", it is a
strict improvement.
Effort: hours. Cross-platform: yes. User setup: none (opt-in).
3. A token command hook: token_command in config or BBX_TOKEN_COMMAND
bbx executes a user-supplied command and uses its stdout as the token:
{ "username": "john@solrevdev.com",
"token_command": "security find-generic-password -s BBX_API_TOKEN -w" }
Works with the macOS Keychain, secret-tool, pass, 1Password (op read),
Bitwarden, anything. bbx ships no native dependencies and the config file no
longer contains a secret. This is the kubectl/docker credential-helper model,
shrunk to one line. Pairs well with option 2: the env var for CI, the command
for humans.
Caveats: the user must put the token into their store themselves (one
documented command per OS), and executing a string from a config file needs a
clear docs note that the config is trusted input. Headless Linux without a
Secret Service daemon just keeps using the file, which stays supported as the
fallback.
Effort: a day or two with tests and docs. Cross-platform: yes. User setup:
one command, optional.
4. Shell out to the OS store directly from bbx
bbx auth login writes to the Keychain via security on macOS, secret-tool
on Linux, PowerShell SecretManagement or cmdkey on Windows, and reads it
back the same way. The orchat wrappers, moved inside the tool.
This is the "just works" version of option 3, but the edges are real:
secret-tool needs libsecret, D-Bus and an unlocked keyring, which headless
servers and containers do not have; Windows has no single guaranteed CLI; and
parsing CLI output across OS versions is a maintenance tax. It would need a
plaintext fallback anyway, so it adds the complexity of option 5 without the
reliability of a library.
Effort: several days plus ongoing breakage. Verdict: skip in favour of 3 or 5.
5. Native credential-store integration via a .NET library
The full gh treatment: token goes into the platform store by default, file
fallback with a warning. Prior art in .NET is solid:
- Git Credential Manager (git-ecosystem/git-credential-manager) is a .NET
tool doing exactly this for Bitbucket already: macOS Keychain, Windows
Credential Manager, libsecret, plus a DPAPI-encrypted file and a plaintext
file as fallbacks. Its source is the map for any store abstraction.
- Microsoft.Identity.Client.Extensions.Msal ships a cross-platform
protected storage helper (Keychain / DPAPI / libsecret) used by Microsoft's
own CLIs, and is usable outside MSAL.
Best default UX and the strongest at-rest story, at the price of native
interop, a fallback path that must exist anyway (headless Linux), extra
failure modes at dotnet tool install time, and per-OS testing. The macOS
Keychain also ties entries to the signing identity of the binary, and .NET
global tools run through the shared dotnet host, which blunts the per-app
ACL benefit and can cause re-prompts after SDK updates.
Effort: a week plus. Verdict: right destination if bbx grows an audience;
premature today.
6. Windows-only: encrypt the file with DPAPI
ProtectedData.Protect (NuGet System.Security.Cryptography.ProtectedData)
encrypts the token with a per-user OS key. No prompts, no services, no user
setup, works headless. It is the exact Windows counterpart of the Unix 600
file and closes gap 2 outright. GCM uses the same API for its "encrypted file"
store.
Effort: a day. Cross-platform: Windows only, by design, alongside option 1.
7. Rejected
- Passphrase-encrypted config. Kills non-interactive use, and agents and
scripts are the point of bbx.
- OAuth. Already removed for cause: Bitbucket makes every user create
their own consumer, which needs admin rights and more work than pasting a
token.
- Do nothing about Windows. Cheap to fix; no reason to leave it.
Recommendation
Threat-model first: the config file only falls to someone who can already read
the user's home directory, and on that machine class the OS keychain adds
little except at rest. So spend effort where the leverage is:
- Now: option 1 (permission warning, Windows story) and option 2
(BBX_API_TOKEN/BBX_EMAIL). Small, no user cost, unblocks CI and
keychain wrappers immediately. Add a docs section with the per-OS wrapper
recipes, lifted from orchat.
- Next: option 3 (
token_command). One config line gets the secret out
of the file entirely, on every OS, with no native code.
- Later, on demand: option 6 for Windows at-rest parity, and option 5
only if users ask for keychain-by-default. GCM's store code is the
reference when that day comes.
Questions and answers
Q. Writing to the stores, and how the token command hook works
Read GitHub issue five using the gh tool. Look at point option number two,
which I'll paste below. The reference looks good from a
reading-the-variable or reading-the-value point of view. I like that it uses
the macOS keychain security find-generic-password, Linux's libsecret, or
the PowerShell secret management. Great for reading.
Is there an idiomatic way of writing that file so that, instead of pasting
the password, which is starred in the console, we write to each of those
three password management tools:
- the macOS keychain
- Linux libsecret
- PowerShell secret management?
Also how does the token command hook actually work? Let's say I've got
LastPass installed. How does the command actually deal with these tools? Do
these tools, when installed, notice something's happened and then pop up a
dialogue or something?
A1. Writing to each store
Yes, all three have an idiomatic write, and all three have a form where the
secret never appears in argv or shell history. That matters: the read side is
safe by construction, the write side is where people leak the token.
macOS Keychain
security add-generic-password -a "$USER" -s bbx-api-token -U -w
# prompts "password data for new item:" twice, no echo
-U updates an existing item instead of failing. Read back with
security find-generic-password -s bbx-api-token -w.
Do not write -w "$TOKEN". Anything after -w is in argv and visible to
ps for every user on the box. For a scripted write, feed it through
security -i, which reads commands from stdin:
printf 'add-generic-password -a %s -s bbx-api-token -U -w %s\n' "$USER" "$TOKEN" | security -i
Linux libsecret
secret-tool store --label='bbx API token' service bbx account john@solrevdev.com
# prompts "Password:" with no echo
secret-tool store reads the secret from stdin by design and has no flag to
pass it inline, so it is the best behaved of the three. Scripted:
printf '%s' "$TOKEN" | secret-tool store --label='bbx API token' service bbx account john@solrevdev.com
Use printf '%s', not echo, or you store a trailing newline. Read back with
secret-tool lookup service bbx account john@solrevdev.com.
PowerShell SecretManagement
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore -Scope CurrentUser
Register-SecretVault -Name BbxVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name bbx-api-token -Secret (Read-Host 'API token' -AsSecureString)
Read-Host -AsSecureString is the no-echo prompt. Read back with
Get-Secret -Name bbx-api-token -AsPlainText.
One wrinkle worth documenting: Microsoft.PowerShell.SecretStore locks itself
behind its own password by default and will prompt on every read. For
unattended use:
Set-SecretStoreConfiguration -Authentication None -Interaction None
That drops it to a file encrypted with a per-user key, which is roughly
DPAPI-grade, the same place option 6 lands.
Should bbx auth login do the writing?
No. That is option 4 wearing a different hat, and it buys one command's worth
of convenience for a permanent per-OS maintenance cost. Keep the write manual
and documented, three recipes, one per OS. Migration for an existing user is a
pipe, since bbx auth token already prints to stdout:
bbx auth token | cut -d: -f2- | tr -d '\n' | secret-tool store --label='bbx' service bbx account john@solrevdev.com
A2. How the token_command hook actually works
It is dumb and pull-based, which is the point. Nothing registers, nothing
subscribes, no password manager is aware bbx exists. On each invocation bbx
forks the command, reads stdout, trims the trailing newline, and uses that
string as the token. Non-zero exit means fail with the command's stderr
attached.
Nothing pops up because of bbx. Any dialogue you see comes from the helper CLI,
and only because that CLI decided to ask. Who asks, and when:
| Helper |
Prompts when |
What you see |
security find-generic-password -w |
Item's ACL does not list the caller |
GUI Allow / Always Allow sheet |
secret-tool lookup |
Login keyring locked, or no D-Bus |
gnome-keyring unlock dialogue, or hard failure |
lpass show --password bbx |
Agent session expired (LPASS_AGENT_TIMEOUT, 1h default) |
Master password on the terminal, or a pinentry dialogue if configured |
op read op://Private/bbx/credential |
First use per session |
Touch ID sheet from the 1Password desktop app |
So for the LastPass case: you run lpass login john@solrevdev.com once,
lpass-agent holds the decrypted session in memory for an hour, and inside
that hour lpass show --password bbx-api-token returns instantly with no
prompt at all. After it expires the next bbx command triggers a master password
prompt.
Three implementation consequences:
- Stdin and stderr must be inherited, not captured. Capture stdout only.
If bbx swallows stdin the helper's prompt has nowhere to read from and you
get a hang instead of a password field. This is the single detail that
decides whether the feature feels good or broken.
- Set a timeout, and make it generous.
security returns in
milliseconds; a Touch ID sheet or a typed master password can take 30
seconds. 120s with a clear timeout message is about right. Anything shorter
will bite op and lpass users.
- Do not cache the result. bbx is a one-shot process, so the hook runs on
every invocation. Caching the token to disk would rebuild the plaintext file
the option exists to remove. Helpers with agents already solve this
themselves, and that is their job, not ours.
One bonus that argues for the hook over options 4 and 5 on macOS specifically.
With token_command, the process touching the Keychain is security, a stable
Apple-signed binary that put the item there in the first place, so it is
already on the item's ACL and never prompts. With a native Keychain call from a
.NET global tool, the caller is the shared dotnet host, whose identity shifts
under SDK updates, so users get re-prompted for no visible reason. Option 5
notes this; it is a stronger point than it reads there.
Review of how
bbxstores the API token today, the gaps, and seven options. No code was changed; this is the write-up. Review date: 2026-08-06.Pick what goes in the PR
Tick the options to implement. My recommendation is 1 + 2 now, 3 next; see Recommendation for why.
BBX_API_TOKEN/BBX_EMAILenvironment override (recommended now)token_commandhook, so the token lives in a keychain or password manager (recommended next)What the app does today
The token comes in through
bbx auth login. The flow, in order of preference:SecretInput.ReadSecret()intercepts keystrokes, so thetoken never echoes to the terminal (
src/Bbx/Auth/SecretInput.cs).pbpaste | bbx auth login --email xkeeps the token out of history.--tokenflag. The help text warns against it because it lands in shellhistory.
AuthGateruns before every non-auth command and triggers the same prompt onfirst use, on a TTY only (
src/Bbx/Auth/AuthGate.cs).LoginApiTokenHandlerverifies the token against
/2.0/userbefore saving anything, so a bad pastenever gets persisted.
Storage is
FileCredentialStore(src/Bbx/Auth/FileCredentialStore.cs):~/.config/bbx/config.jsonholding email, token anddefault workspace.
UnixCreateMode, so there is no window where the file is world-readable.FileMode.CreateNew, flush to disk, thenFile.Moveover the target. A crash mid-write cannot leave a torn orworld-readable config.
bbx auth tokenprintsemail:tokento stdout on purpose, for curl andscripts. Same footgun as
gh auth token; fine, but worth remembering.There is no environment-variable override.
BBX_NO_INTERACTIVEandBBX_JSON_COMPACTexist; aBBX_API_TOKENdoes not.How secure is that?
For a plaintext-file design, this is close to as good as it gets. Permissions
are set at create time, writes are atomic, the token is never logged, and the
prompt does not echo. That matches what
ghdid for years and what many CLIsstill do.
The honest gaps:
backups, disk images, a stolen unencrypted laptop, malware running as the
user. FileVault/BitLocker covers the stolen-laptop case on modern machines.
UnixCreateModebranch isskipped and the file relies on default profile ACLs. Usually fine, but
nothing enforces it, and
~/.configis a non-standard spot on Windows.file to 644, bbx reads it without complaint.
sshrefuses a loose key file;bbx could at least warn.
password. Whatever the storage, the blast radius on leak is the whole
account, which raises the value of doing better than plaintext.
Also worth being honest about the ceiling: on Linux and Windows, any process
running as the same user can read the OS credential store too (Secret Service
answers same-user callers; DPAPI decrypts for the same user). macOS is the
exception, because Keychain prompts per application. So a keychain mostly buys
protection at rest (backups, disk reads, sync accidents), not protection
from a compromised user account.
Options
1. Harden the current file (small, do regardless)
Effort: hours. Cross-platform: yes. User setup: none.
2. Environment-variable override:
BBX_API_TOKEN+BBX_EMAILRead env vars before the config file, the way
GH_TOKENworks. This is thesingle cheapest change with the biggest reach:
in bbx. This is exactly the orchat pattern (
solrevdev.orchatREADME): theapp only reads
OPENROUTER_API_KEY, and thin wrappers fetch it from themacOS Keychain (
security find-generic-password), Linux libsecret(
secret-tool), or PowerShell SecretManagement on Windows.Caveats: an exported variable is visible to every child process, and inline
use can land in shell history. Documented as "for CI and wrappers", it is a
strict improvement.
Effort: hours. Cross-platform: yes. User setup: none (opt-in).
3. A token command hook:
token_commandin config orBBX_TOKEN_COMMANDbbx executes a user-supplied command and uses its stdout as the token:
{ "username": "john@solrevdev.com", "token_command": "security find-generic-password -s BBX_API_TOKEN -w" }Works with the macOS Keychain,
secret-tool,pass, 1Password (op read),Bitwarden, anything. bbx ships no native dependencies and the config file no
longer contains a secret. This is the kubectl/docker credential-helper model,
shrunk to one line. Pairs well with option 2: the env var for CI, the command
for humans.
Caveats: the user must put the token into their store themselves (one
documented command per OS), and executing a string from a config file needs a
clear docs note that the config is trusted input. Headless Linux without a
Secret Service daemon just keeps using the file, which stays supported as the
fallback.
Effort: a day or two with tests and docs. Cross-platform: yes. User setup:
one command, optional.
4. Shell out to the OS store directly from bbx
bbx auth loginwrites to the Keychain viasecurityon macOS,secret-toolon Linux, PowerShell SecretManagement or
cmdkeyon Windows, and reads itback the same way. The orchat wrappers, moved inside the tool.
This is the "just works" version of option 3, but the edges are real:
secret-toolneeds libsecret, D-Bus and an unlocked keyring, which headlessservers and containers do not have; Windows has no single guaranteed CLI; and
parsing CLI output across OS versions is a maintenance tax. It would need a
plaintext fallback anyway, so it adds the complexity of option 5 without the
reliability of a library.
Effort: several days plus ongoing breakage. Verdict: skip in favour of 3 or 5.
5. Native credential-store integration via a .NET library
The full
ghtreatment: token goes into the platform store by default, filefallback with a warning. Prior art in .NET is solid:
tool doing exactly this for Bitbucket already: macOS Keychain, Windows
Credential Manager, libsecret, plus a DPAPI-encrypted file and a plaintext
file as fallbacks. Its source is the map for any store abstraction.
protected storage helper (Keychain / DPAPI / libsecret) used by Microsoft's
own CLIs, and is usable outside MSAL.
Best default UX and the strongest at-rest story, at the price of native
interop, a fallback path that must exist anyway (headless Linux), extra
failure modes at
dotnet tool installtime, and per-OS testing. The macOSKeychain also ties entries to the signing identity of the binary, and .NET
global tools run through the shared
dotnethost, which blunts the per-appACL benefit and can cause re-prompts after SDK updates.
Effort: a week plus. Verdict: right destination if bbx grows an audience;
premature today.
6. Windows-only: encrypt the file with DPAPI
ProtectedData.Protect(NuGetSystem.Security.Cryptography.ProtectedData)encrypts the token with a per-user OS key. No prompts, no services, no user
setup, works headless. It is the exact Windows counterpart of the Unix 600
file and closes gap 2 outright. GCM uses the same API for its "encrypted file"
store.
Effort: a day. Cross-platform: Windows only, by design, alongside option 1.
7. Rejected
scripts are the point of bbx.
their own consumer, which needs admin rights and more work than pasting a
token.
Recommendation
Threat-model first: the config file only falls to someone who can already read
the user's home directory, and on that machine class the OS keychain adds
little except at rest. So spend effort where the leverage is:
(
BBX_API_TOKEN/BBX_EMAIL). Small, no user cost, unblocks CI andkeychain wrappers immediately. Add a docs section with the per-OS wrapper
recipes, lifted from orchat.
token_command). One config line gets the secret outof the file entirely, on every OS, with no native code.
only if users ask for keychain-by-default. GCM's store code is the
reference when that day comes.
Questions and answers
Q. Writing to the stores, and how the token command hook works
A1. Writing to each store
Yes, all three have an idiomatic write, and all three have a form where the
secret never appears in
argvor shell history. That matters: the read side issafe by construction, the write side is where people leak the token.
macOS Keychain
-Uupdates an existing item instead of failing. Read back withsecurity find-generic-password -s bbx-api-token -w.Do not write
-w "$TOKEN". Anything after-wis inargvand visible topsfor every user on the box. For a scripted write, feed it throughsecurity -i, which reads commands from stdin:Linux libsecret
secret-tool storereads the secret from stdin by design and has no flag topass it inline, so it is the best behaved of the three. Scripted:
Use
printf '%s', notecho, or you store a trailing newline. Read back withsecret-tool lookup service bbx account john@solrevdev.com.PowerShell SecretManagement
Read-Host -AsSecureStringis the no-echo prompt. Read back withGet-Secret -Name bbx-api-token -AsPlainText.One wrinkle worth documenting:
Microsoft.PowerShell.SecretStorelocks itselfbehind its own password by default and will prompt on every read. For
unattended use:
That drops it to a file encrypted with a per-user key, which is roughly
DPAPI-grade, the same place option 6 lands.
Should
bbx auth logindo the writing?No. That is option 4 wearing a different hat, and it buys one command's worth
of convenience for a permanent per-OS maintenance cost. Keep the write manual
and documented, three recipes, one per OS. Migration for an existing user is a
pipe, since
bbx auth tokenalready prints to stdout:A2. How the
token_commandhook actually worksIt is dumb and pull-based, which is the point. Nothing registers, nothing
subscribes, no password manager is aware bbx exists. On each invocation bbx
forks the command, reads stdout, trims the trailing newline, and uses that
string as the token. Non-zero exit means fail with the command's stderr
attached.
Nothing pops up because of bbx. Any dialogue you see comes from the helper CLI,
and only because that CLI decided to ask. Who asks, and when:
security find-generic-password -wsecret-tool lookuplpass show --password bbxLPASS_AGENT_TIMEOUT, 1h default)op read op://Private/bbx/credentialSo for the LastPass case: you run
lpass login john@solrevdev.comonce,lpass-agentholds the decrypted session in memory for an hour, and insidethat hour
lpass show --password bbx-api-tokenreturns instantly with noprompt at all. After it expires the next bbx command triggers a master password
prompt.
Three implementation consequences:
If bbx swallows stdin the helper's prompt has nowhere to read from and you
get a hang instead of a password field. This is the single detail that
decides whether the feature feels good or broken.
securityreturns inmilliseconds; a Touch ID sheet or a typed master password can take 30
seconds. 120s with a clear timeout message is about right. Anything shorter
will bite
opandlpassusers.every invocation. Caching the token to disk would rebuild the plaintext file
the option exists to remove. Helpers with agents already solve this
themselves, and that is their job, not ours.
One bonus that argues for the hook over options 4 and 5 on macOS specifically.
With
token_command, the process touching the Keychain issecurity, a stableApple-signed binary that put the item there in the first place, so it is
already on the item's ACL and never prompts. With a native Keychain call from a
.NET global tool, the caller is the shared
dotnethost, whose identity shiftsunder SDK updates, so users get re-prompted for no visible reason. Option 5
notes this; it is a stronger point than it reads there.