Skip to main content

Modern SSH terminal widget for PyQt6 with credential vault and jump host support

Project description

nterm

A modern SSH terminal for network engineers

PyQt6 terminal widget with encrypted credential vault, jump host chaining, YubiKey/FIDO2 support, and legacy device compatibility.

Built for managing hundreds of devices through bastion hosts with hardware security keys.

nterm screenshot


Features

Terminal

  • xterm.js rendering via QWebEngineView — full VT100/ANSI support
  • 12 built-in themes: Catppuccin, Dracula, Nord, Solarized, Gruvbox, Enterprise variants
  • Hybrid themes: dark UI chrome with light terminal for readability
  • Custom YAML themes with independent terminal and UI colors
  • Tab or window per session — pop sessions to separate windows
  • Session capture to file (clean text, ANSI stripped)
  • Unicode, emoji, box-drawing characters

Authentication

  • SSH Agent with YubiKey/FIDO2 hardware keys
  • Password, key file, keyboard-interactive, certificate auth
  • Multiple auth methods with automatic fallback
  • RSA SHA-1 fallback for legacy devices (OpenSSH < 7.2)
  • Legacy crypto support for old Juniper/Cisco gear

Connection Management

  • Jump host chaining (unlimited hops)
  • Auto-reconnection with exponential backoff
  • Connection profiles in YAML/JSON
  • Pattern-based credential resolution

Credential Vault

  • AES-256 encryption with PBKDF2 (480,000 iterations)
  • Pattern matching — map credentials to hosts by wildcard or tag
  • Cross-platform keychain: macOS Keychain, Windows Credential Locker, Linux Secret Service
  • Full PyQt6 management UI

Scripting API

  • Query device inventory and credentials programmatically
  • Built-in IPython console with API pre-loaded
  • Platform-aware commands - one API, correct syntax everywhere
  • Interactive REPL with quick commands and structured output
  • Foundation for MCP tools and agentic workflows

Screenshots

Gruvbox Hybrid Theme Credential Manager
gruvbox vault
Connection Dialog Multi-vendor Support
connect neofetch

Dev Console

nterm includes a built-in development console accessible via Dev → IPython or Dev → Shell. Open in a tab alongside your SSH sessions, or pop out to a separate window.

IPython Console

IPython Console

The IPython console runs in the same Python environment as nterm, with the scripting API pre-loaded. Query your device inventory, inspect credentials, and prototype automation workflows without leaving the app.

# Available immediately when IPython opens
api.devices()                    # List all saved devices
api.search("leaf")               # Search by name/hostname  
api.credentials()                # List credentials (after api.unlock())
api.help()                       # Show all commands

Use cases:

  • Debug connection issues with live access to session objects
  • Prototype automation scripts against your real device inventory
  • Test credential resolution patterns
  • Build and test MCP tools interactively

Requires the scripting extra: pip install ntermqt[scripting]


Installation

Be aware due to a naming conflict, the pypi package is actually "ntermqt"

From PyPI

https://pypi.org/project/ntermqt/

pip install ntermqt

# With optional scripting support (IPython)
pip install ntermqt[scripting]

# With all optional features
pip install ntermqt[all]

# Run
nterm

From Source

git clone https://github.com/scottpeterman/nterm.git
cd nterm

# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # Linux/macOS
# .venv\Scripts\activate   # Windows

# Install in development mode
pip install -e ".[all]"

# Run
nterm
# or
python -m nterm

Requirements

  • Python 3.10+
  • PyQt6 with WebEngine
  • paramiko
  • cryptography
  • pyyaml

Platform Support

Platform PTY Keychain
Linux ✅ pexpect Secret Service
macOS ✅ pexpect macOS Keychain
Windows 10+ ✅ pywinpty Credential Locker

Scripting API

nterm includes a full scripting API for programmatic access to your device inventory, credential vault, and network devices. Use it from IPython, CLI, or Python scripts.

IPython Console

Open Dev → IPython → Open in Tab to get an interactive console with the API pre-loaded:

api.devices()                    # List all saved devices
api.search("leaf")               # Search by name/hostname
api.devices("eng-*")             # Glob pattern filter
api.folders()                    # List all folders

api.unlock("vault-password")     # Unlock credential vault
api.credentials()                # List credentials (metadata only)

# Connect and execute commands
with api.session("usa-leaf-1") as s:
    result = api.send(s, "show version")
    print(result.parsed_data)

api.help()                       # Show all commands

Interactive REPL

Start the REPL for interactive device exploration with platform-aware quick commands:

api.repl()
nterm> :unlock
nterm> :connect usa-leaf-1

📊 usa-leaf-1> :version
──────────────────────────────────────────────────
  Version:  15.2(4.0.55)E
  Hardware: IOSv
  Serial:   9J0PD0QB9W1
  Uptime:   1 week, 4 days, 7 minutes
──────────────────────────────────────────────────

📊 usa-leaf-1> :neighbors
Local Interface      Neighbor                       Remote Port
----------------------------------------------------------------
Gi0/0                usa-spine-2.lab.local          Ethernet1
Gi0/1                usa-spine-1.lab.local          Ethernet1

📊 usa-leaf-1> :interfaces
[Rich formatted interface table]

Quick Commands:

  • :version - Structured version info
  • :config - Running configuration
  • :interfaces - Interface status
  • :neighbors - CDP/LLDP neighbors (auto-detects)
  • :bgp - BGP summary
  • :routes - Routing table

Python Scripts

from nterm.scripting import NTermAPI

api = NTermAPI()
api.unlock("vault-password")

# Context manager for automatic cleanup
for device in api.devices("*spine*"):
    with api.session(device.name) as s:
        # Platform-aware commands - works on Cisco, Arista, Juniper
        result = api.send_platform_command(s, 'version')
        print(f"{device.name}: {result.parsed_data[0].get('VERSION')}")

# Try multiple commands until one works
with api.session("router1") as s:
    result = api.send_first(s, [
        "show cdp neighbors detail",
        "show lldp neighbors detail",
    ])

CLI

nterm-cli devices                     # List all devices
nterm-cli search leaf                 # Search devices
nterm-cli device eng-leaf-1           # Device details
nterm-cli credentials --unlock        # List credentials
nterm-cli --json devices              # JSON output for scripting

Key Features

Feature Description
Context Manager with api.session() auto-disconnects
Platform-Aware send_platform_command() picks correct syntax
Fallback Commands send_first() tries alternatives
Structured Output TextFSM parsing to List[Dict]
ANSI Filtering Clean output, no escape sequences
Paging Detection Raises error if paging not disabled

See scripting/README_API_IPython.md for full API documentation. See scripting/README_REPL.md for REPL documentation.


Quick Start

As a Widget

from PyQt6.QtWidgets import QApplication, QMainWindow
from nterm import ConnectionProfile, AuthConfig, SSHSession, TerminalWidget, Theme

app = QApplication([])

terminal = TerminalWidget()
terminal.set_theme(Theme.gruvbox_hybrid())

profile = ConnectionProfile(
    name="router",
    hostname="192.168.1.1",
    auth_methods=[AuthConfig.password_auth("admin", "secret")],
)

session = SSHSession(profile)
terminal.attach_session(session)
session.connect()

window = QMainWindow()
window.setCentralWidget(terminal)
window.resize(1000, 700)
window.show()

app.exec()

With Credential Vault

from nterm.vault import CredentialStore, CredentialResolver

store = CredentialStore()
store.unlock("master-password")

# Add credential with pattern matching
store.add_credential(
    name="network-devices",
    username="admin",
    password="secret",
    match_hosts=["*.network.corp", "192.168.1.*"],
    match_tags=["cisco", "juniper"],
)

# Auto-resolve credentials by hostname
resolver = CredentialResolver(store)
profile = resolver.resolve_for_device("switch01.network.corp", tags=["cisco"])

session = SSHSession(profile)
session.connect()

Themes

nterm includes 12 built-in themes covering dark, light, and hybrid styles.

Built-in Themes

# Dark themes
Theme.default()           # Catppuccin Mocha
Theme.dracula()           # Dracula
Theme.nord()              # Nord
Theme.solarized_dark()    # Solarized Dark
Theme.gruvbox_dark()      # Gruvbox Dark
Theme.enterprise_dark()   # Microsoft-inspired dark

# Light themes
Theme.gruvbox_light()     # Gruvbox Light
Theme.enterprise_light()  # Microsoft-inspired light
Theme.clean()             # Warm paper tones

# Hybrid themes (dark UI + light terminal)
Theme.gruvbox_hybrid()    # Gruvbox dark chrome, light terminal
Theme.nord_hybrid()       # Nord polar night chrome, snow storm terminal
Theme.enterprise_hybrid() # VS Code-style dark/light split

Hybrid themes combine a dark application chrome (menus, tabs, sidebars) with a light terminal for maximum readability — ideal for long sessions reviewing configs or logs.

Custom YAML Themes

# ~/.nterm/themes/my-theme.yaml
name: my-theme

terminal_colors:
  background: "#1a1b26"
  foreground: "#c0caf5"
  cursor: "#c0caf5"
  black: "#15161e"
  red: "#f7768e"
  green: "#9ece6a"
  yellow: "#e0af68"
  blue: "#7aa2f7"
  magenta: "#bb9af7"
  cyan: "#7dcfff"
  white: "#a9b1d6"
  # ... bright variants

# UI chrome (can differ from terminal)
background_color: "#1a1b26"
foreground_color: "#c0caf5"
border_color: "#33467c"
accent_color: "#7aa2f7"

Session Capture

Capture session output to a file for documentation, auditing, or extracting config snippets.

Right-click in terminal → Start Capture... to begin recording. Output is saved as clean text with ANSI escape sequences stripped — ready for grep, diff, or pasting into tickets.

  • Per-session capture (each tab independent)
  • File dialog for save location
  • Menu shows active capture filename
  • Auto-stops when session closes

Jump Hosts

profile = ConnectionProfile(
    name="internal-db",
    hostname="db01.internal.corp",
    auth_methods=[AuthConfig.agent_auth("dbadmin")],
    jump_hosts=[
        JumpHostConfig(
            hostname="bastion.corp.com",
            auth=AuthConfig.agent_auth("youruser"),
            requires_touch=True,
            touch_prompt="Touch YubiKey for bastion...",
        ),
    ],
)

Legacy Device Support

nterm automatically handles old network equipment:

  • RSA SHA-1 fallback for OpenSSH < 7.2 servers
  • Legacy KEX algorithms: diffie-hellman-group14-sha1, group1-sha1
  • Legacy ciphers: aes128-cbc, 3des-cbc
  • Auto-detection: tries modern crypto first, falls back as needed

Tested with:

  • Junos 14.x (2016)
  • Cisco IOS 12.2
  • Old Arista EOS
  • Any device running OpenSSH 6.x

Architecture

nterm/
├── connection/        # ConnectionProfile, AuthConfig, JumpHostConfig
├── session/
│   ├── ssh.py         # SSHSession (Paramiko) with legacy fallback
│   ├── interactive_ssh.py   # Native SSH + PTY
│   ├── local_terminal.py    # Local shell/IPython sessions
│   └── pty_transport.py     # Cross-platform PTY
├── terminal/
│   ├── widget.py      # TerminalWidget (PyQt6 + xterm.js)
│   └── bridge.py      # Qt ↔ JavaScript bridge
├── theme/
│   ├── engine.py      # Theme system
│   └── themes/        # YAML theme files
├── vault/
│   ├── store.py       # Encrypted credential storage
│   ├── resolver.py    # Pattern-based resolution
│   └── manager_ui.py  # PyQt6 credential manager
├── manager/           # Session tree, connection dialogs
└── scripting/         # API, REPL, automation support
    ├── api.py             # NTermAPI class
    ├── models.py          # ActiveSession, CommandResult, DeviceInfo
    ├── platform_data.py   # Platform commands and patterns
    ├── platform_utils.py  # Platform detection, extraction helpers
    ├── ssh_connection.py  # Low-level SSH with ANSI filtering
    ├── repl.py            # NTermREPL command router
    └── repl_interactive.py # Interactive REPL display

Related Projects


License

GPLv3


Contributing

Contributions welcome:

  • Additional themes
  • Windows testing
  • Session recording/playback
  • Telnet/serial support

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ntermqt-0.1.12.tar.gz (236.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ntermqt-0.1.12-py3-none-any.whl (261.3 kB view details)

Uploaded Python 3

File details

Details for the file ntermqt-0.1.12.tar.gz.

File metadata

  • Download URL: ntermqt-0.1.12.tar.gz
  • Upload date:
  • Size: 236.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for ntermqt-0.1.12.tar.gz
Algorithm Hash digest
SHA256 9cf53af23d36a95a0a7f47479e931ded6ad50e22a5417e128d54c35d971b6473
MD5 a1165c88a73fa2fa4141be90a3c78984
BLAKE2b-256 ac72ed40756799daff4dc1f52e4ec37abd506ffe4876272e8a9f5656f04b37df

See more details on using hashes here.

File details

Details for the file ntermqt-0.1.12-py3-none-any.whl.

File metadata

  • Download URL: ntermqt-0.1.12-py3-none-any.whl
  • Upload date:
  • Size: 261.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for ntermqt-0.1.12-py3-none-any.whl
Algorithm Hash digest
SHA256 daafe8cc9f359274010da1c08fb7185e35f6554bcd99759d7a36593a8881875d
MD5 c1a58f2537d039e6088239c5fa3d696c
BLAKE2b-256 1a4f6c0e0788f7681485f20f01c6814036d66b5a457eeb3b58237c00b11a0584

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page