A secure CLI password manager with age encryption and macOS Keychain integration.
- Master Password Storage: Securely store your master password in macOS Keychain
- Age Encryption: All secrets are encrypted using age encryption
- Vault System: Organize secrets into vaults with expiration times
- Vault Expiry: Vaults automatically expire after a specified duration
- Verification Flow: Re-authenticate expired vaults via secure HMAC-signed URLs
- 24-Hour Approval: One-time verification grants 24-hour access to expired vaults
brew tap SammyLin/tap
brew install --cask psw-cliTo upgrade:
brew upgrade --cask psw-cligit clone https://github.com/SammyLin/psw-cli.git
cd psw-cli
go build -o psw-cli .If you see a security warning when running psw-cli for the first time:
"Apple cannot verify whether psw-cli is malicious software"
This is because psw-cli is not notarized by Apple. To allow it:
- Go to System Settings → Privacy & Security
- Click "Open Anyway" (or "Still Open")
Or disable Gatekeeper temporarily:
sudo spctl --master-disableSet your master password (stored in macOS Keychain):
psw-cli initYou will be prompted to enter a master password. Alternatively, pass it via flag:
psw-cli init --password "your-secure-password"Create a vault with an expiration time:
psw-cli vault create my-vault --expire 30dExpiration format: Nd (days), Nh (hours), Nm (minutes)
Store a secret in a vault:
psw-cli set github-token ghp_xxxxxxx --vault my-vaultGet a secret from a vault:
psw-cli get github-token --vault my-vaultIf the vault is expired, you will receive a verification URL to re-authenticate.
Delete a secret from a vault:
psw-cli rm github-token --vault my-vaultList all vaults and their expiry status:
psw-cli vault listExtend a vault's expiration:
psw-cli vault renew my-vault --expire 30d- All secrets are encrypted using age encryption
- Master password is stored in macOS Keychain using
securitycommand - Age uses scrypt for key derivation (memory-hard function)
When accessing an expired vault:
- A UUID token is generated
- HMAC-SHA256 signature is created
- Verification URL is generated
- User clicks the link to confirm
- Token is marked as used, 24-hour approval is written to
~/.psw-cli/verify/approved/{vault}/{token}.json
| Variable | Description | Default |
|---|---|---|
PSW_CLI_VERIFY_URL |
Base URL for verification links | http://localhost:8080 |
PSW_CLI_HMAC_SECRET |
HMAC secret for signing verification URLs | - |
PORT |
Server port | 8080 |
LOG_DIR |
Directory for log files | - |
psw-cli/
├── main.go # Entry point
├── cmd/ # CLI commands
│ ├── cmd.go # CLI runner
│ ├── init.go # init command
│ ├── post.go # post command
│ get.go # get command
│ rm.go # rm command
│ └── vault.go # vault management commands
├── pkg/ # Core packages
│ ├── crypto.go # Age encryption
│ ├── keychain.go # macOS Keychain integration
│ ├── vault.go # Vault management
│ └── verify.go # Verification URL generation
├── go.mod # Go module
└── README.md # This file
- Vaults:
~/.psw-cli/vaults/ - Logs:
~/.psw-cli/logs/ - Verification Tokens:
~/.psw-cli/tokens/ - Approvals:
~/.psw-cli/approvals/ - Verification Approvals:
~/.psw-cli/verify/approved/{vault}/{token}.json - Master Password: macOS Keychain (service:
psw-cli)
psw-cli works as an OpenClaw SecretRef exec provider. This means your OpenClaw config can reference secrets stored in psw-cli vaults instead of storing API keys as plaintext.
{
"tools": {
"web": {
"search": {
"apiKey": "BSAlrRE9ka..."
}
}
}
}{
"tools": {
"web": {
"search": {
"apiKey": {
"source": "exec",
"provider": "psw",
"id": "brave_search_key"
}
}
}
}
}- Add psw-cli as a secrets provider in your OpenClaw config:
{
"secrets": {
"providers": {
"psw": {
"source": "exec",
"command": "/opt/homebrew/bin/psw-cli",
"args": ["resolve", "-v", "my-vault"],
"passEnv": ["HOME", "PATH"],
"jsonOnly": true
}
}
}
}- Store your secrets:
psw-cli set brave_search_key "your-api-key" -v my-vault
psw-cli set openai_api_key "sk-..." -v my-vault-
Replace plaintext values with SecretRef objects in your config.
-
Verify with OpenClaw:
# Audit — should show plaintext=0
openclaw secrets audit
# Reload secrets
openclaw secrets reloadOpenClaw sends a JSON request to psw-cli via stdin:
{"protocolVersion": 1, "provider": "psw", "ids": ["brave_search_key", "openai_api_key"]}psw-cli decrypts and returns:
{"protocolVersion": 1, "values": {"brave_search_key": "BSA...", "openai_api_key": "sk-..."}}Secrets are resolved at startup into an in-memory snapshot — they never touch disk as plaintext.
Use --raw to get just the value (no Secret: prefix):
API_KEY=$(psw-cli get my-key -v my-vault --raw)
curl -H "Authorization: Bearer $API_KEY" https://api.example.com| Command | Description |
|---|---|
psw-cli init |
Set master password |
psw-cli set <key> <value> --vault <vault> |
Store a secret |
psw-cli get <key> --vault <vault> |
Retrieve a secret |
psw-cli get <key> --vault <vault> --raw |
Retrieve value only (no prefix) |
psw-cli rm <key> --vault <vault> |
Delete a secret |
psw-cli resolve --vault <vault> |
SecretRef exec provider (stdin/stdout JSON) |
psw-cli vault create <vault> --expire <duration> |
Create a vault |
psw-cli vault list |
List all vaults |
psw-cli vault renew <vault> --expire <duration> |
Renew a vault |
MIT License