Skip to content
Fran Gonzalez
← Back to blog
(updated Sep 30, 2026)·Clanker·4 min read

Two Proton Pass CLI PAT Gotchas: --pat Argv Exposure and Rotation Limits

pass-cli login --pat exposes the token in argv and shell history, PAT-authenticated sessions cannot manage PATs, and a rotated token that never reaches the Keychain silently breaks auto-renewal.

Some matmuls wrote this slop, sorry. My goal with this content is to document some work I (a real human bean) do while poking the Clanker, and try to learn something along the way.

I audited whether the Keychain-backed PAT renewal I set up in August still worked after weeks of re-login prompts. The shell plumbing was intact, but its Keychain credential was stale: an earlier pat renew had invalidated the stored token without a matching Keychain update. The helper kept reading a dead credential while I pasted the live one by hand.

Note

Tested with Proton Pass CLI 2.4.1 on macOS on 2026-09-20.

I had typed pass-cli login --pat <token> manually 19 times, and zsh had kept every token in plaintext.

Usage

--pat places the token in the command’s argv, visible to ps while the login runs, and in shell history permanently:

pass-cli login --pat 'pst_…::…'

The credential-provider variable avoids argv and durable history exposure. This helper briefly holds the token in a non-exported local shell variable, copies it into the login process’s environment, then unsets it:

proton_pass_login() {
  local pat status
  pat="$(security find-generic-password -a "$USER" -s "dev-mamini-pat" -w)" || return
  PROTON_PASS_PERSONAL_ACCESS_TOKEN="$pat" pass-cli login
  status=$?
  unset pat
  return "$status"
}

unset limits the variable’s lifetime but does not guarantee that the shell erases its process memory. Same-user processes can also inspect the login child’s environment while it runs.

pass-cli login provides no interactive prompt for tokens: --pat requires the value as a command-line argument (--pat with no value is an argument-parsing error), and login --interactive covers username and password, not tokens. The credential-provider variable is the supported prompt-free channel.

To stop hitting the two-hour server-side session expiry outside my wrapper functions, I wrapped the CLI itself (trimmed; the real one guards on command -v and warns when it sees --pat). login, logout, and pat pass through so an expired session never blocks the password login that rotation needs:

# ~/.config/shell/common.d/40-proton-pass.sh
pass-cli() {
  case "${1:-}" in
    login | logout | pat) ;;
    *) proton_pass_login_ensure ;;
  esac
  command pass-cli "$@"
}

The second gotcha: PAT sessions cannot manage PATs.

$ pass-cli pat list
Error: Failed to list personal access tokens

Caused by:
    Cannot manage or act on personal access tokens while logged in with a
    personal access token or agent session

pat renew is the shortest rotation path: it prints a new token string, keeps the name and every granted vault access, and the old token stops working immediately. Rotating after a leak means pass-cli logout, one interactive password login (PAT sessions cannot manage PATs), pass-cli pat renew --pat-name "dev-mamini-pat" --expiration 6m, then security add-generic-password -U … -w to refresh the Keychain item. That last step is the one that matters: a renewed token that never reaches the Keychain leaves the helper reading a dead credential, which is exactly how I ended up pasting tokens into argv.

Run interactively, security prompts for the value on the terminal, hidden, with a retype (it only accepts a piped value when no controlling TTY exists). So I wrapped the update in a small proton_pass_pat_store helper: paste at security’s own prompts, then it checks that the stored value looks like pst_…::…, stops the SSH agent when necessary, ends the password session, authenticates with the stored PAT, and only then reports success. The pat delete + pat create path also works but re-grants access from scratch.

Notes

  • Purging history requires replacing the file, not printing filtered lines. I closed every other shell, then used a same-directory temporary file for an atomic replacement with owner-only permissions:

    tmp="$(mktemp "$HOME/.zsh_history.clean.XXXXXX")"
    grep -av -- "pass-cli login --pat" "$HOME/.zsh_history" >"$tmp" &&
      chmod 600 "$tmp" &&
      mv "$tmp" "$HOME/.zsh_history"
    unset tmp
    exec zsh
  • By cleanup time the server rejected both leaked values (“invalid, expired or has been deleted”), so the purge was hygiene rather than an emergency. A backup still contains the values, even when they are dead, and should be deleted after verification.

  • Environment variables stay readable by same-user processes while the login runs (ps -E on macOS). Argv and history are the durable leaks.

  • Wrapper internals must call command pass-cli, or the wrapper recurses into itself.

References

This post was written with AI assistance.