Store project secrets in the macOS Keychain and hand them to one command's environment,
instead of keeping a plaintext .env file.
A small Python command-line tool for macOS. Each project gets a namespace in your login Keychain holding named environment variables. You store values at a hidden prompt, then run your development command through the tool, which places the variables in that command's environment and nowhere else. Your shell is never modified. Nothing is written to disk.
Requires macOS 13 or later and Python 3.11 or later. There is no fallback storage on other platforms; the tool exits with code 6.
From PyPI:
uv tool install keychain-cli
keychain-cli --versionOr from the git repository:
uv tool install git+https://github.com/civicactions/keychain-cli
keychain-cli --versionTo uninstall:
uv tool uninstall keychain-cli-
Store secrets in a project namespace:
keychain-cli set my-project API_TOKEN DB_PASSWORDYou are prompted for each value without terminal echo:
Value for my-project/API_TOKEN: Value for my-project/DB_PASSWORD: stored: API_TOKEN, DB_PASSWORD -
Run your command with secrets injected:
keychain-cli run my-project -- npm run dev
The command starts with
API_TOKENandDB_PASSWORDavailable in its environment. When the process finishes, no secrets remain in your shell session.
Projects can commit a .keychain-cli.toml manifest in the project root directory to declare
the namespace and required environment variable names (names only, never values):
# .keychain-cli.toml
namespace = "my-project"
variables = [
"API_TOKEN",
"DB_PASSWORD",
]-
Check for missing variables without storing anything:
keychain-cli check
Exits
0if all variables are present, or10if any are missing (listing the missing names). -
Initialize missing secrets:
keychain-cli init
Prompts only for declared variables not yet in your Keychain. Existing values are never touched.
-
Run without typing the namespace:
keychain-cli run -- npm run dev
The tool infers
my-projectfrom.keychain-cli.tomlin the current directory.
| Command | Summary |
|---|---|
set <namespace> <VAR>... |
Store one or more secrets via hidden prompt or standard input |
list [<namespace>] |
List namespaces, or variable names in a namespace |
run [<namespace>] -- <cmd> |
Run a command with secrets injected into its environment |
import <namespace> <file> |
Import an existing .env file into a namespace |
delete <namespace> [<VAR>] |
Remove a single variable or an entire namespace |
init [<namespace>] |
Prompt for missing secrets declared in .keychain-cli.toml |
check [<namespace>] |
Report which declared secrets are missing |
For full command documentation, options, and exit codes, run keychain-cli <command> --help
or see specs/001-keychain-secret-manager/contracts/cli.md.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Unexpected internal error |
| 2 | Usage error: bad arguments, missing --, out-of-range option |
| 3 | Not found: namespace, variable, file, or manifest |
| 4 | Refused: overwrite or deletion declined, or non-interactive without --force |
| 5 | Keychain error: locked, denied, canceled, unavailable, or unexpected status |
| 6 | Unsupported platform or Python version |
| 7 | Invalid input: bad name, malformed manifest, empty or ambiguous stdin value |
| 8 | run: command not found or not executable |
| 9 | copy: clipboard write failed, or clearing could not be scheduled |
| 10 | check: one or more required variables are missing |
| 130 | Interrupted during set, import, or init |
For destinations that cannot read environment variables (such as a vendor web console), a single secret can be copied to the system clipboard:
keychain-cli copy my-project API_TOKENSecurity notice:
- This is an occasional fallback, not the normal way to use secrets; prefer
keychain-cli run. - The clipboard is readable by every application running as your user. Clipboard managers, history tools, and clipboard sync services (such as Universal Clipboard) can defeat automatic clearing.
- The secret is automatically cleared after 45 seconds (configurable with
--clear-after SECONDS, 1–300) only if the clipboard still contains the value. - Governed by exception entry CLIP-001 in SECURITY-EXCEPTIONS.md.
macOS asks me to allow keychain-cli to access an item. Items in the login Keychain
trust the application that created them, and for this tool that application is the Python
interpreter. After uv tool upgrade switches to a different Python, or when you run from a
development checkout, macOS asks once per item. Click Always Allow. This is the
operating system protecting the item, not a defect.
"Keychain operation failed ... interaction not allowed" over SSH. The login Keychain is locked and there is no screen on which macOS can show the unlock dialog. Unlock it at the console, or run the command locally.
Exit code 6. You are not on macOS 13 or later, or Python is older than 3.11. Reinstall
with uv tool install, which brings its own supported interpreter.
- Secret values never appear in command-line arguments, shell history, log output, error
messages, temporary files, or exception text. The Keychain is driven directly through the
Security framework, not through the
securitycommand. - Each developer's Keychain holds their own values. The tool never syncs or shares them.
- The threat model is in docs/threat-model.md. Deliberate exceptions to the rules above are listed in SECURITY-EXCEPTIONS.md. Report vulnerabilities as described in SECURITY.md.