Using your OS keychain from the terminal on macOS, Linux and Windows
Each operating system already ships an encrypted credential store with a command-line interface. This guide covers the real commands, where the data lives, how to script them without leaking, and when a wrapper is worth adding.
· 8 min read
Where each OS keeps its secrets, and how they are protected
Three vaults, three designs, one shared idea: secrets are encrypted on disk with a key that is only available while you are logged in, and processes get values by asking a service rather than reading a file.
macOS Keychain. The login keychain is a SQLite-backed file at ~/Library/Keychains/login.keychain-db, encrypted with a key derived from your login password and opened by securityd when you log in. Every item carries an access control list of applications allowed to read it. An application that is not on the list triggers the "wants to use your confidential information" dialog. iCloud Keychain is a separate synced store; the command-line tools do not write there by default.
Linux Secret Service. Not a program but a D-Bus interface, org.freedesktop.secrets, with several implementations: GNOME Keyring on GNOME and most other desktops, ksecretservice alongside KWallet on KDE, and KeePassXC when its Secret Service integration is turned on. GNOME Keyring stores its collections under ~/.local/share/keyrings/, encrypted with your login password through a PAM module, so the default collection opens with your session. There is no per-application ACL; anything in your session that can reach the bus can ask.
Windows Credential Manager. Credentials are stored in per-user vault files under your profile and encrypted with DPAPI, which derives the master key from your account credentials. Another local account cannot decrypt them, and an offline copy of the disk is useless without your password. Access is per user, not per application.
macOS: the security command
security is the command-line front end to the Keychain. Generic passwords are the item type for API keys and tokens. -s is the service name (what Keychain Access shows as Name) and -a is the account.
# Store: end the line with -w and no value to be prompted
security add-generic-password -a "$USER" -s openai-api-key -w
# Update an existing item instead of failing
security add-generic-password -U -a "$USER" -s openai-api-key -w
# Read: -w prints only the password
security find-generic-password -a "$USER" -s openai-api-key -w
# Delete
security delete-generic-password -a "$USER" -s openai-api-keyTwo things bite people. First, -w VALUE inline puts the secret in your shell history and, briefly, in ps output; ending the line with a bare -w avoids both. Second, an item created by one binary is not automatically readable by another. If a Node or Python program stored the item, the first security find-generic-password triggers the allow dialog, and the reverse is true too. -T /path/to/app at creation time pre-authorizes an application.
Linux: secret-tool and the Secret Service
secret-tool ships with libsecret (libsecret-tools on Debian and Ubuntu, part of libsecret on Fedora and Arch). Items are identified by arbitrary attribute pairs, not a single name, so pick a convention and keep it.
# Store: the value is read from the terminal or stdin, never from argv
secret-tool store --label='OpenAI API key' service openai account default
# Read
secret-tool lookup service openai account default
# Remove
secret-tool clear service openai account defaultsecret-tool search service openai prints matching items including the secret, so keep it out of scripts and logs. The tool talks to whichever process owns org.freedesktop.secrets on your session bus. On a desktop that is normally GNOME Keyring, started by PAM at login. To use KeePassXC instead, enable Secret Service Integration in its settings and expose a group; only one provider can own the bus name, so stop GNOME Keyring's secrets component first or you will get whichever claimed it.
The headless caveat matters more than any command. Over SSH, in a container, or under a systemd service there is usually no session bus and no daemon, and secret-tool fails with a message about D-Bus autolaunch or org.freedesktop.secrets not being provided. You can start one for the duration of a command:
dbus-run-session -- sh -c 'echo -n "$KEYRING_PASSWORD" | gnome-keyring-daemon --unlock; exec ./deploy.sh'Do not use an empty password to make that convenient: GNOME Keyring stores a keyring with a blank password unencrypted.
Windows: cmdkey and PowerShell SecretManagement
cmdkey writes to Credential Manager but was built for Windows authentication, not for scripts. It can add, list and delete generic credentials, and it never prints a stored password:
# Omit the value after /pass and cmdkey prompts for it
cmdkey /generic:openai-api-key /user:token /pass
cmdkey /list
cmdkey /delete:openai-api-keyFor anything you need to read back, use PowerShell's SecretManagement module with the SecretStore vault. Note that SecretStore is its own encrypted file under your profile, not Credential Manager; a community extension vault is needed to read Credential Manager entries from PowerShell. Microsoft has declared the modules feature complete, so they are maintained but not evolving.
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore
Register-SecretVault -Name LocalStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
# With no -Secret argument, Set-Secret prompts for a SecureString
Set-Secret -Name OPENAI_API_KEY
# Read back as plaintext for a script
$env:OPENAI_API_KEY = Get-Secret -Name OPENAI_API_KEY -AsPlainTextUsing these in scripts without echoing values
The commands above are safe on their own. The leaks come from the glue around them: set -x, echo for debugging, values passed as arguments to a program that logs its argv. A pattern that holds on all three systems:
-
Read the value into a variable, never onto the terminal:
bashKEY="$(security find-generic-password -s openai-api-key -w)" # macOS KEY="$(secret-tool lookup service openai account default)" # Linux -
Pass it through the environment of one child process, not through
exportin the parent shell:bashOPENAI_API_KEY="$KEY" node scripts/embed.js -
Keep tracing off around that block.
set +xbefore the read,set -xafter, or the value lands in the trace output. -
Unset the variable when the block is done:
unset KEY. -
Prefer stdin over arguments when a tool offers it. Arguments are visible to every process on the machine through
psand/proc; stdin is not.
For a store operation in a script, pipe the value in: printf '%s' "$VALUE" | secret-tool store --label='...' service openai account default. On macOS security add-generic-password has no stdin mode, so run it interactively or accept the argv exposure on a single-user machine.
Compared with pass and gpg
pass stores each secret as a gpg-encrypted file under ~/.password-store/, and gpg on its own does the same without the directory conventions. Both are worth knowing because they solve the headless problem: no D-Bus, no session, just a key in gpg-agent. The store is a plain directory, so it syncs with git and works identically on every OS that has gpg.
The costs are real. You own a gpg keypair and its backup story, the agent's cache timeout is your effective session length, and there is no OS-level access control or notification when something reads a file. pass show prints the value to stdout by design, so the scripting hygiene above applies twice. For an interactive laptop, the OS vault is simpler; for a server you administer over SSH, pass or gpg is often the more honest choice.
When a cross-platform wrapper is worth it
If you work on one OS and store a handful of keys, the native tool is enough. A wrapper earns its place when one of these is true:
- You move between macOS, Linux and Windows. q-ring exposes one command set (
qring set,qring get --raw,qring list,qring delete) and writes to the native vault on each: Keychain items, Secret Service items, or Credential Manager generic credentials, all under service names likeq-ring:global. - An AI coding agent needs the secrets. None of the native tools has a notion of an agent. q-ring ships an MCP server,
qring-mcp, that gives Cursor, Claude Code, Kiro and VS Code tools to check, inject and, where you allow it, read secrets, with key-level deny rules and an audit log. See MCP Setup. - You want health checks.
qring doctorverifies the vault works with a write-read-delete probe.qring healthreports which keys are expired or stale by their TTL.qring validate KEYasks the provider whether the key is still live. - You need one command that reads a value into a child process safely.
qring exec -- your-commandinjects secrets into the environment and redacts known values from the output, which is the numbered pattern above with the footguns removed.
q-ring is AGPL-3.0 and local: no account, no cloud, no telemetry. On hosts with no Secret Service at all it offers an opt-in AES-256-GCM file backend behind QRING_BACKEND=file, which must be chosen explicitly and refuses to run without a passphrase. The Getting Started page has the install commands; the CLI Reference covers every flag.
Troubleshooting per OS
macOS: "The specified item could not be found." Service and account must both match; -a defaults to nothing, not to $USER. Run security dump-keychain | grep -A5 '"svce"' to see stored service names, or search in Keychain Access.
macOS: a dialog appears every time a script runs. The item was created by a different binary. Click Always Allow once, or recreate it with -T for the binary that reads it.
Linux: "Cannot autolaunch D-Bus without X11 $DISPLAY". You are outside a graphical session. Use dbus-run-session as shown above, or, on a systemd user session, check systemctl --user status gnome-keyring-daemon.
Linux: the collection is locked after login. Your login password and keyring password have diverged, usually after a password change through a non-PAM path. Open Seahorse or KeePassXC and set the keyring password to match, or open it by hand with gnome-keyring-daemon --unlock.
Linux: two providers fight over the bus name. KeePassXC and GNOME Keyring both want org.freedesktop.secrets. Disable one; busctl --user status org.freedesktop.secrets shows which process owns it.
Windows: cmdkey /list shows the entry but nothing can read it. That is expected; cmdkey has no read path. Use SecretManagement, a Credential Manager extension vault, or a tool that calls the Win32 credential API directly.
Windows: Get-Secret asks for a password. SecretStore is password-protected by default with a timeout. Set-SecretStoreConfiguration -Authentication None removes the prompt at the cost of protecting the store with your Windows account alone.
Next steps
Frequently asked questions
- Is the macOS Keychain encrypted if someone copies login.keychain-db?
- Yes. The file is encrypted with a key derived from your login password, and an offline copy cannot be opened without it. The practical risk is a process running in your logged-in session, not the file at rest.
- Why does secret-tool fail over SSH when it works on the desktop?
- Over SSH there is no session D-Bus and no running Secret Service daemon. Start one for the duration of a command with dbus-run-session and gnome-keyring-daemon --unlock, or use a store that does not need a session, such as pass or q-ring's encrypted file backend.
- Can cmdkey read a password back on Windows?
- No. cmdkey adds, lists and deletes credentials but never prints a stored password. For scriptable reads use PowerShell SecretManagement with a vault, or a tool that calls the credential API directly. q-ring writes Credential Manager generic credentials and reads them back with qring get --raw.
- Do I need q-ring if I only use one operating system?
- Not for a handful of keys you read by hand. It becomes useful when an AI coding agent needs the secrets over MCP, when you want health checks and audit history, or when the same scripts have to work on more than one OS.