Skip to content

Instantly share code, notes, and snippets.

@mietzen
Last active November 1, 2025 17:52
Show Gist options
  • Save mietzen/f383d87c0973c1af877b39e09b50021e to your computer and use it in GitHub Desktop.
Save mietzen/f383d87c0973c1af877b39e09b50021e to your computer and use it in GitHub Desktop.
How to use use Bitwarden CLI with macOS Touch ID

How to use Bitwarden CLI with macOS Touch ID

If you want to use Bitwarden CLI for ssh have a look at: How to use use Bitwarden CLI for SSH-Keys in macOS

Wirtten and tested on macOS Ventura

Configure Touch ID for the sudo command

To allow Touch ID on your Mac to authenticate you for sudo access instead of a password you need to do the following.

  • Open Terminal
  • Switch to the root user with: sudo -i
  • Edit /etc/pam.d/sudo:
nano /etc/pam.d/sudo

The contents of this file should look like this:

# sudo: auth account password session
auth       sufficient     pam_smartcard.so
auth       required       pam_opendirectory.so
account    required       pam_permit.so
password   required       pam_deny.so
session    required       pam_permit.so
  • You need to add an additional auth line to the top:

auth sufficient pam_tid.so

  • So it now looks like this:
# sudo: auth account password session
auth       sufficient     pam_tid.so
auth       sufficient     pam_smartcard.so
auth       required       pam_opendirectory.so
account    required       pam_permit.so
password   required       pam_deny.so
session    required       pam_permit.so
  • Save the file with ctrl o and exit with crtl x

  • Try to use sudo, and you should be prompted to authenticate with Touch ID.

Source: https://apple.stackexchange.com/a/306324/409134

Get bw to use Touch ID (via sudo)

  • Add the following line to your .zshrc with: nano ~/.zshrc
export BW_USER='<YOUR-USER>'

bw() {
  bw_exec=$(sh -c "which bw")
  local -r bw_session_file='/var/root/.bitwarden.session' # Only accessible as root

  _read_token_from_file() {

    local -r err_token_not_found="Token not found, please run bw --regenerate-session-key"
    case $1 in
    '--force')
      unset bw_session
      ;;
    esac

    if [ "$bw_session" = "$err_token_not_found" ]; then
      unset bw_session
    fi

    # If the session key env variable is not set, read it from the file
    # if file it not there, ask user to regenerate it

    if [ -z "$bw_session" ]; then
      bw_session="$(
        sh -c "sudo cat $bw_session_file 2> /dev/null"
        # shellcheck disable=SC2181
        if [ "$?" -ne "0" ]; then
          echo "$err_token_not_found"
          sudo -k # De-elevate privileges
          exit 1
        fi
        sudo -k # De-elevate privileges
      )"

      # shellcheck disable=SC2181
      if [ "$bw_session" = "$err_token_not_found" ]; then
        echo "$err_token_not_found"
        return 1
      fi
    fi
  }

  case $1 in
  '--regenerate-session-key')
    echo "Regenerating session key, this has invalidated all existing sessions..."
    sudo rm -f /var/root/.bitwarden.session && ${bw_exec} logout 2>/dev/null # Invalidate all existing sessions

    ${bw_exec} login "${BW_USER}" --raw | sudo tee /var/root/.bitwarden.session &>/dev/null # Generate new session key

    _read_token_from_file --force # Read the new session key for immediate use
    sudo -k                       # De-elevate privileges, only doing this now so _read_token_from_file can resuse the same sudo session
    ;;

  'login' | 'logout' | 'config')
    ${bw_exec} "$@"
    ;;


  '--help' | '-h' | '')
    ${bw_exec} "$@"
    echo "To regenerate your session key type:"
    echo "  bw --regenerate-session-key"
    ;;

  *)
    _read_token_from_file

    ${bw_exec} "$@" --session "$bw_session"
    ;;
  esac
}
  • Then run: exec zsh and bw --regenerate-session-key

If you logout of bitwarden cli again you have to generate a new sessionkey! This might be usefull when traveling internationally.

Now you're good to go! Use with e.g.:

bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412

image

The default sudo timout will be applied (Change sudo timeout)

Edits:

27.08.2023: Updated the help menu, credits to Moulick

10.09.2023: Don't keep elevated rights in Terminal, credits to Moulick

@Moulick
Copy link

Moulick commented Aug 27, 2023

Though I get prompted for touch ID, I still get asked for my master password and 2FA 😢 Is there a way to not have to input master password/2fa everytime?

@mietzen
Copy link
Author

mietzen commented Aug 27, 2023

Though I get prompted for touch ID, I still get asked for my master password and 2FA 😢

Does is ask for Touch ID if you type e.g. sudo ls in terminal?

@Moulick
Copy link

Moulick commented Aug 27, 2023

Nope, once I run --regenerate-session-key in a session, sudo is prompted once but then never again for all other sudo commands

@Moulick
Copy link

Moulick commented Aug 27, 2023

I see that the function above is calling ${bw_exec} logout &> /dev/null, would that not invalidate all sessions and force master password/2fa input everytime?

@Moulick
Copy link

Moulick commented Aug 27, 2023

ah, I completely mis understood the use of the function above. Sorry! Simply running bw list items on a new terminal is prompting me for sudo only, running bw --regenerate-session-key will asks for master password. Which is what you wrote it to do. I thought we had to run --regenerate-session-key in every terminal/script once before bw get item

But absolutely brilliant workaround!! Hats off to you ❤️

@mietzen
Copy link
Author

mietzen commented Aug 27, 2023

ah, I completely mis understood the use of the function above. Sorry! Simply running bw list items on a new terminal is prompting me for sudo only, running bw --regenerate-session-key will asks for master password. Which is what you wrote it to do. I thought we had to run --regenerate-session-key in every terminal/script once before bw get item

I'm glad it worked!

But absolutely brilliant workaround!! Hats off to you ❤️

Thanks ❤️

@Moulick
Copy link

Moulick commented Aug 27, 2023

Cleaned up the function a little, courtesy of shellcheck https://github.com/koalaman/shellcheck, namely quoting to prevent splitting and globing and combining the --help, -h

bw() {
    bw_exec=$(sh -c "which bw")
    case $1 in
    '--regenerate-session-key')
        ${bw_exec} logout &> /dev/null
        ${bw_exec} login "${BW_USER}" --raw | sudo tee /var/root/.bitwarden.session &> /dev/null
        ;;

    '--help'|'-h')
        ${bw_exec} "$@"
        echo "To regenerate your session key type:"
        echo "  bw --regenerate-session-key"
        ;;

    *)
        ${bw_exec} "$@" --session "$(sudo cat /var/root/.bitwarden.session)"
        ;;
    esac
}

@mietzen
Copy link
Author

mietzen commented Aug 27, 2023

Cleaned up the function a little, courtesy of shellcheck https://github.com/koalaman/shellcheck, namely quoting to prevent splitting and globing and combining the --help, -h

bw() {
    bw_exec=$(sh -c "which bw")
    case $1 in
    '--regenerate-session-key')
        ${bw_exec} logout &> /dev/null
        ${bw_exec} login "${BW_USER}" --raw | sudo tee /var/root/.bitwarden.session &> /dev/null
        ;;

    '--help'|'-h')
        ${bw_exec} "$@"
        echo "To regenerate your session key type:"
        echo "  bw --regenerate-session-key"
        ;;

    *)
        ${bw_exec} "$@" --session "$(sudo cat /var/root/.bitwarden.session)"
        ;;
    esac
}

Thanks, I updated the gist 👍

@Moulick
Copy link

Moulick commented Aug 27, 2023

I wonder if there is a way to authorize only bitwarden with sudo. As right now, once you run bw get item and authorize with touchID, the whole terminal gets elevated to sudo :(

@mietzen
Copy link
Author

mietzen commented Aug 27, 2023

bw() {
    bw_exec=$(sh -c "which bw")
    case $1 in
    '--regenerate-session-key')
        ${bw_exec} logout &> /dev/null
        ${bw_exec} login "${BW_USER}" --raw | sudo tee /var/root/.bitwarden.session &> /dev/null
        ;;

    '--help'|'-h')
        ${bw_exec} "$@"
        echo "To regenerate your session key type:"
        echo "  bw --regenerate-session-key"
        ;;

    *)
        ${bw_exec} "$@" --session "$(sudo cat /var/root/.bitwarden.session)"
        ;;
    esac
}

I find no temporary way, only the globally: https://unix.stackexchange.com/questions/382060/change-default-sudo-password-timeout

@Moulick
Copy link

Moulick commented Aug 27, 2023

This now caches the token into an env var rather than reading from the file. Which means we only need to read the file once per session. After read/writing to the file, sudo is revoked with sudo -k. Then I added some error handling for different situations. Now this works in all cases, file missing, env var missing etc.

bw() {
  bw_exec=$(sh -c "which bw")
  local -r bw_session_file='/var/root/.bitwarden.session' # Only accessible as root

  _read_token_from_file() {

    local -r err_token_not_found="Token not found, please run bw --regenerate-session-key"
    case $1 in
    '--force')
      unset bw_session
      ;;
    esac

    if [ "$bw_session" = "$err_token_not_found" ]; then
      unset bw_session
    fi

    # If the session key env variable is not set, read it from the file
    # if file it not there, ask user to regenerate it

    if [ -z "$bw_session" ]; then
      bw_session="$(
        sh -c "sudo cat $bw_session_file 2> /dev/null"
        # shellcheck disable=SC2181
        if [ "$?" -ne "0" ]; then
          echo "$err_token_not_found"
          sudo -k # De-elevate privileges
          exit 1
        fi
        sudo -k # De-elevate privileges
      )"

      # shellcheck disable=SC2181
      if [ "$bw_session" = "$err_token_not_found" ]; then
        echo "$err_token_not_found"
        return 1
      fi
    fi
  }

  case $1 in
  '--regenerate-session-key')
    echo "Regenerating session key, this has invalidated all existing sessions..."
    sudo rm -f /var/root/.bitwarden.session && ${bw_exec} logout 2>/dev/null # Invalidate all existing sessions

    ${bw_exec} login "${BW_USER}" --raw | sudo tee /var/root/.bitwarden.session &>/dev/null # Generate new session key

    _read_token_from_file --force # Read the new session key for immediate use
    sudo -k                       # De-elevate privileges, only doing this now so _read_token_from_file can resuse the same sudo session
    ;;

  '--help' | '-h' | "")
    ${bw_exec} "$@"
    echo "To regenerate your session key type:"
    echo "  bw --regenerate-session-key"
    ;;

  *)
    _read_token_from_file

    ${bw_exec} "$@" --session "$bw_session"
    ;;
  esac
}

@andrzejnovak
Copy link

Thanks a lot! Just to chime in this also works on WSL with https://github.com/nullpo-head/WSL-Hello-sudo

@zestysoft
Copy link

Just wanted to give a heads up that macos has a new /etc/pam.d/sudo_local file that you should edit instead. The settings will stick across updates.

This is the contents of that file for myself:

auth       optional       /opt/homebrew/lib/pam/pam_reattach.so ignore_ssh
auth       sufficient     pam_tid.so

The reattach is required if you use iterm with tmux. you can use brew install pam-reattach to get the .so file.
Also that path assumes an apple silicon device. I think it differs for intel system.

It's unfortunate bw can't authenticate the way the one password cli does -- just prompt for touch id, no passwords or mfa.

@Opa-
Copy link

Opa- commented Dec 18, 2024

Thanks for this gist, here's a Fish shell variant if anyone's interested https://gist.github.com/Opa-/b828995590ca79e653a01c63bbaca64f

@gh10ltz
Copy link

gh10ltz commented Mar 25, 2025

Thanks for the starting point. I've made a few changes to this which include:

feat(zsh): harden Bitwarden CLI wrapper — Keychain storage, env-scoped sessions, arg sanitization, fail-fast

Summary (before → after):

  • Session storage: root file /var/root/.bitwarden.session via sudo cat → macOS Keychain item BW_SESSION (account root)
  • Token propagation: --session "$bw_session" in argv → BW_SESSION=… in the environment
  • Behavior: plain pass-through → optional sync+<subcommand> chain; guarded and silent
  • Error handling: best-effort → fail-fast with clear messages and exit codes

Details:

  • Replace root-owned plaintext session file with macOS Keychain

    • Read via security find-generic-password -a root -s BW_SESSION -w under sudo
    • Regeneration deletes the old item, logs out, performs interactive login, then stores the new session
    • Suppress security stdout/stderr noise; reset the sudo timestamp after each privileged call
    • Clear the transient new_session variable after persisting
  • Keep secrets out of argv and sanitize user input

    • Switch from --session "$bw_session" to BW_SESSION="$bw_session" when invoking bw
    • Strip any user-supplied --session/--session=* to prevent override or leakage
    • Build arguments with arrays to preserve whitespace and avoid word-splitting hazards
  • Robust command resolution and defensive defaults

    • Resolve the real bw binary via builtin whence -p with fallback to command -v; bail with 127 if not found
    • Quote all expansions and rebuild arg lists safely
    • Fail-fast on token read, sync, and login errors; emit actionable messages
    • Require BW_USER for --regenerate-session-key and stop early if missing
  • UX and ergonomics

    • Add sync+<subcommand>: perform a silent bw sync once, then run the provided subcommand with the same session
    • Guard empty sync+ usage and display a concise usage hint
    • Preserve existing passthrough behavior for login|logout|config|--help|-h|""
    • Keep the regeneration help hint for discoverability

Security impact:

  • Eliminates plaintext session file and the use of sudo cat/tee on secrets
  • Removes session tokens from command-line arguments, reducing exposure in logs and history
  • Minimizes sudo exposure by resetting timestamps after privileged operations
  • Notes: the session is still present in the child process environment while bw runs, and security add-generic-password -w "$new_session" passes the secret in security’s argv briefly; both are time- and scope-limited and standard for CLI flows

Behavioral changes and compatibility:

  • --session flags provided by users are now ignored (sanitized)
  • --regenerate-session-key requires BW_USER to be set
  • New sync+… convenience path; other commands remain unchanged

Testing hints:

  • bw --help (unchanged)
  • bw --regenerate-session-key then bw list items (new session via Keychain)
  • bw sync+list items (sync once, then run the command with the same session)
bw() {
  local bw_exec
  bw_exec=$(builtin whence -p bw 2>/dev/null) || bw_exec=$(command -v bw 2>/dev/null)
  if [ -z "$bw_exec" ]; then
    printf '%s\n' "bw not found in PATH" >&2
    return 127
  fi
  local bw_session

  _read_token_from_file() {
    local -r err_token_not_found="Token not found, please run bw --regenerate-session-key"
    case $1 in
    '--force')
      unset bw_session
      ;;
    esac

    if [ -z "$bw_session" ]; then
      bw_session="$(sudo security find-generic-password -a root -s BW_SESSION -w 2>/dev/null)"
      local rc=$?
      sudo --reset-timestamp
      if [ $rc -ne 0 ] || [ -z "$bw_session" ]; then
        printf '%s\n' "$err_token_not_found" >&2
        return 1
      fi
    fi
  }

  if [[ "$1" == "sync+"* ]]; then
    local command="${1#sync+}"
    if [ -z "$command" ]; then
      printf '%s\n' "Usage: bw sync+<subcommand> [args...]" >&2
      return 2
    fi
    shift
    _read_token_from_file || return $?
    BW_SESSION="$bw_session" "$bw_exec" sync >/dev/null 2>&1 || return $?
    local -a args=("$command")
    while [ $# -gt 0 ]; do
      case "$1" in
        --session) shift 2; continue ;;
        --session=*) shift; continue ;;
      esac
      args+=("$1")
      shift
    done
    BW_SESSION="$bw_session" "$bw_exec" "${args[@]}"
    return
  fi

  case $1 in
  '--regenerate-session-key')
    sudo security delete-generic-password -a root -s BW_SESSION >/dev/null 2>&1
    "$bw_exec" logout 2>/dev/null
    if [ -z "${BW_USER:-}" ]; then
      printf '%s\n' "BW_USER is not set" >&2
      return 1
    fi
    local new_session
    new_session=$("$bw_exec" login "$BW_USER" --raw) || return $?
    sudo security add-generic-password -U -a root -s BW_SESSION -w "$new_session" >/dev/null 2>&1
    unset new_session
    _read_token_from_file --force || return $?
    sudo --reset-timestamp
    ;;
  login|logout|config)
    "$bw_exec" "$@"
    ;;
  --help|-h|'')
    "$bw_exec" "$@"
    printf '%s\n' "To regenerate your session key type:"
    printf '%s\n' "  bw --regenerate-session-key"
    ;;
  *)
    _read_token_from_file || return $?
    local -a args=()
    while [ $# -gt 0 ]; do
      case "$1" in
        --session) shift 2; continue ;;
        --session=*) shift; continue ;;
      esac
      args+=("$1")
      shift
    done
    BW_SESSION="$bw_session" "$bw_exec" "${args[@]}"
    ;;
  esac
}
sudo() {
  unset -f sudo
  if [[ "$(uname)" == 'Darwin' ]] && ( ! grep -q '^auth sufficient pam_tid.so' /etc/pam.d/sudo_local || 
                                       ! grep -q '^auth requisite pam_deny.so' /etc/pam.d/sudo_local || 
                                       [ $(grep -c '^auth ' /etc/pam.d/sudo_local) -ne 2 ] ); then
    sudo cp /etc/pam.d/sudo_local /etc/pam.d/sudo_local.bak 2>/dev/null
    echo "# sudo_local: local config file which survives system update and is included for sudo" | sudo tee /etc/pam.d/sudo_local > /dev/null
    echo "auth sufficient pam_tid.so" | sudo tee -a /etc/pam.d/sudo_local > /dev/null
    echo "auth requisite pam_deny.so" | sudo tee -a /etc/pam.d/sudo_local > /dev/null
  fi
  sudo "$@"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment