From 604c1c653828e310b322dabc94be4806337d0f0f Mon Sep 17 00:00:00 2001 From: Fabian Ising Date: Wed, 14 Oct 2026 09:55:14 +0200 Subject: [PATCH] [TMUX] Paste 1Password passwords with prefix+P Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- setup_mac.sh | 2 +- tmux/.mac_config | 4 ++ tmux/op_password_paste.zsh | 92 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100755 tmux/op_password_paste.zsh diff --git a/AGENTS.md b/AGENTS.md index 54dc287..dddc57e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Practical rule: **edit the `.example` source, not the generated symlink target.* - **zsh** (`zsh/.zshrc`): Powerlevel10k instant prompt + [antidote](https://github.com/mattmc3/antidote) plugin manager (plugin list in `zsh/.zsh_plugins.txt`). `.zshrc` sources optional layered configs if present: `.powerline_config`, `.user_config`, `.virtual_env_config.zsh`, `.mac_config.zsh`, `.local_config`. `update_dotfiles()` auto-`git pull`s this repo in the background on every shell start. Machine-local overrides belong in `.local_config` (not committed). - **nvim** (`nvim/init.vim`): [vim-plug](https://github.com/junegunn/vim-plug), auto-bootstrapped on first launch (`:PlugInstall`). Python provider points at the `~/python-envs/neovim` virtualenv on Apple Silicon. `colors/` holds a large bundled colorscheme collection. -- **tmux** (`tmux/.tmux.conf`): TPM-style `plugins/`. `tmux/tmux_attach.sh` is an attach helper. +- **tmux** (`tmux/.tmux.conf`): TPM-style `plugins/`. `tmux/tmux_attach.sh` is an attach helper. On macOS, `.mac_config` binds `prefix+P` to `tmux/op_password_paste.zsh` (pastes a 1Password password into the pane, e.g. remote sudo; needs the `op` CLI with app integration). - **alacritty** (`.config/alacritty/`): active theme selected via the `import = [...]` line in `alacritty.toml`. - **gitconfig** (`gitconfig/gitconfig`): identity/behavior split by remote host using `includeIf "hasconfig:remote.*.url:..."` — `github`, `fhms` (fh-muenster), `murgi`, and `sit` (Fraunhofer) each load their own `*.gitconfig`. Change per-context email/signing in the matching include file, not the base config. diff --git a/setup_mac.sh b/setup_mac.sh index d174865..634ca41 100755 --- a/setup_mac.sh +++ b/setup_mac.sh @@ -7,7 +7,7 @@ # brew install neovim tmux coreutils cmake golang jq npm prettier virtualenvwrapper fzf git-delta -brew install --cask font-fira-mono-nerd-font +brew install --cask font-fira-mono-nerd-font 1password-cli ./install_alacritty_mac.sh BREW_PREFIX="$(brew --prefix)" source "$BREW_PREFIX/bin/virtualenvwrapper.sh" diff --git a/tmux/.mac_config b/tmux/.mac_config index 00bd77a..218ad87 100644 --- a/tmux/.mac_config +++ b/tmux/.mac_config @@ -1,4 +1,8 @@ set-option -g default-shell /bin/zsh + +# prefix+P: paste a password from 1Password into the pane (e.g. remote sudo) +# (via run-shell: display-popup does not expand formats in its command) +bind P run-shell -b "tmux display-popup -c '#{client_name}' -E -w 80% -h 50% -T ' 1Password ' '$HOME/.tmux/op_password_paste.zsh #{pane_id} #{client_name}'" #bind -Tcopy-mode-vi y send -X copy-pipe "pbcopy" \; display-message "copied to system clipboard" #bind C-p run "tmux set-buffer \"$pbpaste\"; tmux paste-buffer" #bind C-y run "tmux save-buffer - | pbcopy" diff --git a/tmux/op_password_paste.zsh b/tmux/op_password_paste.zsh new file mode 100755 index 0000000..e92dd21 --- /dev/null +++ b/tmux/op_password_paste.zsh @@ -0,0 +1,92 @@ +#!/bin/zsh +# Paste a password from 1Password into a tmux pane, e.g. at a remote sudo +# prompt. Run in a popup (see .mac_config): +# op_password_paste.zsh [client_name] +emulate -L zsh +setopt extended_glob pipe_fail no_unset + +pane=${1:?usage: op_password_paste.zsh [client_name]} +client=${2:-} +# -l: messages may contain item titles, which must not be expanded as formats +die() { + print -ru2 -- "1Password: $*" + tmux display-message -l ${client:+-c} $client "1Password: $*" + # keep the popup (and op's error output) visible until a key is pressed + [[ -t 0 ]] && read -sk1 "?Press any key to close." + exit 1 +} + +(( $+commands[op] )) || die "op CLI missing (brew install --cask 1password-cli)" + +# Guard: a paste outside a password prompt echoes the secret (and may run it). +# The cursor line must end in a prompt like "[sudo] password for x:". +cursor_line() { + local pos=$(tmux display-message -p -t $pane '#{cursor_x},#{cursor_y}') + print -r -- "$pos $(tmux capture-pane -p -t $pane -S ${pos#*,} -E ${pos#*,})" +} +prompt=$(cursor_line) +if [[ ${prompt#* } != *(#i)(password|passphrase|passwort|kennwort)*:[[:space:]]# ]]; then + print -r -- "No password prompt at the cursor:" + print -r -- " ${${prompt#* }:-}" + read -q "?Paste anyway? [y/N] " || exit 0 + print +fi + +# Destination of an ssh running in the pane, used as the initial fzf query. +ssh_host() { + local -a words=(${(z)1}) + local w i + [[ ${words[1]:t} == ssh ]] || return 1 + for (( i = 2; i <= $#words; i++ )); do + w=${words[i]} + if [[ $w == -- ]]; then + w=${words[i+1]:-} + break + elif [[ $w == -?* ]]; then + # option letters taking an argument; the last letter may take the next word + [[ $w == -[^BbcDEeFIiJLlmOoPpQRSWw]#[BbcDEeFIiJLlmOoPpQRSWw] ]] && (( i++ )) + else + break + fi + done + w=${${${w#ssh://}#*@}%%/*} + w=${w%:[0-9]##} + [[ -n $w ]] && print -r -- $w +} + +host= +tty=$(tmux display-message -p -t $pane '#{pane_tty}') +for args in ${(f)"$(ps -o args= -t ${tty#/dev/} 2>/dev/null)"}; do + host=$(ssh_host $args) && break +done + +# All signed-in accounts; the desktop app integration asks for Touch ID. +accounts=(${(f)"$(op account list --format json | jq -r '.[].account_uuid')"}) || + die "op account list failed" +(( $#accounts )) || die "no 1Password account (enable CLI integration in the app)" +rows=$(for acc in $accounts; do + op item list --account $acc --categories Login,Password,Server --format json | + jq -r --arg acc $acc '.[] | [$acc, .vault.id, .id, .title, + (.additional_information // ""), .vault.name, + ([.urls[]?.href] | join(" "))] | @tsv' || exit 1 +done) || die "op item list failed for an account" +[[ -n $rows ]] || die "no Login/Password/Server items found" + +# The prefill comes from the local ssh; after another hop it names the wrong host. +choice=$(print -r -- $rows | fzf --delimiter='\t' --with-nth=4.. --reverse \ + --query=$host --prompt='password> ' \ + --header="paste into $pane${host:+ (local ssh: $host)}") || exit 0 +fields=("${(@ps:\t:)choice}") + +pw=$(op read --account $fields[1] "op://$fields[2]/$fields[3]/password") || + die "no 'password' field in ${fields[4]}" + +# Named buffer, deleted by paste-buffer -d; no clipboard (no load-buffer -w). +buf=op-password-$$ +trap 'tmux delete-buffer -b $buf 2>/dev/null' EXIT +print -rn -- $pw | tmux load-buffer -b $buf - +pw= +# Picking and Touch ID take a while: refuse if the prompt moved meanwhile +# (sudo timeout, dropped connection). +[[ "$(cursor_line)" == "$prompt" ]] || die "pane changed since the check, not pasted" +tmux paste-buffer -d -b $buf -t $pane