A Python CLI wrapper for Duo Security APIs — built for operators and AI agent integration.
duo_client_python is a great library for building Duo integrations in Python. But there's no CLI you can use directly from a terminal, a shell script, or an AI agent.
duo-cli fills that gap:
- Terminal-first —
duo-cli auth push jsmith,duo-cli universal login jsmith - AI-agent ready — any agent that can call shell commands can use Duo (MCP, LangChain, etc.)
- Human-in-the-loop — use
duo-cli auth pushto gate privileged agent actions behind real Duo approval
pip install duo-cliOr from source:
git clone https://github.com/cmedfisch/duo-cli.git
cd duo-cli
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Duo has separate Auth API and Universal Prompt (Web SDK) integrations — configure each one you need:
duo-cli configure --api auth
duo-cli configure --api universalThe interactive setup walks you through where to find credentials in the Duo Admin Panel.
Credentials are stored in ~/.duo-cli/config.json. You can also use environment variables (useful for CI/agents):
# Auth API
export DUO_AUTH_IKEY=DIXXXXXXXXXXXXXXXXXX
export DUO_AUTH_SKEY=your-secret-key
export DUO_AUTH_HOST=api-XXXXXXXX.duosecurity.com
# Universal Prompt (Web SDK)
export DUO_UNIVERSAL_CLIENT_ID=DIXXXXXXXXXXXXXXXXXX
export DUO_UNIVERSAL_CLIENT_SECRET=your-client-secret
export DUO_UNIVERSAL_HOST=api-XXXXXXXX.duosecurity.com# Verify credentials
duo-cli auth check
# Check if a user is enrolled and see their devices
duo-cli auth preauth jsmith
# Send a Duo Push
duo-cli auth push jsmith --reason "Agent requesting elevated access"
# Send a push with custom info fields
duo-cli auth push jsmith -p "action=deploy" -p "target=prod-us-east"
# Browser-based auth with full Duo policy enforcement
duo-cli universal login jsmith
# JSON output for piping / agent consumption
duo-cli -o json universal login jsmith| Command | Description |
|---|---|
duo-cli auth check |
Verify Auth API credentials |
duo-cli auth preauth <username> |
Check if a user can authenticate, list their devices |
duo-cli auth push <username> |
Send a Duo Push |
duo-cli auth sms <username> |
Send SMS passcodes to a user |
duo-cli auth passcode <username> <code> |
Authenticate with a passcode |
duo-cli auth status <txid> |
Check async auth transaction status |
duo-cli auth push <username> [OPTIONS]
-r, --reason TEXT Reason shown in the push prompt
-p, --pushinfo TEXT Custom key=value pairs (repeatable)
-d, --device TEXT Device ID or "auto" (default: auto)
--type TEXT Label for Duo auth logs
--display-username TEXT Override displayed username
--ipaddr TEXT Client IP for Duo's risk engine
--wait / --no-wait Wait for response (default: wait)| Command | Description |
|---|---|
duo-cli universal check |
Verify Universal Prompt credentials |
duo-cli universal login <username> |
Authenticate via browser with full Duo policy enforcement |
The universal login command opens your browser to the Duo Universal Prompt, spins up a local callback server, and returns the full JWT result including auth context, device info, and user details.
This is the policy-enforced flow — trusted devices, allowed networks, remembered devices, and all other Duo policies apply. Unlike auth push which calls the Auth API directly, this goes through the same OIDC flow as your SSO apps.
# Browser-based auth with full policy enforcement
duo-cli universal login jsmith
# JSON output returns the full decoded JWT
duo-cli -o json universal login jsmithThe killer feature: any AI agent can request human approval via Duo Push before taking a privileged action.
# As an MCP tool, an agent can call:
# duo-cli auth push jsmith --reason "Deploy to production"
#
# The user gets a Duo Push on their phone.
# If they approve, the agent proceeds. If they deny, the agent stops.# Send push without waiting
duo-cli auth push jsmith --reason "Approve deploy" --no-wait
# Output: Push sent. Transaction ID: abc123
# Poll for result
duo-cli auth status abc123
# Output: Status: allowfrom langchain.tools import ShellTool
shell = ShellTool()
# Agent decides it needs approval before a destructive action
result = shell.run("duo-cli auth push jsmith --reason 'Delete staging environment'")
if "allow" in result.lower():
# proceed with the action
...auth push |
universal login |
|
|---|---|---|
| Method | Direct Auth API call | Browser-based OIDC flow |
| Duo Policy | Not enforced | Fully enforced |
| Trusted devices | No | Yes |
| Allowed networks | No | Yes |
| Remembered devices | No | Yes |
| User interaction | Phone push only | Full Universal Prompt |
| Return value | allow/deny | Full JWT with auth context |
| Best for | Quick agent approvals | Compliance-sensitive flows |
All commands support --output json for machine-readable output:
duo-cli -o json auth preauth jsmith
duo-cli -o json universal login jsmith- duo_client_python — the official Duo Python SDK
- duo_universal_python — Duo Universal Prompt SDK
- Click — CLI framework
- Rich — beautiful terminal output