• Stop Storing API Keys in Plaintext Config Files (KeepassXC + Secret Service)
  • ___ _
  • / __| | |_ ___ _ __
  • \__ \ | _| / _ \ | '_ \
  • |___/ \__| \___/ | .__/
  • |_|
  • ___ _ _
  • / __| | |_ ___ _ _ (_) _ _ __ _
  • \__ \ | _| / _ \ | '_| | | | ' \ / _` |
  • |___/ \__| \___/ |_| |_| |_||_| \__, |
  • |___/
  • _ ___ ___
  • /_\ | _ \ |_ _|
  • / _ \ | _/ | |
  • /_/ \_\ |_| |___|
  • _ __
  • | |/ / ___ _ _ ___
  • | ' < / -_) | || | (_-<
  • |_|\_\ \___| \_, | /__/
  • |__/
  • _
  • (_) _ _
  • | | | ' \
  • |_| |_||_|
  • ___ _ _ _ _
  • | _ \ | | __ _ (_) _ _ | |_ ___ __ __ | |_
  • | _/ | | / _` | | | | ' \ | _| / -_) \ \ / | _|
  • |_| |_| \__,_| |_| |_||_| \__| \___| /_\_\ \__|
  • ___ __ _
  • / __| ___ _ _ / _| (_) __ _
  • | (__ / _ \ | ' \ | _| | | / _` |
  • \___| \___/ |_||_| |_| |_| \__, |
  • |___/
  • ___ _ _
  • | __| (_) | | ___ ___
  • | _| | | | | / -_) (_-<
  • |_| |_| |_| \___| /__/
  • __ _ __ __ __ ___
  • / / | |/ / ___ ___ _ __ __ _ ___ ___ \ \/ / / __|
  • | | | ' < / -_) / -_) | '_ \ / _` | (_-< (_-< > < | (__
  • | | |_|\_\ \___| \___| | .__/ \__,_| /__/ /__/ /_/\_\ \___|
  • \_\ |_|
  • _
  • _| |_
  • |_ _|
  • |_|
  • ___ _
  • / __| ___ __ _ _ ___ | |_
  • \__ \ / -_) / _| | '_| / -_) | _|
  • |___/ \___| \__| |_| \___| \__|
  • ___ _ __
  • / __| ___ _ _ __ __ (_) __ ___ \ \
  • \__ \ / -_) | '_| \ V / | | / _| / -_) | |
  • |___/ \___| |_| \_/ |_| \__| \___| | |
  • /_/
  • ╔─*──*──*──*──*──*──*──*──*──*──*──*──*──*──*──*─╗
  • ║1 ........................................ 1║
  • ║2* ........................................ *2║
  • ║3 ........................................ 3║
  • ║1 ...........Posted: 2026-03-31........... 1║
  • ║2* .Tags: sysadmin linux security debian .. *2║
  • ║3 ........................................ 3║
  • ║1 ........................................ 1║
  • ╚────────────────────────────────────────────────╝
  • I found API keys and tokens sitting in plaintext in ~/.claude.json -- the config
  • file for Claude Code's MCP servers. Trello tokens, Habitica API keys, an Otter
  • password. Just sitting there in a user file that could end up in a backup, a
  • dotfile repo, a screen share.
  • This is how I moved them into KeepassXC via the freedesktop Secret Service API,
  • so the config file only contains variable references.
  • This post assumes you already have KeepassXC set up as your Secret Service
  • provider. If not, see my earlier post on KeepassXC as a keyring manager.
  • ## The problem
  • Tools that use config files with env blocks store secrets in plaintext:
  • ```
  • "env": {
  • "TRELLO_API_KEY": "04e633dc273440d1df0d5fa6a0e7d873",
  • "TRELLO_TOKEN": "ATTA598f072ba685622a606..."
  • }
  • ```
  • These are user files. They get backed up, synced, shared, grepped, accidentally
  • committed.
  • ## The fix
  • Three pieces:
  • 1. Store secrets in KeepassXC via secret-tool
  • 1. Export them in .zshrc at shell startup
  • 1. Replace plaintext values with ${VAR} references in the config
  • ## 1. Install secret-tool
  • ```
  • sudo apt install libsecret-tools
  • ```
  • This is the CLI for the freedesktop Secret Service D-Bus API. It talks to
  • whatever is registered as the Secret Service provider -- in our case, KeepassXC.
  • ## 2. Disable gnome-keyring as Secret Service provider
  • If gnome-keyring is installed, it will race KeepassXC for the
  • org.freedesktop.secrets D-Bus name. When KeepassXC is locked or hasn't started
  • yet, gnome-keyring silently wins and your secrets go into its own keyring file
  • instead.
  • Neuter gnome-keyring's secrets component without uninstalling it (other things
  • depend on it for SSH, PKCS#11, etc.):
  • ```
  • # Stop D-Bus auto-activation for secrets
  • sudo bash -c 'cat > /usr/share/dbus-1/services/org.freedesktop.secrets.service << EOF
  • [D-BUS Service]
  • Name=org.freedesktop.secrets
  • Exec=/usr/bin/false
  • EOF'
  • # Hide the autostart entry
  • mkdir -p ~/.config/autostart
  • cp /etc/xdg/autostart/gnome-keyring-secrets.desktop ~/.config/autostart/
  • echo "Hidden=true" >> ~/.config/autostart/gnome-keyring-secrets.desktop
  • # Kill any running instance
  • killall gnome-keyring-daemon
  • ```
  • Then delete the old gnome-keyring data if you don't need it:
  • ```
  • rm ~/.local/share/keyrings/login.keyring
  • ```
  • ## 3. Enable KeepassXC Secret Service
  • In KeepassXC:
  • - Tools > Settings > Secret Service Integration > Enable
  • - Database Settings > Secret Service Integration > Expose a group
  • Verify it's working:
  • ```
  • dbus-send --session --dest=org.freedesktop.secrets \
  • --type=method_call --print-reply \
  • /org/freedesktop/secrets \
  • org.freedesktop.Secret.Service.ReadAlias string:default
  • ```
  • You should see a path like `/org/freedesktop/secrets/collection/Passwords`, not
  • `/collection/login` (that's gnome-keyring).
  • ## 4. Store secrets
  • KeepassXC must be unlocked. Each secret gets a label, a service name for
  • grouping, and an env_var attribute for the export name:
  • ```
  • echo -n "YOUR_API_KEY" | secret-tool store \
  • --label="MCP Trello API Key" \
  • service trello-drex \
  • env_var TRELLO_API_KEY \
  • usage mcp
  • ```
  • Repeat for each secret. The attributes are arbitrary key-value pairs used for
  • lookup. I used `service` to group by MCP server and `env_var` for the
  • environment variable name.
  • ## 5. Export in .zshrc
  • At the bottom of ~/.zshrc:
  • ```
  • if command -v secret-tool &>/dev/null; then
  • export HABITICA_USER_ID=$(secret-tool lookup service habitica env_var HABITICA_USER_ID 2>/dev/null)
  • export HABITICA_API_TOKEN=$(secret-tool lookup service habitica env_var HABITICA_API_TOKEN 2>/dev/null)
  • export TRELLO_DREX_API_KEY=$(secret-tool lookup service trello-drex env_var TRELLO_API_KEY 2>/dev/null)
  • export TRELLO_DREX_TOKEN=$(secret-tool lookup service trello-drex env_var TRELLO_TOKEN 2>/dev/null)
  • export OTTER_EMAIL=$(secret-tool lookup service otter env_var OTTER_EMAIL 2>/dev/null)
  • export OTTER_PASSWORD=$(secret-tool lookup service otter env_var OTTER_PASSWORD 2>/dev/null)
  • fi
  • ```
  • ## 6. Replace plaintext in config
  • Claude Code's MCP config supports ${VAR} expansion. Replace:
  • ```
  • "TRELLO_API_KEY": "04e633dc273440d1df0d5fa6a0e7d873"
  • ```
  • With:
  • ```
  • "TRELLO_API_KEY": "${TRELLO_DREX_API_KEY}"
  • ```
  • The variable is resolved from the process environment at MCP server launch time.
  • ## Security: what this does and doesn't do
  • ### What it fixes
  • - No plaintext secrets in user files (backups, dotfile repos, screen shares,
  • grep results)
  • - Single source of truth for secrets (KeepassXC database, which is encrypted)
  • - Lock KeepassXC and new shells can't retrieve secrets
  • ### What it doesn't fix
  • - Secrets are exported as env vars in every shell session. Any process running
  • as your user can read them via /proc/PID/environ.
  • - If KeepassXC is unlocked, any user-level process can query the Secret Service
  • D-Bus API directly.
  • - This is not meaningfully better against malware running as your user. If you
  • have that problem, env vars and plaintext files are equally compromised.
  • ### Env vars vs. plaintext files: the tradeoff
  • This setup trades one attack surface for another.
  • Plaintext files are worse for storage exposure: they persist on disk, get backed
  • up, synced, committed, grepped, indexed, and shared. The secret survives the
  • session and exists in a form that's easy to accidentally leak.
  • Env vars are worse for runtime exposure: every child process inherits them (not
  • just the ones that need them), they're visible via /proc/PID/environ to anything
  • running as your user, and they can show up in crash dumps or logging frameworks
  • that dump the environment.
  • The core difference: env vars have a broader runtime attack surface, plaintext
  • files have a broader storage attack surface.
  • For a single-user desktop where the realistic threat is accidental file sharing
  • (not local malware), the env var approach wins. If you wanted to eliminate both,
  • you could use a wrapper script that fetches secrets on-demand and only exports
  • them into the MCP server process -- but that's more complexity than most setups
  • warrant.
  • ### The honest assessment
  • On a single-user machine with full disk encryption, the practical security gain
  • is protection against accidental exposure, not against a determined attacker
  • with local access. That's still worth doing -- accidental exposure is far more
  • likely than targeted local compromise for most people.
  • ## Better: on-demand secrets with a wrapper script
  • The .zshrc approach above works, but exports secrets into every shell session. A
  • tighter approach: a wrapper script that fetches secrets on-demand, only in the
  • MCP server process.
  • Save as ~/.local/bin/mcp-secret-launch:
  • ```
  • #!/usr/bin/env python3
  • """Fetch secrets from Secret Service and exec an MCP server."""
  • import os, sys, secretstorage
  • service, command, args = sys.argv[1], sys.argv[2], sys.argv[2:]
  • conn = secretstorage.dbus_init()
  • collection = secretstorage.get_default_collection(conn)
  • if collection.is_locked():
  • print("KeepassXC is locked.", file=sys.stderr)
  • sys.exit(1)
  • for item in collection.search_items({"service": service, "usage": "mcp"}):
  • env_var = item.get_attributes().get("env_var")
  • if env_var:
  • os.environ[env_var] = item.get_secret().decode("utf-8")
  • os.execvp(command, args)
  • ```
  • Make it executable:
  • ```
  • chmod +x ~/.local/bin/mcp-secret-launch
  • ```
  • Then change your MCP config from:
  • ```
  • "habitica": {
  • "command": "npx",
  • "args": ["-y", "habitica-mcp-server"],
  • "env": {
  • "HABITICA_USER_ID": "${HABITICA_USER_ID}",
  • "HABITICA_API_TOKEN": "${HABITICA_API_TOKEN}",
  • "MCP_LANG": "en"
  • }
  • }
  • ```
  • To:
  • ```
  • "habitica": {
  • "command": "/home/you/.local/bin/mcp-secret-launch",
  • "args": ["habitica", "npx", "-y", "habitica-mcp-server"],
  • "env": {
  • "MCP_LANG": "en"
  • }
  • }
  • ```
  • The first arg ("habitica") tells the wrapper which secrets to fetch. It looks up
  • all Secret Service items with service=habitica and usage=mcp, exports them as
  • env vars using the env_var attribute, then execs the real command. Non-secret
  • env vars like MCP_LANG stay in the config as normal.
  • Remove the secret-tool exports from .zshrc -- they're no longer needed.
  • ### What this buys you
  • - Secrets only exist in the MCP server process memory
  • - Nothing in /proc/PID/environ of your shells
  • - No shell startup latency from D-Bus lookups
  • - grep -r on your homedir finds nothing
  • ### What it costs
  • - One extra script to maintain (~15 lines)
  • - MCP launch errors are slightly harder to debug
  • - Python + secretstorage must be available (standard on most desktops)
  • ## Caveats
  • ### KeepassXC must be unlocked
  • If it's locked when an MCP server tries to start, the wrapper exits with an
  • error. With the .zshrc approach, the lookups silently return empty strings --
  • arguably worse since you get no error, just broken auth.
  • ### gnome-keyring will silently intercept your secrets
  • This is the biggest gotcha. If gnome-keyring is running when you call
  • secret-tool store, your secrets go into ~/.local/share/keyrings/ instead of
  • KeepassXC. You won't get an error. You have to check which collection owns the
  • default alias (see the dbus-send verify step above).
  • ### apt remove gnome-keyring will break things
  • Don't do it. Other packages depend on gnome-keyring for non-secrets
  • functionality. Just disable the secrets component as shown above.