Your 1Password secrets in every shell: cached in the system keychain, exported as environment variables or files, and added to ssh-agent.
keysafe is not another password manager: 1Password stays the source of truth. It reads a YAML config of profiles, fetches each secret from 1Password on first use, and caches it in the macOS Keychain or the Linux Secret Service. After that, secrets come from the keychain: your shell starts without waiting for 1Password or asking for Touch ID.
- Environment secrets are printed as shell-quoted
exportstatements (or JSON). - File secrets are written to private
0600files, and their path is exported. - SSH keys are added to ssh-agent with an expiration, piped through
ssh-addwithout touching the disk. - Secrets never appear in process arguments.
Add one line to your shell's startup file and keysafe sets up your secrets in every new shell, with completions for your profiles and secrets.
- macOS (Keychain) or Linux (Secret Service)
- 1Password CLI (
op) - OpenSSH (
ssh-add) for SSH keys
Cargo (builds from crates.io):
cargo install keysafePrebuilt binary with cargo-binstall, for macOS (Apple Silicon, Intel) and Linux (x86-64, arm64):
cargo binstall keysafeNix:
nix profile install github:keysafe-dev/keysafeDownload: each release has a binary per platform: keysafe-aarch64-apple-darwin, keysafe-x86_64-apple-darwin, keysafe-x86_64-unknown-linux-musl and keysafe-aarch64-unknown-linux-musl. The Linux binaries are static and run on any distribution.
curl -fsSL --create-dirs -o ~/.local/bin/keysafe https://github.com/keysafe-dev/keysafe/releases/latest/download/keysafe-aarch64-apple-darwin
chmod +x ~/.local/bin/keysafeThen set up your shell (see Shell integration) and run keysafe doctor to check everything.
Create a starter config, then add your secrets:
keysafe config init # writes ~/.config/keysafe/config.yml (or $XDG_CONFIG_HOME/keysafe/config.yml)
keysafe config edit # opens it in $VISUAL or $EDITOR, and checks it when you close the editor
keysafe config path # shows which config file is used, and whyA complete config looks like this:
version: 1
profiles:
- name: personal
provider:
type: 1password
account: my.1password.com
secrets:
- kind: env
name: GITHUB_TOKEN
path: op://Personal/GitHub/Secrets/GITHUB_TOKEN
- kind: ssh
name: personal-key
path: op://Private/SSH Key/private key?ssh-format=openssh
- kind: file
name: GOOGLE_APPLICATION_CREDENTIALS
path: op://Personal/GCP/service-account-json
- name: work
provider:
type: 1password
account: team.1password.com
secrets:
- kind: env
name: MYAPP_API_KEY
path: op://Infra/Prod/API_KEYSee config.example.yml for a complete annotated example. To find an op:// path, right-click an item in the 1Password desktop app and select Copy Secret Reference. Append ?ssh-format=openssh for SSH keys.
Each profile reads its secrets from one provider. Today that's type: 1password, with an optional account: the account to use, as op --account takes it (a sign-in address, an email address or an account ID). Without it, the 1Password CLI's default account is used.
Commands use the profile marked default: true, or the first profile, when you don't pass -p.
Secret names of env and file secrets must be valid environment variable names; SSH key names can be any label.
| Variable | Default | Description |
|---|---|---|
KEYSAFE_CONFIG_FILE |
$XDG_CONFIG_HOME/keysafe/config.yml (~/.config/keysafe/config.yml) |
Config file (--config) |
KEYSAFE_STATE_DIR |
$XDG_STATE_HOME/keysafe (~/.local/state/keysafe) |
Records which profiles were loaded (--state-dir) |
KEYSAFE_DEFAULT_PROFILE |
the profile marked default: true, or the first one |
Profile used when none is given (-p) |
Add to ~/.zshrc:
eval "$(keysafe init zsh)"or to ~/.bashrc:
eval "$(keysafe init bash)"This defines a keysafe shell function, so load, unload and export change your current shell. On every new shell, it also:
- exports the cached secrets of the profiles you loaded before, from the keychain only, without contacting 1Password (
init zsh --no-exportturns this off), - completes commands, options, profile names and secret names,
- removes the files of file secrets when the shell exits.
Usage: keysafe <COMMAND>
Commands:
load Load secrets of a profile into the current shell.
unload Unload secrets of a profile from the current shell.
read Print the value of a secret.
export Export the environment and file secrets of a profile as shell statements.
exec Execute a command with the secrets of a profile in its environment.
status Show what is loaded: secrets in this shell, SSH keys in the agent, exported profiles.
doctor Check the setup and say how to fix problems.
profile List, show, clear and prune profiles.
config Create, edit and locate the config file.
init Print the shell integration script for zsh or bash.
Every command takes -q (warnings and errors only) and -v (details for debugging, never secret values), and its --help ends with examples.
load puts secrets in place: environment secrets are exported, file secrets are written to private files whose paths are exported, and SSH keys are added to ssh-agent.
keysafe load -p work # every secret of the profile
keysafe load -p work -e 8h # ... with SSH keys that expire after 8 hours
keysafe load GITHUB_TOKEN # one secret of the default profile
keysafe load github-work -e 4h # one SSH keyLoading a whole profile records it, so its cached secrets are exported in every new shell. Loading individual secrets doesn't.
unload undoes load: it unsets the variables, deletes the files of file secrets and removes the SSH keys keysafe added from ssh-agent. That includes secrets loaded before you removed them from the config.
keysafe unload -p work # the whole profile; new shells no longer get it either
keysafe unload GITHUB_TOKEN # one secretThe cached secrets stay in the keychain, so loading again is instant. keysafe profile clear deletes them.
keysafe read GITHUB_TOKEN # print a value (SSH keys are never printed)
keysafe exec -p work -- terraform plan # run one command with the secrets; files are removed afterwards
keysafe export -p work --format json # the secrets as a JSON objectWithout the shell integration, load and export print export statements to evaluate yourself, since a program can't change the environment of the shell that started it. On a terminal, load and unload never print them, so secrets don't end up on screen: they still add or remove SSH keys, and say how to set up the integration for the variables.
eval "$(keysafe export -p work)"Add --refresh (-r) to load, read, export or exec to bypass the cache and fetch from 1Password again.
keysafe status # every profile: variables set in this shell, SSH keys and their expiry
keysafe status -p work # one profilestatus never prints values. It also tells you whether the shell integration is active.
keysafe profile list # profile names
keysafe profile show work # provider, secrets and whether it's exported in new shells (never values)
keysafe profile clear work # delete its cached secrets and forget it was loaded
keysafe profile prune work # delete only what the config no longer namesWhen you remove a secret from the config, its cached value and any SSH key keysafe added stay behind until you prune them. keysafe doctor tells you when there is something to prune, including profiles you removed from the config. Pruning doesn't touch open shells: run keysafe unload there.
- Configuration: profiles map secret names to
op://references. - 1Password CLI: secrets are fetched with
op readwhen they aren't cached, soopcan still ask for authorization. - Keychain cache: values are stored as generic passwords, service
keysafe.<profile>, account<secret name>. Loaded profiles are recorded in<state dir>/<profile>.metadata. - Output:
exportstatements single-quote every value, so quotes,$, backticks and newlines surviveevalunchanged. Under the shell integration they travel on file descriptor 3, so values, JSON, help and errors still go straight to the terminal. - SSH agent: keys are piped to
ssh-add -with your expiration. A key the agent already holds is left alone unless you pass--refresh. - File secrets are written to
--runtime-dirwhen given, otherwise to a private temporary directory: the shell integration removes it when the shell exits,execonce the command exits, and otherwise you are told to remove it.
keysafe picks up where the zsh-op plugin left off:
- With no config at the new location,
~/.config/op/config.ymlis used, with a hint to move it. - Profiles recorded in
~/.cache/opcount as loaded until you load them again. - Secrets cached under
op-secrets-<profile>move tokeysafe.<profile>the first time they are read; the old items are deleted. - Items zsh-op cached that keysafe never reads, such as secrets no longer in your config, stay behind.
keysafe doctorwarns about them andkeysafe profile prune <profile>deletes them.
Start with keysafe doctor. It checks the config, the 1Password CLI and your accounts, the keychain, ssh-agent and the shell integration, without prompting for anything, and says how to fix each problem:
✓ Config: /Users/me/.config/keysafe/config.yml (the default location), 2 profile(s)
✗ 1password (work): op does not know account team.1password.com (run: op account add)
✓ Keychain: reachable
✓ SSH agent: running, 1 key(s)
! Shell integration: not active in this shell (add `eval "$(keysafe init zsh)"` to ~/.zshrc, or `init bash` to ~/.bashrc)
"not signed in to 1Password account": run op signin --account my.1password.com.
macOS asks to allow keysafe access to the keychain: secrets cached by other programs (for example older zsh-op versions, which used /usr/bin/security) need your approval once per item. Choose Always Allow. Locally built binaries are not signed with a stable identity, so the prompt can come back after an upgrade. Alternatively, run keysafe profile clear <profile> and then keysafe load -r -p <profile> to re-cache the secrets.
"SSH agent is not running": start one with eval $(ssh-agent).