>_ZSH-SUDO
One keybind toggles sudo on the current command line. Sudo absent — prepend it. Sudo already there — strip it along with builtin / command / env and their args. Empty line — pull the last command from history and slap sudo on it.
Install
# Zinit
zinit light MenkeTechnologies/zsh-sudo
# Oh My Zsh
git clone https://github.com/MenkeTechnologies/zsh-sudo \
"${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-sudo"
# then: plugins+=(zsh-sudo)
# Manual
git clone https://github.com/MenkeTechnologies/zsh-sudo
source zsh-sudo/sudo.plugin.zsh
Key binding
Wire the widget to whatever combo you prefer:
bindkey '^N' sudo-command-line # Ctrl-N # or the classic Esc-Esc pattern: bindkey '\e\e' sudo-command-line
Configuration
Override the prepended command (e.g. doas) by setting both variables to the same string before the plugin loads:
export ZPWR_SUDO_REGEX='doas' export ZPWR_SUDO_CMD='doas'
ZPWR_SUDO_CMD is prepended; ZPWR_SUDO_REGEX is what the parser hunts for when stripping. Both default to sudo and are only set when unset, so they never clobber a user override.
The two vars are decoupled so the strip side can use alternation while the prepend side picks one canonical tool. Strip either sudo or doas but always prepend sudo:
export ZPWR_SUDO_CMD='sudo' export ZPWR_SUDO_REGEX='sudo|doas'
ZPWR_SUDO_CMD | String prepended when there's nothing to strip. Default sudo. |
ZPWR_SUDO_REGEX | Regex fragment the strip side hunts for. Default sudo. |
State table
Three behaviours, decided in this order: empty line → history recall; line already has a sudo prefix → strip; otherwise → prepend.
apt update | → sudo apt update |
sudo apt update | → apt update |
| (empty line) | → sudo <last command from history> |
The regex engine handles quoted commands, builtin/command prefixes, env with flags, leading variable assignments, and stacked sudo options (-u root -E, etc).
Worked examples
Every row below corresponds to a pinned test case in tests/t-sudo-command-line.zsh — the toggle is verified, not hypothetical.
ls -la | → sudo ls -la |
sudo -u root ls | → ls (value flag dropped) |
sudo -EH ls | → ls (stacked boolean flags) |
FOO=bar BAZ=qux sudo ls | → ls (leading var-assignments dropped) |
command sudo ls | → ls (wrapper word dropped) |
builtin command sudo ls | → ls |
sudo env -i bash | → bash (env wrapper dropped) |
sudo sudo sudo ls | → ls (stacked sudo collapsed) |
\\sudo ls | → ls (backslash-quoted sudo) |
sudo ls | → ls (leading whitespace preserved) |
sudoers ls | → sudo sudoers ls (no false strip) |
The strip path only fires when a real sudo token is present — a command word that merely starts with sudo (like sudoers) is left alone and gets sudo prepended instead.
Cursor position
ZLE splits the command line at the cursor into $LBUFFER (left of cursor) and $RBUFFER (right of cursor). The widget tries the strip regex against $LBUFFER first, then falls through to $RBUFFER. Because the rewrite happens on whichever side matched — not the whole line — toggling works whether the cursor is at the end of the line, at column 0 (after ^A), or anywhere a clean sudo prefix sits on one side. Hit the chord twice and the line round-trips to its original state.
Notes
- The entrypoint is
sudo.plugin.zsh(notzsh-sudo.plugin.zsh) so oh-my-zsh's stockplugins/sudoauto-load can find it. - The plugin registers the widget with
zle -N sudo-command-linebut does not bind a key — the chord is yours to choose. - Empty-line recall uses
builtin fc -ln -1, which prints the previous history entry with no line number and no trailing newline;builtinguards against a user-aliasedfc. - The buffer is rewritten as inert string data, never
eval'd: glob tokens stay literal, command substitutions are not run at toggle time, and multibyte UTF-8 commands round-trip unchanged. - No external dependencies — the widget runs entirely inside zsh's match engine. Identical behaviour on macOS, Linux, BSD, and WSL.
Troubleshooting
| Chord does nothing | You haven't bound a key — the plugin ships no default. Add bindkey '^N' sudo-command-line. |
doas not stripped | Set ZPWR_SUDO_REGEX and ZPWR_SUDO_CMD before the plugin loads. The default regex matches only sudo. |
| Strip fails mid-line | Place the cursor at the start or end of the line. A cursor landing inside the sudo flag block splits the prefix across $LBUFFER/$RBUFFER so neither side matches and the fallthrough prepends another sudo. |
| Override ignored | The env vars are read at source-time and only set when unset. Export them before sourcing the plugin; re-sourcing won't clobber an existing value. |
Sibling plugins
Part of the MenkeTechnologies zsh plugin family — the MenkeTechnologiesMeta umbrella:
- zsh-better-npm-completion — cache-aware
npm installcompletion - zsh-cargo-completion — live
cargo add/cargo installcompletion - zsh-cpan-completion — live MetaCPAN
cpan/cpanmcompletion - zsh-dotnet-completion — dotnet CLI completion
- zsh-expand — spacebar expansion (11,688 tests)
- zsh-gem-completion — ruby
gem installremote completion - zsh-git-acp — 159 git aliases + add/commit/push keybindings
- zsh-git-repo-cache — index every git repo on disk
- zsh-learn — MySQL-backed learning collection / quiz
- zsh-more-completions — 47,393-file mega completion corpus
- zsh-nginx — nginx commands + service-wrapper aliases
- zsh-pip-description-completion — pip remote completion with descriptions
- zsh-sed-sub — in-place sed substitution on the command line
- zsh-xcode-completions — xcodebuild / xcrun / swift completion