smbex
A terminal (TUI) explorer for remote hosts over SMB, SSH/SFTP, and FTP/FTPS, built for slow links: ranger-style navigation, in-session listing cache, background downloads that yield to browsing, folder preloading, and offline filename translation.
Status: feature-complete (all planned phases done). See CLAUDE.md for the architecture, design notes, and backlog.
Install
pipx (PyPI)
Install the smbex command into its own isolated environment (needs Python 3.10+):
pipx install smbex
# ...with offline filename translation (CTranslate2 + SentencePiece, ~65 MB):
pipx install "smbex[translate]"
smbex --help
Installed this way, run it as smbex … (the examples below use python3 -m smbex …;
the two are interchangeable). Two things are not Python packages and so aren't
pulled in automatically: the --mux feature needs the system OpenSSH ssh client,
and translation needs a one-time language-model download (smbex --install-lang <lang>; see below).
Kali Linux (apt only — no pip)
Every runtime/test dependency except the translation engine is packaged for Kali:
sudo apt update
sudo apt install python3-impacket python3-paramiko python3-textual \
python3-pytest python3-pytest-asyncio
# optional: the reference impacket CLI tools (impacket-smbclient, ...)
# sudo apt install impacket-scripts
Then run straight from the repo (no pip, no venv needed):
cd /path/to/smbex
python3 -m smbex --version
Filename translation is optional and needs two small wheels (not in apt) —
CTranslate2 + SentencePiece (~65 MB, no torch/CUDA). Add them in a venv that
inherits the apt packages, then fetch a language model (one .argosmodel file):
python3 -m venv --system-site-packages ~/.venvs/smbex
~/.venvs/smbex/bin/pip install ctranslate2 sentencepiece
~/.venvs/smbex/bin/python -m smbex --install-lang ja # one-time, online: ja->en model (~130 MB)
Translation runs entirely on this machine (no filename leaves the box); the only
network use is that one-time model download. See CLAUDE.md → Install for details
(offline --model-file, reusing existing Argos models, etc.).
Models are stored per user, one directory per language pair, and --install-lang
prints the destination when it finishes:
~/.local/share/smbex/models/ja_en/ # $XDG_DATA_HOME/smbex/models/ if that is set
# SMBEX_MODEL_DIR overrides both
Each is ~130 MB; delete the directory to remove that language. An existing
argostranslate install is reused in place rather than re-downloaded — packages
under ~/.local/share/argos-translate/packages/ (or $ARGOS_PACKAGES_DIR) are
discovered as well.
Development (any distro, venv)
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q
Run
# SMB, impacket-smbclient-style target (password is prompted if omitted):
python3 -m smbex 'DOMAIN/user:password@host'
python3 -m smbex -H :NThash --no-pass 'DOMAIN/user@host' # pass-the-hash
python3 -m smbex -k --dc-ip 10.0.0.1 'DOMAIN/user@host.domain' # Kerberos (ccache)
# SSH/SFTP (host keys auto-accepted by default; --strict-host-keys to verify):
python3 -m smbex 'ssh://user@host'
python3 -m smbex -i ~/.ssh/id_ed25519 'ssh://user@host:2222/var/log'
# SSH via an existing OpenSSH ControlMaster socket — no re-login (needs the ssh client):
python3 -m smbex --mux # pick from sockets found in ~/.ssh etc.
python3 -m smbex --mux /path/to/master.sock # ride a specific socket directly
# FTP / FTPS (no user -> anonymous; ftps:// for TLS):
python3 -m smbex 'ftp://user@host'
python3 -m smbex 'ftps://user:pass@host:21/pub'
Navigate like ranger: h/j/k/l (or arrows), g/G for top/bottom, q to quit.
Listings show a compact age (3d, 2w) and file sizes; o cycles the sort
(name → newest → oldest). A status gutter marks folders whose listing is cached
(·), and files/folders queued (↓) / downloaded (✓).
Download in the background: d (selected file, or a folder recursively), a (all
files in the current folder). Downloads mirror the remote tree under DIR/<host>
(--download-dir, default ./downloads), resume partial files, skip complete ones,
and yield to browsing so navigation stays snappy. --flat instead drops everything
into the one per-host folder with the remote path folded into each name
(share/2024/report.pdf → share_2024_report.pdf; clashes are numbered ~2).
The task panel stays out of the way: it appears only while transfers are in flight,
lists just those, and disappears when they finish (--download-panel hidden keeps it
off screen entirely; the status bar always shows dl:3/12 ↓47%). w opens the full
list — there j/k select a transfer, K/J move it up or down the queue and x
cancels it (or clears a finished entry); w/h/Esc close it.
Reordering can preempt: moving a transfer past the one that's running makes the
running one yield the wire at the next chunk boundary and resume later from the
bytes already on disk — so when a 10 MB file is hogging a slow link and you want the
5 KB text file now, press J on the big one (or K on the small one). Cancelling
keeps the partial file too, so grabbing it again with d resumes rather than
restarting.
Not on FTP: stopping a RETR early means draining the rest of the data connection anyway (see Protocol fidelity below), so the bytes arrive whether you keep them or not — smbex refuses to interrupt a running FTP transfer rather than lose a file you have already paid for, and says so. Queued transfers can still be cancelled and reordered on FTP; only the one on the wire is untouchable. SMB and SFTP stop immediately.
Optional extras: --preload prefetches surrounding folders (toggle with p);
--translate <lang> shows English filename translations beside the originals
(toggle with t), computed on-box — see Install for the one-time model setup.
Press ? for the key reference. If a link drops it's reported and you press r
to reconnect (a deliberate new login); --auto-reconnect heals silently instead.
--theme <name> (dark/light/nord/gruvbox) sets the colour theme; T switches it.
[ / ] hide the parent / preview column to save space. A downloaded file's
content shows in the preview pane (text, or an xxd-style hex dump for binaries).
Press l/Enter on a downloaded file to open a full content viewer: text scrolls
with j/k (loaded lazily, so large files open instantly) and, with translation on,
shows the original and English side by side; a binary opens as a scrollable
xxd-style hex view. h/Esc goes back.
Defaults live in a config file (~/.config/smbex/config.ini; flags override it):
python3 -m smbex --write-config # drop a commented sample, then edit it
# ...or save the options you just used as your defaults (comments kept):
python3 -m smbex --translate ja --flat --preload --save-config
Keys: preload, auto_reconnect, sort, theme, parent, preview, translate,
download_dir, download_panel, flat, index.
--index FILE writes down everything you browse past — one tab-separated line per
file or folder (path, type, exact size in bytes, mtime, plus the English name while
translation is on) — so you can hand the result to an AI agent and ask which files
look interesting:
smbex --index seen.tsv 'DOMAIN/user@host'
It is fed only from listings the session was already fetching, so it adds no network traffic and cannot walk anywhere you didn't go; a folder is written once, and pointing later runs at the same file accumulates instead of repeating. This is the one thing smbex persists to disk — the listing cache itself stays session-only — so it happens only when you ask for it by name.
smbex --help ends with a quickstart covering connecting, the config file, and
the one-time translation-model setup (with the model index URL).
Build a release to transfer
scripts/make_release.sh writes transferable artifacts to dist/:
bash scripts/make_release.sh
-
dist/smbex.pyz— a single-file zipapp. Copy it to the target and run it; it needs onlypython3plus the runtime deps (on Kali:sudo apt install python3-impacket python3-paramiko python3-textual):python3 smbex.pyz 'demo:demo@127.0.0.1' --port 4455 # or: ./smbex.pyz
-
dist/smbex-<ver>.tar.gz— the source tree (run from it withpython3 -m smbex). -
dist/QUICKSTART.txt— the same quickstart shown at the end of--help.
Try it out (local demo, no root)
Terminal 1 — start a throwaway SMB server with sample files on a high port:
python3 scripts/demo_server.py
Terminal 2 — connect the client (any username/password is accepted):
python3 -m smbex 'demo:demo@127.0.0.1' --port 4455
# with Japanese filename translation (needs: --install-lang ja), then press 't':
python3 -m smbex --translate ja 'demo:demo@127.0.0.1' --port 4455
The demo share has a 日本語/ folder of Japanese-named files and folders (写真/,
仕事/, 地図.png, …) so you can watch them render as 写真 → Photos with t.
Test
.venv/bin/python -m pytest -q # unit + UI tests
.venv/bin/python -m pytest -m integration # local SMB/SSH server tests (Phase 1+)
Protocol fidelity — how smbex differs from the standard tools
smbex builds on the same libraries the reference tools use (impacket for SMB,
paramiko for SSH/SFTP, stdlib ftplib for FTP), so the packets themselves are
well-formed the same way — smbex does not hand-roll protocol framing. The
differences below are all in usage patterns (command sequences, frequency, trust
decisions), not in the bytes on the wire. They are documented here so you can weigh
the risk against your own hosts, especially fragile or tightly-audited ones.
SMB — per-file tree-connect churn
smbex issues one TREE_CONNECT/TREE_DISCONNECT per file, where
impacket-smbclient and the Windows redirector connect a share once and reuse
that tree for the whole session. Each open_file does connectTree → openFile
and its close tears both down (smbex/backend/impacket_backend.py). Downloading 500
files is 500 tree cycles here vs. 1 there.
- Why it's written this way: statelessness. A cached tree id (
tid) goes stale after a dropped link (see the reconnect design), so by never holding one across operations we can't reuse a deadtid. This is a simplicity choice, not a performance one — there is no throughput advantage. - Cost: on a host with SMB auditing this multiplies tree-connect events; it is slightly more churn than a normal client. It is not malformed and cannot desync the session.
- If it matters: cache
{share: tid}on the backend, connect-on-first-use, and invalidate it inreconnect()— a small, self-contained change that restores one-tree-per-share parity.
SMB — stat() uses a directory FIND, not a file QUERY_INFO
stat() reads a single file's metadata via listPath (an SMB2 QUERY_DIRECTORY
against the parent, filename as the search pattern) rather than opening the file and
issuing QUERY_INFO.
- Why: SMB2 has no "getattr by path" — a real
QUERY_INFOrequires first opening the file (CREATE), which emits a file-access audit event and participates in sharing-mode/oplock negotiation (you can collide with a process holding the file open). The directory FIND reads the metadata out of the parent's enumeration without ever opening the file. On a sensitive host this is the lighter, less-intrusive touch, and it matches impacket-smbclient. - Cost: it is a directory-query op rather than a handle getinfo, and (today) it carries a tree-connect cycle with it (see above).
SSH — default host-key policy is trust-on-first-use, not persisted
The default auto policy (smbex/backend/ssh_backend.py) silently accepts an
unknown host key and keeps it only in memory. OpenSSH ssh/scp instead prompt
on an unknown key (and refuse in batch mode), writing accepted keys to
~/.ssh/known_hosts.
- A changed key is still rejected (paramiko raises regardless of policy), so the
exposure is limited to unknown/first-contact hosts (MITM-susceptible on first
connect). Because nothing is written to
known_hosts, every run is "first contact". - If it matters: pass
--strict-host-keys(policystrict) to require a pre-known key, matching default OpenSSH behavior.
SSH — SFTP subsystem, not scp
smbex speaks the SFTP subsystem (sftp-server), not the scp protocol. SFTP was
chosen because it supports listing, stat, and seekable/resumable reads — the things
the browser, resumable downloads, and the lazy preview/viewer need; plain scp is a
transfer-only subset. On a host where the sftp subsystem is disabled
(Subsystem sftp removed, a ForceCommand/restricted shell permitting only scp, or
old dropbear builds without sftp-server), smbex will fail where scp works. See
"scp support" below for the evaluation of adding an scp backend.
SSH — listings probe symlink targets
When listing a directory, smbex issues an extra STAT on each symlink target to
decide whether a link is a directory (_to_entry, smbex/backend/ssh_backend.py).
ls/sftp use the lstat attrs already returned by readdir and do not follow
the link.
- Why: so directory-symlinks are browsable (you can
linto them). - Cost: extra round trips, and it touches paths you did not navigate to.
statdoes not open the target, so the risk is low, but on a sensitive host a link could point at a device node, an automount trigger, or a monitored path.
FTP — early-stopped reads drain the rest of the file instead of ABOR
A normal FTP client that stops a RETR early sends ABOR. smbex instead reads the
data connection to its natural EOF and then consumes the single clean 226
(_drain_and_finish, smbex/backend/ftp_backend.py).
- Why FTP forces the choice: FTP splits control (commands/responses) from
data (file bytes), and the session invariant is that every control response
must be read by the command that provoked it — one response left unread and the
control channel is off-by-one and wedged.
ABORis genuinely awkward: it is preceded by an out-of-band Telnet interrupt (IAC+IP, then Synch) and the server then emits two control responses in a server-dependent order (typically426and226; some do226+225). Real clients (lftp, BSDftp) implement all of that;ftplib'sabort()does not and is known to leave the control channel desynced against many servers. Draining keeps the control channel provably in sync. - The trade-off: correctness/robustness over bandwidth. For a fragile server the drain is the conservative choice — it never leaves the control channel in a weird state (the thing that wedges FTP sessions). The cost is bandwidth: previewing or seeking within a large file can pull the rest of that file. It only triggers when you stop early — a normal download-to-EOF closes cleanly with no drain.
- If the bandwidth ever bites: the alternative is not ftplib's flaky
abort()— it is to drop and reopen the whole control connection to abandon the transfer, which is clean but makes a fresh login/audit event each time.
FTP — passive mode + MLSD
ftplib defaults to passive mode (PASV/EPSV) and smbex prefers MLSD for
listings (falling back to LIST). The classic ftp command defaults to active mode
and uses LIST/NLST. Both smbex choices match modern clients (lftp); if your
baseline is the traditional ftp client, expect a different data-connection direction
(firewall-visible) and MLSD where you would see LIST. TYPE I is also re-sent
before every RETR (extra but valid — MLSD/LIST leave the session in ASCII mode).
Reconnect produces additional login/auth events
By design, a dropped link plus r (or --auto-reconnect) performs a fresh login,
so one smbex "session" can show multiple logins from the same source — unlike a single
long-lived client session. --auto-reconnect makes these re-logins silent. Relevant
for audit correlation.
scp support (evaluated, not implemented)
Adding scp as a peer protocol is technically possible (paramiko exec_command running
scp -f) but fits smbex's model poorly, so it is intentionally not built:
- No listing or stat. The scp protocol only transfers files/trees; browsing would
require shelling out to remote
ls/findover an exec channel — i.e. running commands on the host (a larger footprint than SFTP's read-only file ops, and shell/PATH-dependent). The ranger UI is built onlist()/stat(), which scp lacks. - No seek/resume. scp streams a whole file start-to-finish, so resumable downloads, the lazy preview, and the windowed viewer (all of which seek) cannot work.
- Shrinking need. OpenSSH 9.0 (2022) deprecated the scp protocol and made the
scpcommand use SFTP under the hood, so on modern hosts "scp" already is sftp.
Where it would earn its place: appliances/embedded gear (e.g. dropbear without
sftp-server) or hardened boxes that permit scp but not the sftp subsystem. If such
targets exist in your fleet, the honest shape is not a browsing peer backend but a
restricted download-only mode (transfer via scp, browse via remote ls), with no
resume/preview. Recommendation: don't build it speculatively — revisit only if you can
enumerate real sftp-disabled hosts.
License
MIT — see LICENSE.
Metadata
Release files for smbex 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| smbex-0.3.0.tar.gz | 117.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| smbex-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 200.3 kB
Release files / smbex-0.3.0.tar.gz
| Download URL | smbex-0.3.0.tar.gz |
|---|---|
| Size | 117.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
679c356e159d8856e074c98c7ece0617d88b26b80235e793e07ae6523ee5b9bb
|
|
BLAKE2b-256 checksum How to use checksums |
8f6f4a9e0fa3a309d866d48f21cac54f708cec7bb3f1ce4fef01a342e43ceb87
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 2, 2026.
Transparency logRelease files / smbex-0.3.0-py3-none-any.whl
| Download URL | smbex-0.3.0-py3-none-any.whl |
|---|---|
| Size | 83.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3c1974ce8be48f1ada46e139385eef6a6a2da28196bc4f50d9ec171f6f236755
|
|
BLAKE2b-256 checksum How to use checksums |
3f449d481f792c0907477fee2fbc1c0b46d1774951c450099a6ac511c12af9e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 2, 2026.
Transparency log