A fast SSHFS/SSH integration for NeoVim that works with your setup.
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.
sshfs.nvim.mp4
β¨ What's New / π¨ Breaking Changes
π¨ v2.0 Breaking Changes
mounts.unmount_on_exitβhooks.on_exit.auto_unmountmounts.auto_change_dir_on_mountβhooks.on_mount.auto_change_to_dirui.file_pickerβui.local_pickerui.file_picker.auto_open_on_mountβhooks.on_mount.auto_run
- Mounting now tries a non-interactive socket first, then opens an auth terminal. This passes all login responsibility to ssh which enables native support for the ssh authentication flow.
connections.sshfs_optionsmust be a key/value table (e.g.,{ reconnect = true, ConnectTimeout = 5 }); string arrays are ignored.- Strings/numbers render as
key=value - Boolean
true/falseenables/disables options, the value ofnilalso disables
- Commands: The following have beep deprecated:
:SSHEditβ:SSHConfig:SSHBrowseβ:SSHFiles
- API: The following have beep deprecated:
editβconfigbrowseβfileschange_to_mount_dirβchange_dir
- Keymaps: The following have beep deprecated:
open_dirβchange_diropenβfileseditβconfig
- Uses your toolkit β Auto-detects snacks, telescope, fzf-lua, mini, oil, yazi, nnn, ranger, lf, neo-tree, nvim-tree, or netrw.
- Auth that sticks β ControlMaster sockets + floating auth handle keys/passwords/2FA once, then reuse for mounts, live search, terminals, git, or scp.
- Real SSH config support β Honors Include/Match/ProxyJump and all
ssh_configoptions viassh -G; optional per-host default paths. - On-mount hooks β Auto-run find/grep/live find/live grep/terminal or your own function after connecting.
- Live remote search β Stream
rg/findover SSH (snacks, fzf-lua, telescope, mini) while keeping mounts quiet. - Multi-mount aware β Connect to several hosts, clean up on exit, and jump between mounts with keymaps or commands.
- Command suite β
:SSHFiles,:SSHGrep,:SSHLiveFind/Grep,:SSHTerminal,:SSHCommand,:SSHChangeDir,:SSHConfig,:SSHReload. - Host-aware defaults β Optional global and per-host default paths so you can skip path selection on common servers.
- Modern Neovim β Built for 0.10+ with
vim.uvfor 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.
To use sshfs.nvim on macOS, follow these steps:
-
Install macFUSE Download and install macFUSE from the official site: https://macfuse.github.io/
-
Install SSHFS for macFUSE Use the official SSHFS releases compatible with macFUSE: https://github.com/macfuse/macfuse/wiki/File-Systems-%E2%80%90-SSHFS
{
"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
})- Clone the repository:
git clone https://github.com/uhs-robert/sshfs.nvim ~/.local/share/nvim/site/pack/plugins/start/sshfs.nvim- Add to your
init.lua:
require("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"
},
},
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_args table can accept any configuration option that applies to the sshfs command. 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 via connections.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:SSHDisconnect- Unmount current host:SSHDisconnectAll- Unmount all hosts:SSHConfig- Edit SSH config files:SSHReload- Reload SSH configuration:SSHFiles- Find files with auto-detected picker:SSHGrep [pattern]- Search files with auto-detected tool:SSHLiveFind [pattern]- Stream remotefind/fdresults over SSH (snacks/fzf-lua/telescope/mini):SSHLiveGrep [pattern]- Stream remoterg/grepresults 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)
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 and mount path (home/root/custom/global_paths/host_paths).- Work from the mount:
:SSHFiles,:SSHGrep, or:SSHChangeDir- Live remote search:
:SSHLiveFind/:SSHLiveGrep(streams over SSH, still mounted) - Terminals/commands:
:SSHTerminal,:SSHCommand
- Disconnect with
:SSHDisconnect(or lethooks.on_exit.auto_unmounthandle it).
Auth flow: keys first, then floating terminal for passphrases/passwords/2FA; ControlMaster keeps the session alive across operations.
- Use SSH keys for faster connections (no password prompts)
- Configure
global_pathswith common directories (/var/www,/var/log,~/.config) to have them available across all hosts - Configure
host_pathsfor frequently-used hosts to skip path selection - Set
preferred_pickerfor local/remote pickers to force specific file picker(s) instead of auto-detection