sshfs.nvim mounts hosts from your SSH config and makes them feel local.
You can browse, search, run commands, or open SSH terminals across multiple mounts without changing your workflow.
It stays lightweight and modern: no forced dependencies.
Built for Neovim 0.10+ using the best of both sshfs and ssh in tandem with your existing tools.
https://github.com/user-attachments/assets/d6453878-f93b-429a-9f36-8580cd9ed889
ssh_config options via ssh -G; optional per-host default paths.rg/find over SSH (snacks, fzf-lua, telescope, mini) while keeping mounts quiet.:SSHFiles, :SSHGrep, :SSHLiveFind/Grep, :SSHTerminal, :SSHCommand, :SSHChangeDir, :SSHConfig, :SSHReload.vim.uv for reliable jobs, sockets, and cleanup.| Software | Minimum | Notes |
|---|---|---|
| Neovim | >=0.10 |
Requires vim.uv support |
| sshfs | any | sudo dnf/apt/pacman install sshfs or brew install sshfs |
| SSH client | any | OpenSSH with ControlMaster support (default). Socket directory created automatically if missing. |
| SSH config | working hosts | Hosts come from ~/.ssh/config |
[!NOTE] For Mac users, see the macOS setup steps below.
sshfs.nvim supports both macFUSE and the kextless fuse-t implementation.
Install fuse-t and fuse-t-sshfs from the macos-fuse-t/homebrew-cask tap. fuse-t exposes SSHFS mounts through its NFS backend and currently ships an SSHFS 2.9-compatible implementation; sshfs.nvim detects these mounts and translates its directory-cache option names automatically.
{
"uhs-robert/sshfs.nvim",
opts = {
-- Refer to the configuration section below
-- or leave empty for defaults
},
}
use {
"uhs-robert/sshfs.nvim",
config = function()
require("sshfs").setup({
-- Your configuration here
})
end
}
Plug 'uhs-robert/sshfs.nvim'
Then in your init.lua:
require("sshfs").setup({
-- Your configuration here
})
git clone https://github.com/uhs-robert/sshfs.nvim ~/.local/share/nvim/site/pack/plugins/start/sshfs.nvim
init.luarequire("sshfs").setup({
-- Your configuration here
})
You can optionally customize behavior by passing a config table to setup().
[!NOTE] Only include what you want to edit.
Here's the full set of defaults for you to configure:
require("sshfs").setup({
connections = {
ssh_configs = { -- Table of ssh config file locations to use
"~/.ssh/config",
"/etc/ssh/ssh_config",
},
-- SSHFS mount options (table of key-value pairs converted to sshfs -o arguments)
-- Boolean flags: set to true to include, false/nil to omit
-- String/number values: converted to key=value format
sshfs_options = {
reconnect = true, -- Auto-reconnect on connection loss
ConnectTimeout = 5, -- Connection timeout in seconds
compression = "yes", -- Enable compression
ServerAliveInterval = 15, -- Keep-alive interval (15s ร 3 = 45s timeout)
ServerAliveCountMax = 3, -- Keep-alive message count
dir_cache = "yes", -- Enable directory caching
dcache_timeout = 300, -- Cache timeout in seconds
dcache_max_size = 10000, -- Max cache size
-- allow_other = true, -- Allow other users to access mount
-- uid = "1000,gid=1000", -- Set file ownership (use string for complex values)
-- follow_symlinks = true, -- Follow symbolic links
},
control_persist = "10m", -- How long to keep ControlMaster connection alive after last use
socket_dir = vim.fn.expand("$HOME/.ssh/sockets"), -- Directory for ControlMaster sockets
},
mounts = {
base_dir = vim.fn.expand("$HOME") .. "/mnt", -- where remote mounts are created
},
global_paths = {
-- Optionally define default mount paths for ALL hosts
-- These appear as options when connecting to any host
-- Examples:
-- "~/.config",
-- "/var/www",
-- "/srv",
-- "/opt",
-- "/var/log",
-- "/etc",
-- "/tmp",
-- "/usr/local",
-- "/data",
-- "/var/lib",
},
host_paths = {
-- Optionally define default mount paths for specific hosts
-- These are shown in addition to global_paths
-- Single path (string):
-- ["my-server"] = "/var/www/html"
--
-- Multiple paths (array):
-- ["dev-server"] = { "/var/www", "~/projects", "/opt/app" }
},
hooks = {
on_exit = {
auto_unmount = true, -- auto-disconnect all mounts on :q or exit
clean_mount_folders = true, -- optionally clean up mount folders after disconnect
},
on_mount = {
auto_change_to_dir = false, -- auto-change current directory to mount point
auto_run = "find", -- "find" (default), "grep", "live_find", "live_grep", "terminal", "none", or a custom function(ctx)
},
},
ui = {
file_picker = {
preferred_picker = "auto", -- one of: "auto", "snacks", "fzf-lua", "mini", "telescope", "oil", "neo-tree", "nvim-tree", "yazi", "lf", "nnn", "ranger", "netrw"
fallback_to_netrw = true, -- fallback to netrw if no picker is available
netrw_command = "Explore", -- netrw command: "Explore", "Lexplore", "Sexplore", "Vexplore", "Texplore"
},
remote_picker = {
preferred_picker = "auto", -- one of: "auto", "snacks", "fzf-lua", "telescope", "mini"
},
},
debug = {
enabled = false, -- write SSH/SSHFS diagnostics to a log file
log_file = vim.fn.stdpath("log") .. "/sshfs.nvim.log",
max_size = 1048576, -- rotate to <log_file>.1 past this size (0 disables)
},
lead_prefix = "<leader>m", -- change keymap prefix (default: <leader>m)
keymaps = {
mount = "<leader>mm", -- creates an ssh connection and mounts via sshfs
unmount = "<leader>mu", -- disconnects an ssh connection and unmounts via sshfs
unmount_all = "<leader>mU", -- disconnects all ssh connections and unmounts via sshfs
explore = "<leader>me", -- explore an sshfs mount using your native editor
change_dir = "<leader>md", -- change dir to mount
command = "<leader>mo", -- run command on mount
config = "<leader>mc", -- edit ssh config
reload = "<leader>mr", -- manually reload ssh config
files = "<leader>mf", -- browse files using chosen picker
grep = "<leader>mg", -- grep files using chosen picker
terminal = "<leader>mt", -- open ssh terminal session
},
})
[!TIP] The
sshfs_argstable can accept any configuration option that applies to thesshfscommand. You can learn more about sshfs mount options here.In addition, sshfs also supports a variety of options from sftp and ssh_config.
[!NOTE] ControlMaster sockets are stored at
~/.ssh/sockets/%C(configurable viaconnections.socket_dir). The directory is created automatically with proper permissions (0700) during the first connection.
:checkhealth sshfs - Verify dependencies and configuration:SSHConnect [host] - Mount a remote host. Accepts an ssh_config alias or a host that is in no config at all, e.g. :SSHConnect user@example.com:/srv -p 2222. Without an argument the picker offers "Enter host manually..." for the same thing.:SSHTest [host] - Test SSH resolution and authentication without mounting:SSHDisconnect - Unmount current host:SSHDisconnectAll - Unmount all hosts:SSHConfig - Edit SSH config files:SSHReload - Reload SSH configuration:SSHDebug [on|off] - Toggle debug logging for the current Neovim session:SSHFiles - Find files with auto-detected picker:SSHGrep [pattern] - Search files with auto-detected tool:SSHLiveFind [pattern] - Stream remote find/fd results over SSH (snacks/fzf-lua/telescope/mini):SSHLiveGrep [pattern] - Stream remote rg/grep results over SSH (snacks/fzf-lua/telescope/mini):SSHExplore - Open file browser on mount:SSHChangeDir - Change directory to mount (tcd):SSHCommand [cmd] - Run custom command (e.g. Oil, Telescope):SSHTerminal - Open terminal session (reuses auth)Debug logging records SSH authentication, ControlMaster, remote-home resolution, and SSHFS subprocess diagnostics. See docs/debug-logging.md for details.
Default keybindings under <leader>m (fully customizable):
| Mapping | Description |
|---|---|
<leader>mm |
Mount an SSH host |
<leader>mu |
Unmount an active session |
<leader>mU |
Unmount all active sessions |
<leader>me |
Explore SSH mount via native edit |
<leader>md |
Change dir to mount |
<leader>mo |
Run command on mount |
<leader>mc |
Edit SSH config |
<leader>mr |
Reload SSH configuration |
<leader>mf |
Find files |
<leader>mg |
Grep files |
<leader>mF |
Live find (remote) |
<leader>mG |
Live grep (remote) |
<leader>mt |
Open SSH terminal session |
If which-key.nvim is installed, the <leader>m group will be labeled with a custom icon (๓ฐ).
:SSHConnect โ pick a host (or enter one manually) and mount path (home/root/custom/global_paths/host_paths).:SSHFiles, :SSHGrep, or :SSHChangeDir:SSHLiveFind / :SSHLiveGrep (streams over SSH, still mounted):SSHTerminal, :SSHCommand:SSHDisconnect (or let hooks.on_exit.auto_unmount handle it).Auth flow: keys first, then floating terminal for passphrases/passwords/2FA; ControlMaster keeps the session alive across operations.
global_paths with common directories (/var/www, /var/log, ~/.config) to have them available across all hostshost_paths for frequently-used hosts to skip path selectionpreferred_picker for local/remote pickers to force specific file picker(s) instead of auto-detectionRun the unit test suite with:
make test
Tests run in headless Neovim with no external dependencies and never contact a real SSH server or mount table. See tests/README.md for the harness, the available stubs, and how to add coverage. CI runs the suite and stylua --check on every pull request.