
AI Agents Still Need Passwords: Three Approaches to Credential Security
I use Claude Code with browser automation and MCP tools to manage infrastructure, and my agent constantly needs plain usernames and passwords. PostgreSQL authenticates with a password, not a token; SMTP requires a username and password; Jenkins accepts Basic Auth; Docker needs credentials passed directly to log in to private registries.
The industry guidance says to use OAuth and scoped tokens. The FusionAuth agent auth guide covers how to handle this with OAuth and JSON Web Token delegation chains. That's the right approach when every service supports it, but many don't.
The risks here are not theoretical. In December 2025, security researcher Ari Marzouk published IDEsaster, uncovering over 30 vulnerabilities across every major AI coding tool tested. The research resulted in 24 Common Vulnerabilities and Exposures (CVEs), which are officially cataloged security flaws serious enough that vendors were compelled to issue patches. Credential exfiltration from AI coding tools is an active, exploited attack vector.
I tested three approaches to give a password to an agent:
- Plaintext environment variables: This is the baseline most people use today. Credentials are exposed both on disk and at runtime, so it's only suitable for throwaway staging credentials.
- Bitwarden MCP: Credentials are encrypted at rest in a vault, but the agent still sees them in plaintext when it retrieves them.
- 1Password CLI with secret references: The agent orchestrates workflows using
op://references and never sees the resolved credentials.
None of these approaches fully solve browser login. If the agent needs to fill a login form, it has to pass the actual password to the form field regardless. For CLI tools, API calls, and subprocess-based workflows, the three approaches offer different levels of credential protection.
Approach 1: Plaintext Environment Variables
Claude Code lets you configure environment variables that are available to the agent in every session. Add them to ~/.claude/settings.json under the env key:
{
"env": {
"SERVICE_EMAIL": "me@example.com",
"SERVICE_PASSWORD": "hunter2"
}
}
The downside is that your password sits in a JSON file on disk in plaintext.
The settings.json file isn't the only place the credential ends up. The password also:
- Appears in the agent's conversation context every time it reads the variable
- Could show up in logs
- Could be accidentally synced to a git repo
The Claude Code permission model protects against unauthorized actions, not credential exposure. A CVE (CVE-2025-59536) found that a cloned repo could exfiltrate credentials via crafted MCP server configs. It's been patched, but the point stands: if a credential is in the agent's environment in plaintext, it's only as safe as the agent's own security boundary.
For a throwaway account on a staging server, this is probably fine. For anything more sensitive, it's worth looking at the other options.
Approach 2: Bitwarden MCP
Bitwarden MCP helps for credentials at-rest. credentials are encrypted in a vault instead of sitting in a JSON file on disk. You can manage, rotate, and revoke them in one place, and there's no risk of accidentally committing passwords to a git repo.
The trade-off is that when the agent retrieves a credential, it sees the full password in plaintext. The Bitwarden MCP server is a vault access tool, not a credential isolation tool. The agent also has access to your entire vault, not just the credential it needs. A compromised or manipulated agent could enumerate all stored credentials. To limit this exposure, use a dedicated Bitwarden account containing only the credentials your agent needs.
If you need encrypted storage for free and accept that the agent handles the raw secret at runtime, this is the right approach.
Installing the Bitwarden CLI and MCP Server
-
Install the Bitwarden CLI:
npm install -g @bitwarden/cli -
Log in to your vault:
bw login -
Add credentials through the Bitwarden web vault, desktop app, or CLI. The agent searches for them by name later, so use something recognizable.
The Bootstrap Problem: Who Authenticates the Agent?
Before the MCP server can read your vault, it needs an unlocked session. The bw unlock command returns a session token, but it requires your master password and the token expires between sessions. Every time you restart Claude Code, you'd need to manually run bw unlock and paste the token somewhere the MCP server can find it.
The solution I landed on uses macOS Keychain as the root of trust:
-
Store your Bitwarden master password in Keychain:
security add-generic-password -s "bitwarden-cli" -a "bw-master" -w -
Create a wrapper script at
~/.local/bin/bw-mcp-server.shthat retrieves the master password, unlocks the vault, and starts the MCP server automatically:#!/bin/bash
BW_MASTER=$(security find-generic-password -s "bitwarden-cli" -a "bw-master" -w 2>/dev/null)
if [ -z "$BW_MASTER" ]; then
echo "Failed to retrieve Bitwarden master password from Keychain" >&2
exit 1
fi
export BW_MASTER
export BW_SESSION=$(bw unlock --passwordenv BW_MASTER --raw 2>/dev/null)
unset BW_MASTER
if [ -z "$BW_SESSION" ]; then
echo "Failed to unlock Bitwarden vault" >&2
exit 1
fi
exec npx -y @bitwarden/mcp-server -
Make it executable:
chmod +x ~/.local/bin/bw-mcp-server.sh
At the time of writing, the wrapper script above doesn't work as-is due to a bug in the MCP server package. See the workaround at the end of this article.
The Keychain is protected by your macOS login and, on Apple Silicon, the Secure Enclave. This is the same trust boundary used for SSH keys and browser-saved passwords. It's not perfect, but it means zero manual steps between sessions. A similar approach could be used for Linux with secret-tool or pass, or Windows' Credential Manager.
Connecting Bitwarden to Claude Code via MCP
-
Add the Bitwarden MCP server to your Claude Code config:
claude mcp add -s user bitwarden -- ~/.local/bin/bw-mcp-server.sh -
Run
/mcpin Claude Code to reconnect. The Bitwarden tools should appear.
Retrieving Credentials and Logging In
I asked Claude Code to log in to a web app, and it pulled the credential from the vault, navigated to the login page, and completed the form without any manual steps.
This is an improvement over the plain text of Approach 1, but the agent still handles the raw secret. If you want the agent to never see the password at all, you need a different architecture entirely.
Approach 3: 1Password CLI With Secret References
The 1Password CLI tool op takes a different approach from Bitwarden MCP. Instead of the agent retrieving credentials from a vault, you store secret references (op://vault/item/field) in your config files. The op run command resolves these references at runtime, injecting the real values as environment variables into subprocesses. The agent orchestrates the work, but the raw credentials never appear in its conversation context.
This costs $3.99/month (or a 14-day free trial), but it's the only approach I tested where the agent can use credentials without seeing them.
Installing the 1Password CLI
-
Install the 1Password CLI with Homebrew:
brew install 1password-cli -
Verify the installation:
op --version
Connecting the CLI to the 1Password Desktop App
The recommended way to keep op authenticated is through the 1Password desktop app:
-
Install the desktop app if you don't have it:
brew install --cask 1password -
Open the app and sign in to your account.
-
Go to Settings > Security and enable Touch ID.
-
Go to Settings > Developer and enable Integrate with 1Password CLI.
With this enabled, op authenticates through the desktop app using Touch ID. No manual tokens or session management needed. This solves the same bootstrap problem from the Bitwarden approach, without a wrapper script. The trade-off is that you get a Touch ID prompt every time a secret is resolved, which can get tedious if the agent runs several op run commands in a session.
Test that it works:
op vault list
You should see your vaults listed. If you get a sign-in error, check that the developer integration setting is enabled.
Storing a Credential
Create a login item in your vault. You can do this through the 1Password desktop app or the CLI:
op item create \
--category login \
--title "My Service" \
--url "https://app.example.com" \
--vault Personal \
-- \
"username=me@example.com" \
"password=my-secret-password"
The credential is now in your 1Password vault, encrypted and synced across your devices.
Using Secret References Instead of Plaintext
Instead of having the agent retrieve the password, create a .env file containing op:// references:
EMAIL="op://Personal/My Service/username"
PASSWORD="op://Personal/My Service/password"
To run a command with the real values injected:
op run --env-file=.env -- sh -c 'curl -X POST https://api.example.com/auth/login \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\", \"password\": \"$PASSWORD\"}"'
The op run command resolves the op:// references and decrypts the secrets in memory. It sets them as environment variables for the subprocess and clears them when the process exits.
What the Agent Sees (and Doesn't See)
By default, op run masks any resolved secret that appears in stdout. If a subprocess echoes the password, the agent sees this:
password is: <concealed by 1Password>
Not the actual value. This is automatic and requires no configuration.
The agent constructs the op run command with op:// references, and the 1Password CLI handles the rest. The agent sees the references (op://Personal/My Service/password) but never the resolved values.
Compared to Bitwarden MCP, you gain:
- The raw password never appears in the agent's conversation context.
- Config files contain references, not secrets, so they are safe to commit.
- Output masking prevents secrets from leaking through stdout.
- Touch ID integration removes the need for wrapper scripts or session token management.
- Credentials are managed centrally and synced across devices.
The subprocess itself still has the secret in its environment variables at runtime, but the agent's conversation context stays clean.
Browser Login: The Unsolved Problem
The op run pattern works well for CLI tools and API calls, but it doesn't help with browser automation. If the agent needs to fill a login form, it has to pass the actual value to the form field. There's no way to pipe an environment variable into a browser input without the agent seeing it. For browser login, all three approaches end up in the same place.
I also tried getting the 1Password browser extension to handle autofill while Claude-in-Chrome handled navigation. In theory, the agent navigates to the login page and the 1Password extension fills the form. In practice, the two extensions conflict: with 1Password installed, Claude-in-Chrome's click, screenshot, and JavaScript operations fail with Cannot access a chrome-extension:// URL of different extension errors.
1Password is building Agentic Autofill to solve this, but it's not available for local Chrome yet.
For now, browser login remains the one scenario where no approach keeps the password out of the agent's context. op run with secret references is the most secure option I tested for everything else.
Which Approach to Use
- Low-risk or throwaway credentials: Plaintext env vars in
settings.jsonare fine. Be aware of the risks and don't sync your dotfiles to a public repo. - Free encrypted storage: Set up Bitwarden MCP. Your credentials are encrypted at rest and managed in one place. The agent still sees the password at runtime, but you eliminate the plaintext-on-disk problem.
- Agent never sees your passwords: Use 1Password CLI with
op runandop://secret references. It costs $3.99/month, but credentials stay out of the agent's conversation context entirely. This covers CLI tools, API calls, and any workflow where the agent runs commands through subprocesses. - Browser login: None of the approaches tested keep the password out of the agent's context. The 1Password Agentic Autofill feature will eventually solve this, but it's not available for local Chrome workflows yet.
Other Credential Management Tools Worth Watching
This space is moving fast. Several other projects take different approaches:
- NanoClaw with OneCLI Agent Vault: This proxy-based approach routes HTTPS traffic through a gateway that injects credentials at the network level. It's open source, but requires adopting their full agent framework.
- Kernel Managed Auth: A separate verified agent handles the login in a cloud browser and yours receives an authenticated session. It's designed for SaaS builders, not local agent workflows.
- Multifactor: This service provides its own AI agent that accesses accounts through an isolated cloud browser. You can't use your own agent or tooling.
- Passwd MCP: The agent sees credential metadata but passwords are redacted to
••••••••. It uses a CLI to inject secrets into subprocesses, but requires Google Workspace.
Password managers are becoming infrastructure for AI agents. The tools aren't fully there yet, but the gap between "credentials in a .env file" and "credentials the agent never sees" is closing fast.
Bitwarden MCP Server Bug
The @bitwarden/mcp-server package (v2026.2.0) silently fails to start when run via npx. The cause is in the server's entry point: it checks process.argv[1].endsWith('index.js'), which fails when Node.js receives the npx symlink path instead of the resolved target.
To work around this, replace the last line of the wrapper script (exec npx -y @bitwarden/mcp-server) with:
# Resolve the npx symlink to the real index.js path
BW_MCP_BIN=$(find ~/.npm/_npx -name "mcp-server-bitwarden" \
-path "*/node_modules/.bin/*" 2>/dev/null | head -1)
BW_MCP_PATH=$(readlink -f "$BW_MCP_BIN" 2>/dev/null || \
python3 -c "import os; print(os.path.realpath('$BW_MCP_BIN'))")
exec node "$BW_MCP_PATH"
This may be fixed in a future version of the package.