Using Proton Pass CLI to Keep Linux Scripts Secure

How to use Proton's new pass-cli tool to inject secrets into scripts and config files at runtime, keeping API keys and passwords out of your dotfiles.

If you manage dotfiles in a public Git repository, you’ve probably faced the dilemma of how to handle secrets. API keys, passwords, and tokens need to live somewhere, but committing them to version control is a security risk.

Proton has recently released a CLI tool for Proton Pass that solves this elegantly. Instead of storing secrets in files, you fetch them at runtime from your encrypted Proton Pass vault.

Installation

The CLI is currently in beta. Install it with:

curl -fsSL https://proton.me/download/pass-cli/install.sh | bash

This installs pass-cli to ~/.local/bin/. Then authenticate:

pass-cli login

This opens a browser for Proton authentication. Once complete, you’re ready to use the CLI.

Basic Usage

List your vaults:

pass-cli vault list

View an item:

pass-cli item view --vault-name "Personal" --item-title "My API Key"

Fetch a specific field:

pass-cli item view --vault-name "Personal" --item-title "My API Key" --field "API Token"

Get JSON output (useful for parsing multiple fields):

pass-cli item view --vault-name "Personal" --item-title "My API Key" --output json

Real-World Example: Wrapper Scripts

I have several tools that need API credentials. Rather than storing these in config files, I created wrapper scripts that fetch credentials from Proton Pass at runtime.

Here’s a wrapper for a TUI application that needs API credentials:

#!/bin/bash
set -e

PASS_CLI="$HOME/.local/bin/pass-cli"

# Fetch credentials from Proton Pass (JSON for single API call)
CREDS_JSON=$("$PASS_CLI" item view --vault-name "Personal" --item-title "My API" --output json)

if [ -z "$CREDS_JSON" ]; then
    echo "Error: Failed to fetch credentials from Proton Pass" >&2
    echo "Make sure you're logged in: pass-cli login" >&2
    exit 1
fi

# Parse fields from JSON
API_USER=$(echo "$CREDS_JSON" | jq -r '.item.content.content.Custom.sections[0].section_fields[] | select(.name == "Username") | .content.Text')
API_TOKEN=$(echo "$CREDS_JSON" | jq -r '.item.content.content.Custom.sections[0].section_fields[] | select(.name == "Token") | .content.Hidden')

# Export and run
export API_USER API_TOKEN
exec my-app "$@"

The key insight: fetching JSON once and parsing with jq is faster than making separate API calls for each field.

Adding Credential Caching

The Proton Pass API call takes a few seconds. For frequently-used tools, this adds noticeable latency. The solution is to cache credentials in the Linux kernel keyring:

#!/bin/bash
set -e

PASS_CLI="$HOME/.local/bin/pass-cli"
CACHE_TTL=3600  # 1 hour
KEY_USER="myapp_user"
KEY_TOKEN="myapp_token"

# Try to get from kernel keyring cache
get_cached() {
    local key_id
    key_id=$(keyctl search @u user "$1" 2>/dev/null) || return 0
    keyctl pipe "$key_id" 2>/dev/null
}

# Store in kernel keyring with TTL
cache_credential() {
    local key_id
    key_id=$(keyctl add user "$1" "$2" @u)
    keyctl timeout "$key_id" "$CACHE_TTL"
}

# Try cache first
API_USER=$(get_cached "$KEY_USER")
API_TOKEN=$(get_cached "$KEY_TOKEN")

# If not cached, fetch from Proton Pass
if [ -z "$API_USER" ] || [ -z "$API_TOKEN" ]; then
    CREDS_JSON=$("$PASS_CLI" item view --vault-name "Personal" --item-title "My API" --output json)

    API_USER=$(echo "$CREDS_JSON" | jq -r '.item.content.content.Custom.sections[0].section_fields[] | select(.name == "Username") | .content.Text')
    API_TOKEN=$(echo "$CREDS_JSON" | jq -r '.item.content.content.Custom.sections[0].section_fields[] | select(.name == "Token") | .content.Hidden')

    # Cache for next time
    cache_credential "$KEY_USER" "$API_USER"
    cache_credential "$KEY_TOKEN" "$API_TOKEN"
fi

export API_USER API_TOKEN
exec my-app "$@"

With caching:

  • First run: ~5-6 seconds (fetches from Proton Pass)
  • Subsequent runs: ~0.01 seconds (from kernel keyring)

The cache expires after one hour, or when you log out. Clear it manually with:

keyctl purge user myapp_user
keyctl purge user myapp_token

Built-in Secret Injection

The CLI also has built-in commands for secret injection. The run command passes secrets as environment variables:

pass-cli run --env-file .env.template -- ./my-script.sh

The inject command processes template files:

pass-cli inject -i config.template -o config.conf

These use a URI syntax: pass://vault/item/field to reference secrets.

Updating Config Files

For applications that read credentials from config files (like WeeChat’s sec.conf), the wrapper can update the file before launching:

#!/bin/bash
set -e

SEC_CONF="$HOME/.config/myapp/secrets.conf"

# Fetch password from Proton Pass
PASSWORD=$("$HOME/.local/bin/pass-cli" item view --vault-name "Personal" --item-title "My Service" --field password)

# Update config file
sed -i "s/^password = \".*\"/password = \"$PASSWORD\"/" "$SEC_CONF"

exec myapp "$@"

SSH Agent Integration

The CLI can also act as an SSH agent, loading keys stored in Proton Pass:

pass-cli ssh-agent --help

This is useful if you store SSH private keys in your vault.

Security Considerations

This approach keeps secrets out of your dotfiles repository entirely. The wrapper scripts reference Proton Pass item names, not actual credentials. Your secrets remain encrypted in Proton’s infrastructure and are only decrypted locally when needed.

The kernel keyring cache is per-user and lives only in memory. It’s cleared on logout or reboot, and the TTL ensures credentials don’t persist indefinitely.

For public dotfiles repositories, this is a clean solution: commit your wrapper scripts freely, keep your secrets in Proton Pass.