Because your agent doesn't need to know your secrets.
I keep pasting API keys into Claude Code. Or just letting it cat .env. Every time I tell myself I'll stop doing that. I never do.
# "just read the .env"
cat .env
# "here, use this key"
sk-live-4wB7xK9mN2pL8qR3...Your secrets are now:
- π In the model's context window
- π In your terminal history
- π In that log file you forgot about
- π Training data (maybe?)
- πΈ Screenshot material for your coworker's Slack
There's a better way.
# Agent writes this:
psst STRIPE_KEY -- curl -H "Authorization: Bearer $STRIPE_KEY" https://api.stripe.com
# What the agent sees:
# β
Command executed successfully
# What actually ran:
# curl -H "Authorization: Bearer sk_live_abc123..." https://api.stripe.comThe secret never touches the agent's context. It's injected into the subprocess environment at runtime.
The agent orchestrates. psst handles the secrets.
You set up psst once. Then your agent handles the rest.
npm install -g psst-cli# Create vault (stores encryption key in your OS keychain)
psst init
# Add your secrets
psst set STRIPE_KEY # Interactive prompt, value hidden
psst set OPENAI_API_KEY
psst set DATABASE_URL
# Verify
psst listThat's it. Now onboard your agent:
psst onboardThis adds psst instructions to your CLAUDE.md or AGENTS.md file, teaching your agent:
- How to use
psst SECRET -- command - To ask you to add missing secrets
- To shame you if you try to paste secrets in plain text π€«
psst set <NAME> # Add/update secret (interactive)
psst set <NAME> --stdin # Pipe value in (for scripts)
psst get <NAME> # View value (debugging only)
psst list # List all secret names
psst rm <NAME> # Delete secret
# Import/export
psst import .env # Import from .env file
psst import --stdin # Import from stdin
psst import --from-env # Import from environment variables
psst export # Export to stdout (.env format)
psst export --env-file .env # Export to file
# Vault encryption (for backups/travel)
psst lock # Encrypt vault at rest with password
psst unlock # Decrypt vaultOrganize secrets by environment (dev/staging/prod):
psst init --env prod # Create vault for "prod" environment
psst --env prod set API_KEY # Set secret in prod
psst --env prod list # List secrets in prod
psst --env prod API_KEY -- curl https://api.example.com
# List all environments
psst list envsEnvironments are stored in ~/.psst/envs/<name>/vault.db.
You can also use the PSST_ENV environment variable:
export PSST_ENV=prod
psst list # Uses prod environmentNote: Existing vaults at ~/.psst/vault.db continue to work as the "default" environment.
All commands support:
-g, --global # Use global vault (~/.psst/)
--env <name> # Use specific environment
--tag <name> # Filter by tag (repeatable)
--json # Structured JSON output
-q, --quiet # Suppress output, use exit codesBy default, psst creates a local vault in your project directory:
psst init # Creates .psst/ in current directory
psst init --env dev # Creates .psst/envs/dev/ in current directoryFor user-wide secrets, use the global vault:
psst init --global # Creates ~/.psst/
psst --global set API_KEY # Store in global vault
psst --global list # List global secretsPrevent accidentally committing secrets to git:
# Scan files for leaked secrets
psst scan # Scan all tracked files
psst scan --staged # Scan only git staged files
psst scan --path ./src # Scan specific directory
# Install pre-commit hook (runs scan automatically)
psst install-hookThe scanner checks for actual vault secret values β no regex false positives. If a secret is found:
β Secrets found in files:
config.js:12
Contains: STRIPE_KEY
Found 1 secret(s) in 1 file(s)
Tip: Use PSST_SKIP_SCAN=1 git commit to bypass
Bypass the hook when needed:
PSST_SKIP_SCAN=1 git commit -m "message"
# or
git commit --no-verifyAccidentally overwritten a secret? psst keeps the last 10 versions automatically.
# View version history
psst history API_KEY
# History for API_KEY
#
# β current (active)
# β v2 01/15/2026 14:30
# β v1 01/10/2026 09:15
#
# 2 previous version(s)
# Rollback: psst rollback API_KEY --to <version>
# Restore a previous version
psst rollback API_KEY --to 1
# β Rolled back API_KEY to v1Rollback is reversible β the current value is archived before restoring, so you can always undo.
Organize secrets with tags for easier management:
# Add tags when setting secrets
psst set AWS_KEY --tag aws --tag prod
psst set STRIPE_KEY --tag payments --tag prod
# Manage tags on existing secrets
psst tag DB_URL prod # Add tag
psst untag DB_URL dev # Remove tag
# List secrets filtered by tag
psst list --tag aws # Only aws-tagged secrets
psst list --tag prod # Only prod-tagged secrets
# Run commands with tagged secrets only
psst --tag aws -- aws s3 ls # Inject only aws-tagged secrets
psst --tag prod run ./deploy.sh # Run with only prod secretsTags use OR logic when filtering β psst list --tag aws --tag payments returns secrets with either tag.
You don't read secrets. You use them.
psst run <command>This injects all vault secrets into the command's environment. You never see the values.
# Run any command with all secrets available
psst run ./deploy.sh
psst run python my_script.py
psst run docker-compose upIf you only need certain secrets:
psst <SECRET_NAME> [SECRET_NAME...] -- <command># Single secret
psst STRIPE_KEY -- curl -H "Authorization: Bearer $STRIPE_KEY" https://api.stripe.com
# Multiple secrets
psst AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY -- aws s3 ls- Exit code of the command
- stdout/stderr of the command (with secrets automatically redacted)
- Not the secret value
Secrets are automatically replaced with [REDACTED] in command output. Use --no-mask if you need to see the actual output for debugging.
psst list # See what's available
psst list --json # Structured outputpsst will automatically check environment variables as a fallback. If neither the vault nor the environment has the secret, the command will fail.
Ask the human to add it:
"I need
STRIPE_KEYto call the Stripe API. Please runpsst set STRIPE_KEYto add it."
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Context β
β β
β "I need to deploy the app" β
β > psst run ./deploy.sh β
β β
β [Command executed, exit code 0] β
β β
β (Agent never sees any secret values) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β psst β
β β
β 1. Retrieve encryption key from OS Keychain β
β 2. Decrypt STRIPE_KEY from local vault β
β 3. Inject into subprocess environment β
β 4. Execute: curl ... (with $STRIPE_KEY expanded) β
β 5. Return exit code to agent β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Security model:
- Secrets encrypted at rest (AES-256-GCM)
- Encryption key stored in OS Keychain (macOS Keychain, libsecret, Windows Credential Manager)
- Secrets automatically redacted in command output (
[REDACTED]) - Optional vault lock with password for backups/travel
- Secrets never exposed to agent context
- Zero friction for legitimate use
When keychain isn't available, use the PSST_PASSWORD environment variable:
export PSST_PASSWORD="your-master-password"
psst STRIPE_KEY -- ./deploy.shQ: Why not just use environment variables?
Because export STRIPE_KEY=sk_live_... puts the secret:
- In your shell history
- In your agent's context (if it ran the export)
- Visible to
envandprintenv
psst keeps secrets out of the agent's context entirely.
Q: Why not use a .env file?
.env files are fine for local dev, but:
- Agents can
cat .envand see everything - Easy to accidentally commit
- No encryption at rest
Q: Is this like HashiCorp Vault?
Vault is for teams and infrastructure. psst is for your laptop and your AI agent. Different tools, different problems.
Q: What if the agent runs psst get STRIPE_KEY?
It'll print the value. That's a feature for human debugging. If you're worried, don't give your agent shell access. But honestly, if an agent has shell access, it can already do much worse things.
Q: How is the encryption key stored?
In your OS keychain:
- macOS: Keychain.app (unlocked when you log in)
- Linux: libsecret / gnome-keyring
- Windows: Credential Manager
- Local-first: Your secrets never leave your machine. No cloud, no sync, no account.
- Agent-first: Designed for AI agents to use, not just humans.
- Zero friction: No passwords to type (keychain handles it).
- Single binary: Works everywhere Bun runs.
# Install dependencies
bun install
# Run locally
bun run src/main.ts --help
# Build single binary
bun run buildMIT
psst β because your agent doesn't need to know your secrets