sudoplz
Run privileged commands from Claude Code, Cursor, and other AI coding agents with GUI approval:
sudoplz apt install foo
sudoplz encrypts your sudo password to your SSH public key and decrypts it after you approve a dialog showing the command. If you deny the dialog, sudo authentication fails.
Why
Coding agents can't handle interactive terminal prompts. Ask Claude Code to run sudo apt install foo and you get sudo: Authentication failed. The common workarounds all have problems:
- Passwordless sudo gives the agent and anything else running as your user the privileges allowed by your sudo policy without approval.
/etc/sudoersallowlists require predicting which commands the agent needs. They don't provide case-by-case approval.- Manual copy-paste is tedious and breaks the agent's flow.
sudoplz sets up the askpass helper and runs sudo -k -A for you. The -k option ignores cached sudo credentials. Your sudo policy still controls authentication and which commands you can run. Passwordless rules such as NOPASSWD and alternative authentication methods such as fingerprint readers can bypass askpass and its approval dialog. On Linux, configure password-only authentication if your sudo PAM policy supports biometrics.
This threat model assumes a personal workstation with an encrypted disk and a passphrase-protected SSH key. Not appropriate for shared or production systems.
Installation
- Use traditional
sudo, notsudo-rs.sudo-rsdoesn't support askpass. Check withsudo --version— it should say "Sudo version 1.x.x". If you're onsudo-rs, switch:sudo update-alternatives --install /usr/bin/sudo sudo /usr/bin/sudo.ws 100 sudo update-alternatives --config sudo # pick sudo.ws
- Make sure you have an Ed25519 or RSA SSH key. ECDSA and DSA keys are not supported.
- Install system dependencies:
age— required for SSH-backed password storage and TOTP.sudo pacman -S age/sudo apt install age/brew install age.zenityon Linux — provides the GUI approval dialog. Pre-installed on most GNOME-based distros;sudo apt install zenityif missing. Not needed on macOS (uses AppleScript).
- Install from PyPI with
uv:uv tool install sudoplz
This putsaskpassandsudoplzon yourPATH. For development, clone the repository and useuv tool install .instead. - Store your sudo password:
sudoplz set
- If sudo supports fingerprint authentication on Linux, configure a password-only PAM service before relying on approval dialogs.
Usage
Run a command with sudoplz, then approve or deny the dialog:
sudoplz apt install foo
Sudo options pass through to sudo. For example, to run as another user:
sudoplz -u postgres psql
Arguments, standard input, output, signals, and exit status pass through to sudo. Shell quoting works as usual. A pipeline or redirection outside the command still runs in your shell.
Management commands such as set, get, test, config, and audit keep their meaning. Use -- before an executable whose name matches a management command:
sudoplz -- test -f /etc/shadow
sudoplz rm-stored-password removes stored credentials. It replaces the former sudoplz clear command. sudoplz clear runs the terminal's clear executable through sudo.
Avoid -n (--non-interactive) and -S (--stdin): they conflict with askpass prompting. Use sudoplz --help for management help and sudo --help for sudo options.
Test the integration with:
sudoplz test
Direct sudo integration
For tools that invoke sudo directly, set SUDO_ASKPASS in their environment:
export SUDO_ASKPASS="$(command -v askpass)"
sudo -k -A apt install foo
Add the export to your shell startup file if you need it across sessions. The sudoplz command sets this variable automatically. Using sudo -A without -k can reuse cached credentials and skip approval.
Fingerprint authentication on Linux
sudo -A selects how sudo reads a password. It doesn't force PAM to request one. If sudo's PAM stack accepts a fingerprint first, sudo can execute the command without calling askpass. No sudoplz command-line option can override that root-owned authentication policy.
Sudo 1.9.9 and later support pam_askpass_service, which selects a separate PAM service for -A calls. Configure that service to require a password. Ordinary sudo calls keep their existing authentication policy.
For Debian or Ubuntu with local Unix accounts:
-
Keep a working administrator session open while changing authentication. Copy the existing sudo PAM service:
sudoplz cp /etc/pam.d/sudo /etc/pam.d/sudoplz-askpass sudoplz -e /etc/pam.d/sudoplz-askpass
-
In the copied file, replace
@include common-authwith password authentication:auth required pam_unix.so
Keep the account and session rules from the original sudo service. Don't include an authentication stack that enables fingerprint readers. If your account uses SSSD, LDAP, MFA, or additional authentication modules, have your administrator build a password-only stack that preserves those requirements instead of using this local-account example. When copying an existing stack, review numeric jump counts after removing modules. Review copied rules again after system PAM policy updates.
-
Create a sudoers drop-in with
visudo:sudoplz visudo -f /etc/sudoers.d/sudoplz-askpass
Set the service for your account, replacing
YOUR_USERNAMEwith your username:Defaults:YOUR_USERNAME pam_askpass_service="sudoplz-askpass"
-
Validate sudoers, then test a fresh invocation:
sudoplz visudo -c sudoplz echo hi
The test must show the sudoplz approval dialog without a fingerprint prompt. Deny approval and confirm that
hiisn't printed. Initial setup uses your existing authentication policy until the drop-in is installed.
To undo the change, remove /etc/sudoers.d/sudoplz-askpass first, then /etc/pam.d/sudoplz-askpass, using your administrator session. Other distributions have different PAM layouts; use their password authentication stack rather than copying this example.
Agent reminder with i-insist
If you use i-insist, install its hooks
and shfmt, then install the optional global
rule and its checker:
i-insist ensure
mkdir -p ~/.local/share/sudoplz
cp src/sudoplz/i_insist_guard.py ~/.local/share/sudoplz/i_insist_guard.py
cp hooks/i-insist.toml ~/.i-insist/sudoplz.toml
The rule blocks local shell commands that invoke sudo without -A or
--askpass and recommends sudoplz <command>. Calls to sudoplz pass through.
It also blocks sudo's -n and --non-interactive options even with askpass enabled.
It ignores quoted command strings, including ssh host 'sudo apt update', since sudo runs on the remote
host. It also ignores sudo passed as data to another command, including
heredoc bodies, except heredocs fed to sh, bash, zsh, or dash. Without
shfmt or on a syntax error, the rule allows the command. This is a
convenience check for ordinary shell calls, not a security boundary. i-insist's configuration protection requires human approval
before copying the rule. After upgrading sudoplz, copy the checker again to update the installed warning.
Security
Encryption at rest
New passwords use age encryption with an Ed25519 or RSA SSH key. The encrypted
payload, original key path, and creation time are stored together in
~/.config/sudoplz/credential.json with 600 permissions. Key preference for new
credentials is Ed25519, then RSA. Adding another key does not change how an
existing credential is decrypted.
Without an SSH key, passwords use the system keyring with creation-time metadata in the same credential file. A decryption failure never falls back to an older password. Replacement and clearing retire previous storage; an empty record prevents legacy credentials from being rediscovered after clearing.
Existing ~/.sudo_askpass.age and RSA ~/.sudo_askpass.ssh credentials are imported
on first use, preserving their original modification time. If both exist, the
newest file is selected. Legacy RSA keys can use OpenSSH or PEM format. A legacy
keyring entry has no reliable creation time: run sudoplz set again to use it with
expiration enabled. Keep the original SSH key available during migration.
Defense in depth
Encryption alone doesn't cover every abuse path — anything running as your user can in principle request decryption. The askpass script runs these checks on every invocation; any failure means no decryption:
- Caller path allowlist. Only decrypts when the caller's working directory is on an allowlist (home,
/tmp, etc.). Blocks invocations from unexpected locations like/var/tmp/malicious. - Caller process allowlist. Parent process must be on an allowlist (sudo, your shell, your IDE, your deploy tool). Keeps arbitrary binaries from invoking askpass directly.
- User confirmation. A GUI dialog asks for approval on each decryption, so any sudo elevation you didn't initiate is visible and can be denied.
- Rate limiting. Configurable max-attempts-per-hour and lockout window. Contains runaway scripts and brute-force attempts.
- Password expiration. Retrieval is refused after the configured age (default: 1 week), including keyring credentials. Expiration does not destroy stored data or revoke stolen copies; use
sudoplz rm-stored-passwordto remove stored credentials.
Configure these in ~/.config/sudoplz/config.json — an example is shipped as askpass-config.json in the repo; copy it and edit. Unknown settings, invalid types, and invalid ranges are rejected; malformed policy never silently falls back to defaults.
Why age for Ed25519?
Ed25519 is a signing algorithm (EdDSA), not encryption. OpenSSL handles RSA encryption directly, but Ed25519 keys can't do asymmetric encryption at all. age was designed to work with SSH keys including Ed25519.
SSH key unlocking
Passphrase-protected SSH keys are unlocked in memory using cryptography[ssh].
Askpass requests the passphrase through a hidden GUI prompt, or a hidden terminal
prompt in headless sessions. The decrypted identity is piped to age; it is never
written to a temporary key file or placed in command arguments.
Unlocking is per invocation. When TOTP and the password use the same identity,
askpass prompts once during that invocation. There is no session-wide passphrase
cache and no ssh-agent requirement: age does not support ssh-agent.
A fully non-interactive session cannot unlock a passphrase-protected key. Supplying
TOTP authorizes the request but does not unlock the SSH key.
Commands
Manage stored credentials and inspect configuration:
sudoplz set # Store password; default expiration is 1 week
sudoplz set-totp # Store password with TOTP verification (headless)
sudoplz totp-setup # Set up TOTP for headless sessions
sudoplz get # Check if password exists
sudoplz rm-stored-password # Remove password
sudoplz test # Test sudo integration
sudoplz config --show # View validated policy
sudoplz audit # Show recent askpass usage
Headless/SSH usage with TOTP
For servers or SSH sessions without a display, authenticate with a time-based one-time password (TOTP).
Initial setup (run once from a GUI session)
sudoplz totp-setup
Prints a TOTP secret and an otpauth:// URL to add to your authenticator app.
Setting a password from a headless session
sudoplz set-totp
Enter your 6-digit TOTP code, then your password.
Using sudo with TOTP
On Linux without DISPLAY, askpass prompts for a TOTP code:
# Interactive — prompts for TOTP code
sudoplz command
# Non-interactive — pass TOTP via environment
TOTP="123456" sudoplz command
Development checks
Install age, ssh-keygen, and openssl, then run:
uv run --group dev ruff check .
uv run --group dev mypy src
uv run python -m unittest discover -s tests -v
Tests use temporary SSH keys, dummy credentials, and an in-memory keyring.
Releases
.github/workflows/publish.yml runs the checks on pull requests. Each
successful push to main publishes a release to PyPI and creates a tagged
GitHub release with the wheel and source distribution. Manual workflow
dispatch retries a release without another commit.
CI owns patch versions: it keeps an unpublished version from pyproject.toml,
otherwise it increments the latest PyPI patch version and commits
pyproject.toml and uv.lock. Use uv version --bump minor or
uv version --bump major for an intentional version change. Publishing uses
PyPI Trusted Publishing for
workflow publish.yml; GitHub stores no PyPI token.
Credits
The idea — an SSH-key-encrypted sudo password served via SUDO_ASKPASS, gated by a confirmation dialog — is from GlassOnTin/secure-askpass. That project is dormant; sudoplz is a substantially rewritten and cleaned up fork. Thanks to @GlassOnTin for the original idea.
License
MIT — see LICENSE.
Metadata
Release files for sudoplz 0.5.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sudoplz-0.5.3.tar.gz | 88.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sudoplz-0.5.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 110.7 kB
Release files / sudoplz-0.5.3.tar.gz
| Download URL | sudoplz-0.5.3.tar.gz |
|---|---|
| Size | 88.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
26dfe88ead3bc8e74c5d42a5c2417deec68f881ab0c348383d71787a3994a50e
|
|
BLAKE2b-256 checksum How to use checksums |
90233674dcabfb362011fa115f4305eb59e987b6780cdec7267a773d94891445
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / sudoplz-0.5.3-py3-none-any.whl
| Download URL | sudoplz-0.5.3-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8299745cf1fa8a8e6ce44511cf9ca120d17ae4501f5bd56c84a20dc106db6210
|
|
BLAKE2b-256 checksum How to use checksums |
3400d8e985b05ec2ec839ab897e3bcf9932128b186cf1cc1cda45128e98b92e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for sudoplz 0.5.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sudoplz-0.5.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.