README
ΒΆ
SFTP Commander
A keyboard-driven terminal SFTP client. Browse, transfer, and manage remote files without leaving the shell β with a Midnight-Commander-style two-pane layout, parallel transfers, and full SSH-config integration.
Table of contents
- Highlights
- Install
- Quick start
- Features
- Keyboard shortcuts
- Configuration
- Development
- Releasing
- Roadmap
- Contributing
- License
- Acknowledgments
Highlights
- ποΈ Two-pane layout β Midnight Commander-style remote/local browser, default view on connect.
- β‘ Parallel transfers β worker pool (up to 32) with pooled SFTP sessions; concurrent reads for large files.
- π Secure by default β ssh-agent β key β password chain; strict
known_hostswith TOFU prompt and MITM hard-fail. - π SSH config aware β
~/.ssh/configaliases + single-hopProxyJump. - π¨ Syntax-highlighted preview β chroma-powered, lazy paging.
- βοΈ External editor round-trip β
eopens remote file in$EDITOR, re-uploads on save.
Install
Homebrew (macOS / Linux)
brew install art-ps/tap/sftpcommander
Go
go install github.com/art-ps/sftpcommander@latest
Requires Go 1.25 or newer.
Pre-built binaries
Grab a tarball from the releases page β darwin/linux Γ amd64/arm64.
From source
git clone https://github.com/art-ps/sftpcommander.git
cd sftpcommander
go build -o sftpcommander .
Quick start
sftpcommander
The app opens on the connection list, merging entries from
~/.config/sftpcommander/connections.toml and ~/.ssh/config Host aliases.
nβ new connection form.enterβ connect (auth via ssh-agent β key β password).- After a successful connect, you land in two-pane mode (local on the
left, remote on the right).
F2switches to the classic single-pane view.
Successful connections are auto-saved (deduped by user@host).
Features
Connections & auth
- Saved connections + SSH config β
~/.config/sftpcommander/connections.tomlentries and~/.ssh/configHost aliases shown side by side. - Secure auth chain β ssh-agent (
SSH_AUTH_SOCK) β private key (passphrase-prompted for encrypted keys) β password. - ProxyJump β single-hop
ProxyJumpfrom~/.ssh/config; jump host authenticates via ssh-agent and/or its declaredIdentityFile. - Host key verification β
~/.ssh/known_hostsenforced. Unknown hosts get a TOFU prompt with the SHA256 fingerprint; changed keys hard-fail with a MITM warning. Key-algorithm rotation (e.g. RSA β Ed25519) is detected and silently negotiated.
Transfers
- Parallel batch transfers β worker pool (default 4, cap 32) flattens directory trees into a flat task queue. Many small files transfer in parallel; large files use concurrent reads (
sftp.UseConcurrentReads). Workers share a pooled set of SFTP sessions so reuse is free across batches. - Parallel pre-scan β directory walk during transfer also runs in a worker pool, so deep trees stop blocking on the first slow
ReadDir. - Overwrite prompt β per-file
replace / skip / resume / abortdecision when the destination exists, with sticky "all" variants to apply the same choice to the rest of the batch. - Optional SHA256 verification β hashes each file post-transfer (
sha256sumover an ssh session) and surfaces mismatches as failures. - Cross-pane copy + move β
F5copies,F6moves (same-FS only β atomic rename). - Remote-side copy β in single-pane,
F5runscp -Rover an ssh session, so bytes never round-trip through the client.
Browsing
- Two-pane mode (mc-style) β default after connect.
Tabswitches focus,=aligns the inactive panel,Ctrl-Uswaps panels. - Recursive find β
Fwalks the cwd subtree and shows matches relative to the search root. Patterns: substring, glob, orre:-prefixed regex. - External editor β
edownloads the remote file to a temp path, opens$EDITOR, re-uploads on save (mtime-checked). - Syntax-highlighted preview β first 256 KB rendered via chroma (lexer chosen from filename), line numbers, binary detection.
mloads the next 256 KB chunk on demand. - Symlink targets β symlinks show
β targetin the status bar (resolved async, cached per session). - Directory summary β when nothing is selected, status bar shows
N dirs, M files, X MBtotals. - Filter, sort, hidden toggle β live substring filter
/, three sort modes (name/size/mtime), asc/desc, hidden toggle. Filter resets oncd; sorted entries cached. - Bookmarks β per-host path bookmarks (
badd,Blist). - Cursor memory β jumping into a subdirectory and back puts the cursor where it was.
- Multi-select β
spaceselects,aselects all;d/Doperate on the whole selection. - chmod (recursive) β
ccursor entry,Crecursive.Mmkdir;rrename. - Error log β
Eopens an in-app log of failures recorded across operations; survives panel switches.
Keyboard shortcuts
Browser (single-pane & focused two-pane panel)
| Key | Action |
|---|---|
β/β, j/k |
Move cursor |
pgup/pgdn |
Page |
g / G |
Top / bottom |
enter, l, β |
Open directory or preview file |
h, β, backspace |
Parent directory |
R |
Refresh current directory |
space |
Toggle select on cursor |
a |
Select / clear all |
esc |
Clear selection / error / status |
d |
Download selection (or cursor) |
u |
Upload from local path |
e |
Edit remote file in $EDITOR |
v |
Preview text file |
D |
Delete (with confirm; recursive for directories) |
r |
Rename |
M |
Make directory |
c / C |
Change mode (octal; C is recursive) |
F5 |
Remote-side copy (cp -R via ssh) |
/ |
Filter directory |
F |
Recursive find under cwd |
. |
Toggle hidden files |
s / S |
Cycle sort mode / toggle direction |
b |
Bookmark current path |
B |
Open bookmarks list |
F2 |
Toggle two-pane / single-pane |
E |
Open error log |
? |
Help overlay |
q |
Quit |
Two-pane mode
| Key | Action |
|---|---|
tab |
Switch active panel |
F5 |
Copy selection from active panel to the other panel (auto download/upload) |
F6 |
Move selection across panels (same-FS only β atomic rename) |
= |
Align inactive panel to the active panel's path |
Ctrl-U |
Swap panels |
F2, Ctrl-W |
Switch to single-pane browser |
E |
Open error log |
q |
Quit |
All single-panel shortcuts (/, s, D, r, M, c, v, ?, etc.) also
work inside the focused panel. Transfer keys d/u and bookmark keys
b/B are disabled on the local panel.
Preview, connect form, bookmarks, error log
Preview: β/β/pgup/pgdn, g/G scroll. m loads next 256 KB. esc/q/h/β/backspace return.
Connections list: enter connect, n new form, D delete saved entry, R reload.
Connect form: tab/shift+tab move between fields, enter advance/submit, esc back.
Bookmarks list: enter navigate, D delete, esc back.
Error log: β/β scroll, C clear, esc/q close.
Configuration
All state lives under ~/.config/sftpcommander/. Files are TOML, mode 0600,
created on first write.
connections.toml
[[connection]]
name = "deploy@prod.example.com"
host = "prod.example.com"
port = "22"
user = "deploy"
key_path = "~/.ssh/id_ed25519"
Passwords and passphrases are never persisted.
bookmarks.toml
[[bookmark]]
host = "prod.example.com"
user = "deploy"
path = "/var/log/app"
label = "app logs"
Bookmarks are scoped by host + user, so the bookmarks list only shows
entries relevant to the current session.
SSH config
~/.ssh/config is parsed via kevinburke/ssh_config,
which understands Include, wildcard Host patterns, and Match. Aliases
appear in the connection list tagged [ssh]. Honoured directives:
HostNamePortUserIdentityFileProxyJump(single hop)
Other directives (e.g. IdentitiesOnly) are not yet wired in.
Known hosts
~/.ssh/known_hosts is the source of truth:
- Unknown host β prompt with SHA256 fingerprint. On accept the key is appended (one line per
hostname,ip). - Known host, same key β silent.
- Known host, same algorithm, different key β hard fail with MITM warning. Resolution is manual edit of
~/.ssh/known_hosts. - Known host, different algorithm β silently retry with
HostKeyAlgorithmsrestricted to algorithms already on file, so an RSA β Ed25519 rotation does not trip the MITM path.
Environment
| Variable | Effect |
|---|---|
EDITOR |
Editor invoked by e. |
SFTP_NO_NF=1 |
Force ASCII file-icon fallback for terminals without a Nerd Font. |
SSH_AUTH_SOCK |
Used for ssh-agent auth (set by your agent). |
Development
go mod tidy
go build -o sftpcommander .
go vet ./...
go test ./...
Project layout
.
βββ main.go
βββ internal/
β βββ sftp/ # SSH dial, known_hosts, ProxyJump, file ops, parallel batch transfer
β βββ ui/ # Bubble Tea models (connect, browser, download, upload, preview, edit, errlog, etc.)
β βββ config/ # TOML store for connections and bookmarks
βββ .goreleaser.yaml # cross-build + brew tap publishing
βββ go.mod
Releasing
Releases are built by GoReleaser on tag push.
git tag v0.1.0
git push origin v0.1.0
The release workflow builds darwin/linux Γ amd64/arm64 archives, publishes
a GitHub release, and pushes an updated formula to
art-ps/homebrew-tap.
One-time setup (already done for this repo):
- Create the GitHub repos
art-ps/sftpcommanderandart-ps/homebrew-tap. - In
art-ps/homebrew-tap, create an emptyFormula/directory. - Generate a Personal Access Token with
reposcope, save it as theHOMEBREW_TAP_GITHUB_TOKENsecret onart-ps/sftpcommander.
Roadmap
- Multi-hop
ProxyJumpchains - Adaptive theme for light terminals
- Mouse support
- Cross-FS move (currently
F6errors out unless both panels share the same FS)
Contributing
Issues and PRs welcome. Before opening a PR:
go vet ./...clean.go test ./...passing.- Keep changes focused β a one-feature-per-PR policy makes review easier.
- Match the existing style; this codebase favours small, composable Bubble Tea models.
Bug reports: open an issue with the OS, terminal, Go version, and steps to reproduce.
License
MIT β free for any use, including commercial, with attribution.
Acknowledgments
Built on the shoulders of:
- Bubble Tea β TUI framework
- Lipgloss β styling
- pkg/sftp β SFTP client
- chroma β syntax highlighting
- kevinburke/ssh_config β SSH config parsing
- GoReleaser β release automation
Documentation
ΒΆ
There is no documentation for this package.