Keybay CLI
An encrypted local vault and run-scoped secret injection for macOS ARM64 and ordinary Linux ARM64/x64 desktop. No account, Keybay server, daemon or shell hook.
Commit references in .env, not secret values:
API_URL=https://staging.example.com
API_KEY=kb://my-app/api-key
keybay set my-app/api-key
keybay run -- ./app
The child receives ordinary environment variables and needs no Keybay library. Keybay replaces itself with that process; it does not invoke a shell.
Install
brew install danReynolds/tap/keybay
keybay --version
keybay open
Update with brew update and brew upgrade keybay.
Release 0.3.2 archives
need no Dart SDK. Keep the entire extracted directory: launch ./keybay on
macOS or ./bin/keybay on Linux. Companion files are required.
Linux needs glibc 2.35+ and a reachable, unlocked Secret Service provider on the user D-Bus session. Provider UI may appear; headless availability is not guaranteed. The macOS bundle is Developer ID signed and notarized. Exact hashes are in the 0.3.2 publication record.
Upgrading: V1 stores are not migrated, and 0.3.0 rejects retired V2
singleton-passphrase stores. If your 0.2.0 vault has a passphrase or hardware
key, open keybay open → Settings → Security with 0.2.0 and remove every
method before upgrading, then add them back. If you already upgraded, reinstall
0.2.0 from the keybay_cli-v0.2.0 release
first. Vaults without one upgrade as is.
See the CLI changelog.
Development builds
From the repository root:
dart pub get
dart run keybay:keybay_compile packages/keybay_cli/bin/keybay.dart -o build/keybay
./build/keybay --version
The wrapper embeds the CLI’s keybay-cli application namespace. Desktop builds
need Keypass’s native prerequisites.
For a global command following this checkout:
dart pub global activate --source path packages/keybay_cli
Keep the checkout and put $HOME/.pub-cache/bin before older installations in
PATH. With RK, use rk use local -p keybay_cli from the repo.
Source/Pub activation is a development installation. On macOS it trusts the shared Dart VM for Keychain access, so another Dart program can read the platform root. Use an additional credential for real secrets. Activation may also print Pub resolution messages; use the native build when exact CLI output matters.
Verify a release download
Releases are produced locally by rk from tags signed with the maintainer’s release key; the 0.3.0 tags are the exception and were published unsigned. CLI releases since 0.1.1 are immutable GitHub releases, so a quick check confirms that a download is exactly the file attached to the release:
gh release verify-asset keybay_cli-v0.3.2 keybay-0.3.2-macos-arm64.tar.gz --repo danReynolds/keybay
That trusts GitHub’s release record; it is not build provenance. 0.3.0 has no signed tag to check, though on macOS the codesign check below still applies. To verify independently of that record, check the tag with the maintainer’s release key from SECURITY.md, saved as an allowed-signers file, in a clone of this repository:
git -c gpg.ssh.allowedSignersFile=keybay-allowed-signers verify-tag keybay_cli-v0.3.2
The tag’s release-manifest-sha256 line binds the release’s
release-manifest.json, which lists each archive’s SHA-256 and the Homebrew
formula’s. Compare them (on Linux, use sha256sum for shasum -a 256):
git cat-file tag keybay_cli-v0.3.2 | grep '^release-manifest-sha256:'
shasum -a 256 release-manifest.json keybay-0.3.2-macos-arm64.tar.gz
The 0.3.2 publication record records the exact digests. The CLI release audit repeats this chain for each release and daily for the newest release and its published Homebrew formula.
On macOS, after extracting the archive, verify the signed runtime:
codesign --verify --strict --check-notarization lib/keybay/bin/dartaotruntime
codesign -d -r- lib/keybay/bin/dartaotruntime
The runtime identity is io.github.danreynolds.keybay.cli, signed by
Developer ID Application: Pollyn Inc (5AHFA9FUZG). All code companions must
remain present; the release verification checks their signatures as well.
Interactive vault
Run keybay open. Authenticate once, then browse masked values and search names.
| Action | Key |
|---|---|
| Select / search | ↑ / ↓; / |
| Reveal / hide | Space |
| New / edit / delete | n / e or Enter / d |
| Copy selected value | c |
| Scroll revealed value / return to names | → then ↑ / ↓; ← |
| Settings | s |
| Move focus / activate | Tab / Shift+Tab; Enter or Space |
| Hide / dismiss | Esc |
| Exit | q from vault; Ctrl+C or Ctrl+Z anywhere |
In forms, Enter advances from Key to Value, then saves. Alt+Enter adds a line; Ctrl+S saves, Ctrl+R reveals/hides and Esc cancels. Multiline paste, including CRLF, is preserved until submission. Values must be UTF-8 without NUL, up to 1 MiB; passphrases are 1–1024 UTF-8 bytes with exact confirmation. Oversized drafts are discarded. Key renaming is not supported.
Settings has three sections:
- Security: add/remove passphrases and hardware keys.
- Data: Clear deletes records but keeps protection. Reset requires typing
RESET, deletes the managed store and exits. There is no passphrase recovery. - Appearance: Green/Cyan/Blue/Magenta accent and Normal/High contrast.
NO_COLOR=1 keybayremoves color while retaining focus and validation cues.
Appearance and the last successful unlock-method ID are nonsecret local preferences, separate from the vault. Missing preferences use defaults.
The UI requires foreground controlling-terminal stdin/stdout and at least 80×20 cells or 40×24 for the narrow layout. Resize and reported focus loss hide values. Drafts remain only in memory and are cleared on cancellation, accepted submission, invalidation or exit. Five minutes without input exits; this is not configurable. Orderly exit settles submitted work, which may already have committed. SIGSTOP/SIGKILL and terminal captures cannot guarantee cleanup.
Copy deliberately releases a value to the clipboard. macOS restricts it to the
current host and marks it sensitive/transient. Linux uses fixed wl-copy or
xclip paths without a sensitivity hint. Clipboard managers may retain values;
Keybay cannot erase their history. Generic field copy/cut never exports secrets.
Hardware keys in the TUI
- Open Settings → Security → Add hardware key.
- Connect one compatible FIDO2 key, optionally label it, and choose Add.
- Touch when prompted. If requested, enter the key’s existing PIN.
Rejected PINs are never retried automatically. Cancel waits for cleanup. On reopening, the last successful method starts directly. Without a preference, a sole usable method starts; several methods show a choice. Hardware unlock starts immediately, with explicit Retry after failure and Other methods to cancel and switch. Connection attempts can take up to two minutes.
A store supports one passphrase and multiple passkeys, eight methods total. They are alternatives on top of mandatory platform protection. Removing the last method leaves platform-only protection; removal does not delete the key’s credential or revoke older vault snapshots. Passphrase changes use separate remove/add operations, leaving platform-only protection if it was the last method and adding fails.
The CLI uses a stable hardware RP namespace and needs no website. System passkeys are reserved for supported native app hosts. Standalone commands can unlock with enrolled methods; enrollment/removal lives in the TUI.
For disposable attended testing from source:
./tool/build_cli_hardware_test.sh
open 'build/hardware-tui-test/Start hardware test.command'
Reopen to verify recovery in a fresh process. The companion
Test command unlock.command checks standalone command authentication.
Local browser UX preview
The website demo and local preview use fake records; they cannot access or protect a real vault. From the repo root:
dart run fleury:fleury serve --host=127.0.0.1 --port=5777 \
--spawn dart run packages/keybay_cli/tool/tui_preview.dart
Preview flags include --empty, --locked and --copy-fails. Each connection
gets fresh fake state and a simulated clipboard; idle exit is disabled.
Production disables Fleury debug/hot reload and rejects FLEURY_* runtime
settings or an active Dart VM service before opening the store.
Quickstart
Run the included example from packages/keybay_cli/example/quickstart in the
checkout or example/quickstart in a release archive:
keybay run -- ./app.sh
keybay set acme-example/openai-api-key
keybay run -- ./app.sh
keybay rm acme-example/openai-api-key
The first run reports the missing key without launching. Set a disposable value; the second run confirms availability without printing it. See the examples guide to select installed versus source execution.
Commands
| Command | Purpose |
|---|---|
keybay open |
Interactive vault and protection settings. |
keybay run [-f FILE] -- COMMAND [ARGS...] |
Resolve the manifest and replace Keybay with the child. |
keybay set [--stdin] KEY |
Store a value from hidden input or a pipe. No value argument. |
keybay get KEY |
Explicit reveal to the controlling foreground terminal. |
keybay rm KEY |
Delete; idempotent and silent. |
keybay list |
Sorted names, one per line. |
Keys are 1–120 ASCII characters. Each slash-separated segment starts with a
letter/digit and may contain ., _ and -. Names such as my-app/api-key
organize one shared CLI store; they do not isolate repositories.
Interactive set hides input. Enter submits, Backspace edits and Ctrl+U clears.
Bracketed paste preserves exact UTF-8 bytes, including CRLF and trailing newline;
press Enter to submit. Terminal modes are restored on completion or handled exit.
--stdin requires a pipe/redirection, refuses empty input and accepts strict
UTF-8. It preserves internal newlines and removes at most one final LF/CRLF.
Partial producer output cannot be detected; use pipefail or check its status:
op read 'op://Engineering/OpenAI/credential' |
keybay set --stdin my-app/api-key
get checks stdout before opening and again before reveal. Captured/redirected
output, another terminal and background jobs are refused; stdin is untouched.
It prints the value and a newline, which may remain in scrollback. Values with
control/line-break or unsafe Unicode formatting characters are refused; view
them escaped in the TUI or deliver them with run.
The terminal guard prevents accidental disclosure, not access by another program.
A child launched with run can print or transmit its selected secrets.
Authentication and lifetime
Platform protection is mandatory. With additional methods enrolled, commands
select and authenticate one method per invocation; no credential is cached.
PINs are requested only when required, with no automatic retry. Ctrl+C cancels
and waits for provider cleanup. Authentication uses the controlling terminal,
leaving value pipes and child stdin separate. Help, version and literal-only
run do not open the store.
Ordinary desktop namespaces do not isolate same-user programs. Without an
additional method, a program running as you can use list and run to read
values without a terminal or prompt. Add a passphrase or hardware key when
platform/file access alone should be insufficient.
Before a secret-referencing run, the terminal shows the canonical executable,
arguments, manifest variable names and referenced keys; it omits literal/secret
values. Code-affecting assignments such as PATH, LD_*, DYLD_* and
NODE_OPTIONS are marked as a review aid. For protected stores this precedes
authentication. For platform-only stores it is informational, not an approval.
All missing references are reported together and nothing launches.
Handled interrupts stop subsequent actions and settle submitted work. A write may have committed even when interrupted or failed; reopen and inspect before retrying. Keybay never automatically resets a store.
Manifest
Keybay reads ./.env from the current directory. Use -f FILE to select a
different file, such as keybay run -f .env.production -- npm start.
It reads exactly one file: no automatic loading of .env.local or other
variants, merging, fallback, or parent-directory search. A missing or invalid
selected file stops the command before launch. Keybay never writes a manifest.
The selected input must be a regular file (a symlink to one is accepted).
Devices, directories, and FIFOs are rejected without waiting for input. The
file is opened once, with 1 MiB total and 64 KiB physical-line limits.
The grammar is intentionally smaller than dotenv:
- strict UTF-8; LF or CRLF; one leading BOM tolerated
NAME=VALUE, comments, and blank lines- ASCII space/tab trimming around values
- no quotes, escapes, interpolation,
export, continuations, or inline comments - a value beginning
kb://must be a valid key reference - duplicate environment names are errors
Literals are committed plaintext. Keybay cannot determine whether a literal is actually a secret; that classification remains visible in review.
Initial executable lookup uses the inherited PATH, before any manifest
overlay or secret access. Empty/relative PATH entries use the invocation’s
working directory; an unset or unrepresentable PATH performs no search.
Commands containing / resolve directly. The canonical executable and original
arguments are retained through authentication, with no second search or launch
fallback. A manifest’s environment can still affect interpreters, loaders, and
descendants. Path preparation cannot prevent another same-user program from
replacing the executable later.
Security boundary
Secrets stay out of repositories, argv, routine output and interactive shell
state. Explicit Reveal, Copy and get disclose selected values. run preserves
unmentioned parent environment bytes, overlays manifest entries and resolves
all references before launch. It restores shell-default signal behavior for
the child.
Keybay disables its core files and becomes non-dumpable on Linux before opening the store; failure stops the command. The child receives the caller’s original core-file limit. Injected values are ordinary child environment variables: child code, descendants, process inspection and dumps may expose them. Trust the manifest and launched code. Prefer the Dart SDK when you can avoid injection.
See the security policy and external review guide for guarantees and open work. Flatpak CLI packaging, Windows, Snap and mobile CLI distribution are outside the published release scope.
Regression checks
From the repo root:
./tool/test_cli.sh core # unit, disposable SDK, native PTY/exec checks
./tool/test_cli.sh macos # real Keychain, disposable application identity
./tool/test_cli.sh linux # private Secret Service; Docker on macOS
./tool/test_cli.sh all # requires macOS and Docker
No argument selects core. Reports in build/regression identify source,
platform, ABI and results; missing prerequisites block rather than pass.
Fake-provider terminal tests and native bundle relocation checks do not replace
attended hardware or installed-release qualification. See
native packaging and
qualification.
License
MIT.