# 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.

By Thomas Burkhart · Published September 24, 2026
Source: https://escamoteur.dev/blog/a-password-vault-for-ai-agents/

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](https://bitwarden.com/products/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:

```bash
secret-tool store --label="bws" service bws
```

A 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:

```sh
#!/bin/sh
BWS_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:

```bash
bws config server-base https://vault.bitwarden.eu
```

## Using a secret without seeing it

With that in place, any command can get a secret injected:

```bash
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`:

```bash
# 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" "$@"
fi
```

Then it passes the password on through an environment variable, not as an argument, so it doesn't show up in `ps` either:

```bash
LFTP_PASSWORD="$ftp_pwd" lftp -u "$SFTP_USER" --env-password "sftp://$SFTP_HOST:$SFTP_PORT" <<EOF
...
EOF
```

Now `./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:

```markdown
## 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.
