A password vault for AI agents
How I keep deploy passwords out of files, commits and chat transcripts while Claude Code still runs my deployments, using Bitwarden Secrets Manager and secret injection.
Since I started building exclusively with Claude Code, the agent does a lot more than write code. It builds, tests and deploys. And deploying needs credentials: an SFTP password, API keys, tokens.
For years my credentials lived where most developers keep them: a gitignored JSON file here, an .ftpconfig there, a note somewhere else. Two things always bothered me about that. First, I was afraid of losing them. A gitignored file is not in any backup that follows the repo. Second, with an agent in the loop, secrets started to leak into places they don’t belong.
When I rebuilt this site, I decided to do it properly. While cleaning up, I even found an old password sitting in plain text in Claude Code’s settings.local.json, because at some point I had allowed a command that contained it.
The idea: a vault plus injection
What I wanted:
- All secrets in one place that is backed up and that I can manage from any machine.
- Scripts and the agent can use a secret without it ever being written into a file, a command line or the chat.
- One set of rules the agent follows in every project.
The tool I ended up with is Bitwarden Secrets Manager. It is separate from the normal Bitwarden password manager and is made for machines: you create projects, put secrets into them, and give a machine account access. Its CLI, bws, has one command that makes the whole thing work: bws run. It fetches the secrets of a project and starts a command with them as environment variables. 1Password has the same concept with op run, so the approach is not tied to one vendor.
Setup
I use one project per repository. The project for this site contains a single secret, ftp_pwd.
The machine account needs an access token, and that token is the one secret that must live on the machine. I didn’t want it in .bashrc or any other file, so it lives in the GNOME keyring, which is unlocked with my login:
secret-tool store --label="bws" service bwsA tiny wrapper in ~/.local/bin/bws reads the token from the keyring for exactly one call and then runs the real binary. The token never lives in my shell environment:
#!/bin/shBWS_ACCESS_TOKEN="$(secret-tool lookup service bws)" exec "$HOME/.local/lib/bws/bws" "$@"One gotcha that cost me a few minutes: my Bitwarden account is on the EU server, and bws talks to the US server by default. The error you get is a rather unhelpful invalid_client. The fix is a one-time config:
bws config server-base https://vault.bitwarden.euUsing a secret without seeing it
With that in place, any command can get a secret injected:
bws run --project-id <project-id> -- 'LFTP_PASSWORD="$ftp_pwd" lftp -u deploy --env-password sftp://example.com'Note the single quotes. Inside them, $ftp_pwd is not expanded by my shell (where it doesn’t exist) but by the shell that bws run starts, after the secret has been injected.
For anything used more than once, a script is nicer. My deploy.sh checks whether the secret is already there, and if not, re-runs itself inside bws run:
# Re-run ourselves inside bws so ftp_pwd is available as an env var.if [[ -z "${ftp_pwd:-}" ]]; then exec bws run --project-id "$BWS_PROJECT_ID" -- "$0" "$@"fiThen it passes the password on through an environment variable, not as an argument, so it doesn’t show up in ps either:
LFTP_PASSWORD="$ftp_pwd" lftp -u "$SFTP_USER" --env-password "sftp://$SFTP_HOST:$SFTP_PORT" <<EOF...EOFNow ./deploy.sh just works, for me and for the agent, and there is no password anywhere in the repository.
Rules for the agent
The last piece is telling the agent how to behave. I added a short section to my global CLAUDE.md, so it applies in every project:
## Secrets / credentials- Secrets live in Bitwarden Secrets Manager, one project per repo. Never put passwords in files, commands, settings allowlists or commit them.- Use secrets only via injection, referencing them as env vars in single quotes: `bws run --project-id <id> -- 'cmd "$secret_key"'` or `bws run --project-id <id> -- ./script.sh`.- Never echo or print secret values; list only key names.- If a project needs a new secret, ask me to add it in Bitwarden; don't create or change secrets yourself.The last rule matters to me. The agent can use secrets, but only I create or change them.
What this protects against, and what it doesn’t
I want to be honest here. This is not a hard security boundary. An agent that can run shell commands could run bws run -- env and print everything. The machine account can read every secret it has access to, whoever is asking.
What the setup does solve:
- Nothing gets lost. Secrets are in Bitwarden, not in files that live only on one laptop.
- Nothing ends up in the repo. No config file with a password that one day gets committed by accident.
- Nothing ends up in transcripts and settings by default. The agent works with variable names, not values. That is exactly the kind of leak I found today.
- Limited blast radius. One project per repo, and the machine account only sees the projects I grant it. If a token leaks, I revoke it in one place.
So it doesn’t protect you from a malicious agent. It protects you from the much more common problem: secrets slowly spreading across files, commands and chat logs until you no longer know where they are.
Conclusion
From now on, every project follows the same pattern: create a Bitwarden project, add the secrets, write the project ID into the deploy script. The agent gets everything it needs to deploy, and I no longer have to worry about where my passwords end up.