Edit remote files in Neovim with full LSP and TreeSitter support. This plugin runs language servers directly on remote machines while keeping your editing experience completely local, giving you the best of both worlds: responsive editing with full language features.
[!NOTE] Why this approach wins: You get instant keystrokes and cursor movement (local editing) combined with accurate code intelligence that understands your entire remote project (remote LSP). No more choosing between responsiveness and functionality.
The key insight: Instead of running language servers locally (which lack remote project context) or editing remotely (which has network latency), this plugin runs language servers on the remote machine while keeping file editing completely local.
Local Neovim ââ SSH ââ Remote Language Server
(fast editing) (full project context)
Here's what happens when you open a remote file:
This gives you zero-latency editing with full LSP features like code completion, go-to-definition, and error checking.
ssh user@host (should work without password)Open a remote file:
:RemoteOpen rsync://user@host//path/to/file.cpp
Or browse remote directories:
:RemoteTreeBrowser rsync://user@host//path/to/folder/
Use j/k to navigate, Enter to open files, s to rsync a file/folder to the local machine, q to quit.
Verify it works:
Run TUI applications remotely:
:RemoteTui htop " System monitor
:RemoteTui lazygit " Git interface (https://github.com/jesseduffield/lazygit)
:RemoteTui "tail -f app.log" " Log monitoring
Use Ctrl+H to hide sessions, :RemoteTui (no args) to restore them.
Open a remote terminal:
:RemoteTerminalNew " Opens terminal to current remote host
Use <C-\><C-\> to toggle, picker sidebar for managing multiple terminals.
That's it! The plugin handles the rest automatically.

Ready-to-use configurations for popular language servers:
â Fully Supported & Tested:
đĄ Available But Not Tested:
[!NOTE] If you find that desired LSP is not listed here, try testing it out, if it works (or not), open a GitHub issue and we can get it added to this list with the correct status
| Platform | Support |
|---|---|
| Linux | â Full |
| macOS | â Full |
| Windows | đĄ WSL recommended |
Using lazy.nvim
{
"inhesrom/remote-ssh.nvim",
branch = "master",
dependencies = {
"inhesrom/telescope-remote-buffer", --See https://github.com/inhesrom/telescope-remote-buffer for features
"nvim-telescope/telescope.nvim",
"nvim-lua/plenary.nvim",
"neovim/nvim-lspconfig",
-- nvim-notify is recommended, but not necessarily required into order to get notifcations during operations - https://github.com/rcarriga/nvim-notify
"rcarriga/nvim-notify",
},
config = function ()
require('telescope-remote-buffer').setup(
-- Default keymaps to open telescope and search open buffers including "remote" open buffers
--fzf = "<leader>fz",
--match = "<leader>gb",
--oldfiles = "<leader>rb"
)
-- setup lsp_config here or import from part of neovim config that sets up LSP
require('remote-ssh').setup({
on_attach = lsp_config.on_attach,
capabilities = lsp_config.capabilities,
filetype_to_server = lsp_config.filetype_to_server
})
end
}
Using packer.nvim
use {
'inhesrom/remote-ssh.nvim',
branch = "master",
requires = {
"inhesrom/telescope-remote-buffer",
"nvim-telescope/telescope.nvim",
"nvim-lua/plenary.nvim",
'neovim/nvim-lspconfig',
},
config = function()
require('telescope-remote-buffer').setup()
-- setup lsp_config here or import from part of neovim config that sets up LSP
require('remote-ssh').setup({
on_attach = lsp_config.on_attach,
capabilities = lsp_config.capabilities,
filetype_to_server = lsp_config.filetype_to_server
})
end
}
For seamless remote development, you need passwordless SSH access to your remote servers:
# Generate SSH key if you don't have one
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy key to remote server
ssh-copy-id user@remote-server
# Test passwordless connection
ssh user@remote-server
You'll need to configure LSP servers for the plugin to work properly. Here's a basic setup:
lsp_util.lua):-- lsp_util.lua
local M = {}
-- LSP on_attach function with key mappings
M.on_attach = function(client, bufnr)
local nmap = function(keys, func, desc)
vim.keymap.set('n', keys, func, { buffer = bufnr, desc = desc })
end
-- Key mappings
nmap('gd', require('telescope.builtin').lsp_definitions, '[G]oto [D]efinition')
nmap('gr', require('telescope.builtin').lsp_references, '[G]oto [R]eferences')
nmap('gI', require('telescope.builtin').lsp_implementations, '[G]oto [I]mplementation')
nmap('K', vim.lsp.buf.hover, 'Hover Documentation')
nmap('<leader>rn', vim.lsp.buf.rename, '[R]e[n]ame')
nmap('<leader>ca', vim.lsp.buf.code_action, '[C]ode [A]ction')
end
-- LSP capabilities
local capabilities = vim.lsp.protocol.make_client_capabilities()
M.capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
-- Server definitions
M.servers = {
clangd = {}, -- C/C++
rust_analyzer = {}, -- Rust
pylsp = {}, -- Python
lua_ls = {}, -- Lua
-- Add more servers as needed
}
-- Generate filetype to server mapping
M.filetype_to_server = {}
for server_name, _ in pairs(M.servers) do
local filetypes = require('lspconfig')[server_name].document_config.default_config.filetypes or {}
for _, ft in ipairs(filetypes) do
M.filetype_to_server[ft] = server_name
end
end
return M
[!NOTE] You will need to manually ensure that the corresponding remote LSP is installed on the remote host
-- In your plugin configuration
{
'williamboman/mason.nvim',
dependencies = { 'williamboman/mason-lspconfig.nvim' },
config = function()
require('mason').setup()
require('mason-lspconfig').setup({
ensure_installed = vim.tbl_keys(require('lsp_util').servers),
})
end
}
Install the required language servers on your remote development machines:
# On remote server
pip3 install python-lsp-server[all]
# Optional: for better performance
pip3 install python-lsp-ruff # Fast linting
# Ubuntu/Debian
sudo apt install clangd
# CentOS/RHEL/Rocky
sudo dnf install clang-tools-extra
# macOS
brew install llvm
# Arch Linux
sudo pacman -S clang
# Install via rustup (recommended)
rustup component add rust-analyzer
# Or via package manager
# Ubuntu 22.04+: sudo apt install rust-analyzer
# macOS: brew install rust-analyzer
# Arch: sudo pacman -S rust-analyzer
# Ubuntu/Debian (if available in repos)
sudo apt install lua-language-server
# macOS
brew install lua-language-server
# Or install manually from releases:
# https://github.com/LuaLS/lua-language-server/releases
# Install Java first
sudo apt install openjdk-17-jdk # Ubuntu
brew install openjdk@17 # macOS
# jdtls will be automatically downloaded by Mason
# Install via pip
pip3 install cmake-language-server
# Or via package manager
sudo apt install cmake-language-server # Ubuntu 22.04+
Ensure your remote systems have the following:
# Check Python 3 availability
python3 --version
# Check rsync availability
rsync --version
# Verify SSH server is running
systemctl status ssh # Ubuntu/Debian
systemctl status sshd # CentOS/RHEL
# Test SSH access
ssh user@remote-server "echo 'SSH working'"
Here's a default configuration with comments explaining each option:
require('remote-ssh').setup({
-- Optional: Custom on_attach function for LSP clients
on_attach = function(client, bufnr)
-- Your LSP keybindings and setup
end,
-- Optional: Custom capabilities for LSP clients
capabilities = vim.lsp.protocol.make_client_capabilities(),
-- Custom mapping from filetype to LSP server name
filetype_to_server = {
-- Example: Use pylsp for Python (default and recommended)
python = "pylsp",
-- More customizations...
},
-- Custom server configurations
server_configs = {
-- Custom config for clangd
clangd = {
filetypes = { "c", "cpp", "objc", "objcpp" },
root_patterns = { ".git", "compile_commands.json" },
init_options = {
usePlaceholders = true,
completeUnimported = true
}
},
-- More server configs...
},
-- Async write configuration
async_write_opts = {
timeout = 30, -- Timeout in seconds for write operations
debug = false, -- Enable debug logging
log_level = vim.log.levels.INFO,
autosave = true, -- Enable automatic saving on text changes (default: true)
-- Set to false to disable auto-save while keeping manual saves (:w) working
save_debounce_ms = 3000, -- Delay before initiating auto-save to handle rapid editing (default: 3000)
-- Logging configuration
logging = {
max_entries = 1000, -- Maximum number of log entries to store in memory
include_context = true, -- Include contextual data (URLs, exit codes, etc.) in logs
viewer = {
height = 15, -- Height of log viewer split in lines
auto_scroll = true, -- Auto-scroll to bottom when new logs arrive
position = "bottom" -- Position of split (bottom/top)
}
},
-- Tree browser configuration
tree_browser = {
keymaps = {
rsync = "s" -- Keybind to rsync selected item to local folder
},
rsync = {
default_target = "~/Downloads", -- Default local destination folder
flags = "-avz --progress", -- Default rsync flags
exclude = {} -- Patterns to exclude (e.g., {".git", "node_modules"})
}
}
},
-- Remote TUI session configuration
remote_tui_opts = {
keymaps = {
hide_session = "<C-h>" -- Keymap to hide TUI session (terminal mode)
-- Set to "" to disable
},
window = {
type = "float", -- "float" or "split"
width = 0.9, -- Percentage of screen width (for float)
height = 0.9, -- Percentage of screen height (for float)
border = "rounded" -- Border style for floating windows
},
picker = {
width = 0.6, -- Session picker width
height = 0.6 -- Session picker height
}
},
-- Remote terminal configuration
remote_terminal_opts = {
window = {
height = 0.3, -- 30% of screen height (or absolute lines)
},
picker = {
width = 25, -- Fixed width for picker sidebar
},
keymaps = {
-- Terminal mode keybinds (set to "" to disable)
new_terminal = "<C-\\>n",
close_terminal = "<C-\\>x",
toggle_split = "<C-\\><C-\\>",
next_terminal = "<C-\\>]",
prev_terminal = "<C-\\>[",
},
picker_keymaps = {
-- Picker sidebar keybinds (normal mode)
select = "<CR>",
rename = "r",
delete = "d",
new = "n",
close = "q",
navigate_down = "j",
navigate_up = "k",
},
highlights = {
TerminalPickerSelected = { bg = "#3e4451", bold = true },
TerminalPickerNormal = { fg = "#abb2bf" },
TerminalPickerHeader = { fg = "#61afef", bold = true },
TerminalPickerId = { fg = "#d19a66" },
},
}
})
The plugin includes an intelligent autosave feature that automatically saves remote files as you edit them. This feature is enabled by default but can be customized or disabled:
Enable autosave (default behavior):
require('remote-ssh').setup({
async_write_opts = {
autosave = true, -- Auto-save on text changes
save_debounce_ms = 3000 -- Wait 3 seconds after editing before saving
}
})
Disable autosave while keeping manual saves working:
require('remote-ssh').setup({
async_write_opts = {
autosave = false -- Disable auto-save, but `:w` still works
}
})
Note: Manual saves (:w, :write) always work regardless of the autosave setting. When autosave is disabled, you'll need to manually save your changes using :w or similar commands.
The plugin includes a comprehensive logging system with an interactive log viewer. Logs are stored in a ring buffer (memory only) and can be viewed with :RemoteSSHLog.
Default configuration:
require('remote-ssh').setup({
async_write_opts = {
logging = {
max_entries = 1000, -- Store up to 1000 log entries
include_context = true, -- Include diagnostic context (recommended)
viewer = {
height = 15, -- Log viewer height in lines
auto_scroll = true, -- Auto-scroll to newest logs
position = "bottom" -- Open at bottom of screen
}
}
}
})
View logs:
:RemoteSSHLog " Open interactive log viewer
:RemoteSSHLogClear " Clear all stored logs
:RemoteSSHLogFilter ERROR " Filter by log level
Log viewer keybindings:
1 - Show ERROR only2 - Show WARN and above3 - Show INFO and above4 - Show all (DEBUG+)0 - Clear filterr - RefreshC - Clear all logsg - Toggle auto-scrollq - Close viewerNotification behavior:
:RemoteSSHLog)đĄ Pro tip: Set debug = true and log_level = vim.log.levels.DEBUG to see detailed SSH commands and operations in the log viewer without getting notification spam.
# In your terminal
nvim rsync://user@remote-host/path/to/file.cpp
Or from within Neovim:
:e rsync://user@remote-host/path/to/file.cpp
:RemoteOpen rsync://user@remote-host/path/to/file.cpp
:RemoteTreeBrowser rsync://user@remote-host/path/to/directory
With telescope-remote-buffer, you get additional commands for managing remote buffers:
Default keymaps (configurable during setup as shown above):
<leader>fz - Fuzzy search remote buffers<leader>gb - Browse remote buffers<leader>rb - Browse remote oldfilesThe plugin includes an intelligent file watching system that monitors remote files for changes made by other users or processes. This helps prevent conflicts and keeps your local buffer synchronized with the remote file state.
You can configure the file watcher behavior for each buffer, if you find the defaults are not working for you:
" Set poll interval to 10 seconds
:RemoteWatchConfigure poll_interval 10000
" Enable auto-refresh (automatically pull non-conflicting changes)
:RemoteWatchConfigure auto_refresh true
" Disable file watching for current buffer
:RemoteWatchConfigure enabled false
The file watcher supports SSH config aliases, allowing you to use simplified hostnames:
# ~/.ssh/config
Host myserver
HostName server.example.com
User myuser
Port 2222
Then use in Neovim:
:RemoteOpen rsync://myserver-alias//path/to/file.cpp
Note the double slash (//) format which is automatically detected and handled.
The plugin includes a comprehensive session history feature that tracks all your remote file and directory access, providing quick navigation to recently used items.
:RemoteHistory
Opens a floating window with your session history where you can:
j/k or arrow keys to move through sessionsEnter or Space to open the selected sessionp to pin or unpin sessions/ to enter filter mode, then type to searchq or Esc to close the pickerEach session shows: [PIN] [TIME] [HOST] [ICON] [PATH] [(pinned)]
Example:
âļ đ 12/04 14:30 myserver /home/user/config.lua (pinned)
12/04 14:25 myserver đ /home/user/project
12/04 14:20 devbox đ /app/main.py
12/04 14:15 myserver đ /home/user/README.md
Sessions are automatically tracked when you:
:RemoteOpen or :e rsync://...:RemoteTreeBrowser~/.local/share/nvim/remote-ssh-sessions.jsonDownload files or directories from the remote tree browser to your local machine with real-time progress tracking.
When browsing remote directories with :RemoteTreeBrowser, you can quickly download any file or directory to your local machine using rsync. This is useful for:
:RemoteTreeBrowser rsync://user@host//path/to/folder/s (configurable) to start the rsync downloadDestination Prompt: You'll be prompted to enter the local destination folder
~/Downloads (configurable)Directory Mode Selection (directories only): Choose how to copy the directory:
~/Downloads/myproject/)Progress Window: A floating window shows real-time rsync progress:
Cancellation: Press q in the progress window to cancel the transfer
Customize the rsync behavior in your setup:
require('remote-ssh').setup({
async_write_opts = {
tree_browser = {
keymaps = {
rsync = "s" -- Change the keybind (default: "s")
},
rsync = {
default_target = "~/Downloads", -- Default destination folder
flags = "-avz --progress", -- Rsync flags
exclude = { -- Patterns to exclude
".git",
"node_modules",
"*.pyc"
}
}
}
}
})
For programmatic access, the tree browser module exposes:
local tree_browser = require("async-remote-write.tree_browser")
-- Start rsync for the currently selected item
tree_browser.rsync_selected()
-- Cancel an in-progress rsync operation
tree_browser.cancel_rsync()
-- Check if rsync is currently running
if tree_browser.is_rsync_in_progress() then
print("Rsync is running")
end
Benefits: Run and manage multiple TUI applications (htop, lazygit, yazi, even nvim) on remote machines with session persistence and instant switching. Hide/restore sessions without losing state, perfect for multitasking across different remote tools.
The plugin includes a powerful TUI session management system that lets you run terminal applications on remote servers with full session control - think tmux-like functionality integrated directly into Neovim.
Ctrl+H to hide sessions, restore instantly from pickerCreate a TUI session:
:RemoteTui htop " Run htop on current remote host
:RemoteTui lazygit " Run lazygit for git operations
:RemoteTui "tail -f app.log" " Monitor log files
Manage sessions:
Ctrl+H while in any TUI session:RemoteTui (no arguments)j/k or arrow keys to select sessionsEnter or Space on selected sessiond then y to confirm deletionThe picker shows sessions in this format: [TIME] APP @ HOST:DIRECTORY
Example display:
âļ [12/25 14:30] htop @ myserver.com:~/projects
[12/25 14:25] lazygit @ devbox:~/repo
[12/25 14:20] tail @ production:/var/log
With remote buffer open: Automatically uses the current buffer's connection info Without remote buffer: Prompts for connection details:
user@host format (e.g., ubuntu@myserver.com)~)" Monitor system resources
:RemoteTui htop
" Work with git (hide when done)
:RemoteTui lazygit
<Ctrl+H>
" Check logs while coding
:RemoteTui "tail -f /var/log/app.log"
<Ctrl+H>
" Switch between sessions
:RemoteTui
" Use picker to restore any session
đĄ Pro tip: Each remote host can run multiple concurrent TUI sessions. Use descriptive commands like :RemoteTui "htop -d 1" to distinguish similar tools with different options.
Benefits: Run persistent SSH terminal sessions to remote machines directly in Neovim with a VS Code-style interface. Manage multiple terminals with an integrated picker sidebar, quickly switch between sessions, and keep your workflow contained in your editor.
The remote terminal module provides an integrated terminal experience with SSH connections, automatic context detection, and a visual picker for managing multiple terminal sessions.
<C-\>] and <C-\>[<C-\><C-\>Create a terminal session:
:RemoteTerminalNew " Create new SSH terminal (auto-detects host)
If you have a remote file open or are in the tree browser, the terminal automatically connects to that host. Otherwise, you'll be prompted to enter connection details.
Manage terminals:
<C-\><C-\> (or run :RemoteTerminalToggle)<C-\>n in terminal mode (or run :RemoteTerminalNew)<C-\>] in terminal mode<C-\>[ in terminal mode<C-\>x in terminal mode (or run :RemoteTerminalClose):RemoteTerminalRename [name]Picker sidebar keybinds (normal mode in picker):
Enter - Select/switch to terminaln - Create new terminalr - Rename selected terminald - Delete selected terminalj/k - Navigate up/downq - Close pickerThe terminal split appears at the bottom of the screen with two panes:
+--------------------------------------------------+
| Editor |
+--------------------------------------------------+
| Terminal Output | [1] shell @ host |
| | [2] dev @ server |
| $ ls -la | [3] build @ ci |
| total 48 | |
| drwxr-xr-x ... | [n]ew [r]ename |
+--------------------------------------------------+
" Open remote file, then open terminal to same host
:RemoteOpen rsync://user@server//home/user/project/main.cpp
:RemoteTerminalNew
" Work with multiple terminals
:RemoteTerminalNew " Terminal 1: general work
:RemoteTerminalNew " Terminal 2: build commands
:RemoteTerminalNew " Terminal 3: logs
<C-\>] " Cycle through terminals
<C-\><C-\> " Hide/show terminal split
" Rename for clarity
:RemoteTerminalRename build
:RemoteTerminalNew
:RemoteTerminalRename logs
đĄ Pro tip: The terminal split remembers which terminal was active. Toggle it away with <C-\><C-\> while working, then toggle back to resume exactly where you left off.
| Primary Commands | What does it do? |
|---|---|
:RemoteOpen |
Open a remote file with scp:// or rsync:// protocol |
:RemoteTreeBrowser |
Browse a remote directory with tree-based file explorer |
:RemoteTreeBrowserHide |
Hide the remote file browser |
:RemoteTreeBrowserShow |
Show the remote file browser |
:RemoteTui [app] |
Run TUI application on remote host (with args) or show session picker (no args) |
:RemoteHistory |
Open remote session history picker with pinned items and filtering |
:RemoteGrep |
Search for text in remote files using grep |
:RemoteRefresh |
Refresh a remote buffer by re-fetching its content |
:RemoteRefreshAll |
Refresh all remote buffers |
| Remote History Commands | What does it do? |
|---|---|
:RemoteHistory |
Open session history picker with pinned items and filtering |
:RemoteHistoryClear |
Clear remote session history |
:RemoteHistoryClearPinned |
Clear pinned remote sessions |
:RemoteHistoryStats |
Show remote session history statistics |
| Remote Terminal Commands | What does it do? |
|---|---|
:RemoteTerminalNew |
Create new SSH terminal (uses current remote context or prompts) |
:RemoteTerminalClose |
Close the active terminal |
:RemoteTerminalToggle |
Hide/show the terminal split |
:RemoteTerminalRename [name] |
Rename the active terminal |
:RemoteTerminalList |
List all remote terminals (debug) |
| File Watcher Commands | What does it do? |
|---|---|
:RemoteWatchStart |
Start file watching for current buffer (monitors remote changes) |
:RemoteWatchStop |
Stop file watching for current buffer |
:RemoteWatchStatus |
Show file watching status for current buffer |
:RemoteWatchRefresh |
Force refresh from remote (overwrite local changes) |
:RemoteWatchConfigure |
Configure file watcher settings (enabled, poll_interval, auto_refresh) |
:RemoteWatchDebug |
Debug file watcher SSH connection and commands |
| Debug Commands | What does it do? |
|---|---|
:RemoteLspStart |
Manually start LSP for the current remote buffer |
:RemoteLspStop |
Stop all remote LSP servers and kill remote processes |
:RemoteLspRestart |
Restart LSP server for the current buffer |
:RemoteLspSetRoot |
Manually set the root directory for the remote LSP server, override automatic discovery |
:RemoteLspServers |
List available remote LSP servers |
:RemoteLspDebug |
Print debug information about remote LSP clients |
:RemoteLspDebugTraffic |
Enable/disable LSP traffic debugging |
:RemoteFileStatus |
Show status of remote file operations |
:AsyncWriteCancel |
Cancel ongoing asynchronous write operation |
:AsyncWriteStatus |
Show status of active asynchronous write operations |
:AsyncWriteForceComplete |
Force complete a stuck write operation |
:AsyncWriteDebug |
Toggle debugging for async write operations |
:AsyncWriteLogLevel |
Set the logging level (DEBUG, INFO, WARN, ERROR) |
:AsyncWriteReregister |
Reregister buffer-specific autocommands for current buffer |
:RemoteDependencyCheck |
Check all plugin dependencies (local tools, Neovim, Lua modules, SSH hosts) |
:RemoteDependencyQuickCheck |
Quick dependency status overview with summary |
:RemoteSSHLog |
Open log viewer to see all plugin logs with filtering and context |
:RemoteSSHLogClear |
Clear all stored log entries |
:RemoteSSHLogFilter |
Filter log viewer by level (ERROR, WARN, INFO, DEBUG) |
:TSRemoteHighlight |
Manually enable TreeSitter highlighting for remote buffers |
The plugin includes a comprehensive dependency checking system to help diagnose setup issues and ensure all required components are properly installed and configured.
For a rapid overview of your system status:
:RemoteDependencyQuickCheck
This provides a simple â /â ī¸/â status indicator and tells you if critical dependencies are missing.
For detailed diagnostics and troubleshooting:
:RemoteDependencyCheck
This performs a thorough check of:
Local Machine:
ssh, scp, rsync, python3, statplenary.nvim, nvim-lspconfig, telescope.nvim (optional), nvim-notify (optional)Remote Hosts:
python3, rsync, find, grep, stat, ls~/.ssh/configYou can check specific hosts instead of auto-discovery:
" Single host
:RemoteDependencyCheck myserver
" Multiple hosts
:RemoteDependencyCheck server1,server2,server3
The dependency checker provides color-coded results:
Each failed dependency includes:
The dependency checker will identify issues like:
rsync (prevents RemoteOpen from working)plenary.nvim, nvim-lspconfig)đĄ Pro tip: Run :RemoteDependencyCheck after initial setup to ensure everything is configured correctly, and whenever you encounter issues with RemoteOpen or RemoteTreeBrowser.
Before diving into specific troubleshooting steps, always start with the dependency checker:
:RemoteDependencyCheck
This will identify most common setup issues including missing dependencies, SSH configuration problems, and plugin installation issues.
For diagnosing failures and understanding what's happening behind the scenes, use the log viewer:
:RemoteSSHLog
The log viewer provides:
1 for errors only, 2 for warnings+, 3 for info+, 4 for allj/k to scroll, r to refresh, C to clear logs, q to quitExample workflow:
:RemoteSSHLog1đĄ Pro tip: Keep the log viewer open in a split while working to see real-time errors and warnings.
Symptoms: No LSP features (completion, hover, etc.) in remote files
Solutions:
Check if language server is installed on remote:
ssh user@server "which clangd" # Example for clangd
ssh user@server "which rust-analyzer" # Example for rust-analyzer
Verify Mason installation locally:
:Mason
:MasonLog
Check LSP client status:
:LspInfo
:RemoteLspDebug
Enable LSP debug logging:
:RemoteLspDebugTraffic on
:LspLog
Symptoms: "Connection refused", "Permission denied", or timeout errors
Solutions:
Test basic SSH connectivity:
ssh user@server
Check SSH key authentication:
ssh-add -l # List loaded keys
ssh user@server "echo SSH key auth working"
Verify SSH config:
# Add to ~/.ssh/config
Host myserver
HostName server.example.com
User myuser
IdentityFile ~/.ssh/id_ed25519
Check remote SSH server status:
ssh user@server "systemctl status sshd"
Symptoms: Files won't open, save, or refresh
Solutions:
View detailed error logs:
:RemoteSSHLog
Press 1 to filter errors only and see SSH command failures, exit codes, and stderr.
Check file permissions:
ssh user@server "ls -la /path/to/file"
Verify rsync availability:
ssh user@server "rsync --version"
Test file operations manually:
rsync user@server:/path/to/file /tmp/test-file
Check async write status:
:AsyncWriteStatus
:RemoteFileStatus
Symptoms: "Python not found" or proxy connection errors
Solutions:
Check Python 3 on remote:
ssh user@server "python3 --version"
ssh user@server "which python3"
Verify proxy script permissions:
ls -la ~/.local/share/nvim/lazy/remote-ssh.nvim/lua/remote-lsp/proxy.py
Check proxy logs:
ls -la ~/.cache/nvim/remote_lsp_logs/
Symptoms: No autocomplete suggestions in remote files
Solutions:
Check nvim-cmp configuration:
:lua print(vim.inspect(require('cmp').get_config()))
Verify LSP client attachment:
:LspInfo
Check LSP server capabilities:
:lua print(vim.inspect(vim.lsp.get_clients()[1].server_capabilities))
Symptoms: File watcher shows "not a remote buffer" or doesn't detect changes
Solutions:
Check if file watcher is running:
:RemoteWatchStatus
Test SSH connection manually:
:RemoteWatchDebug
Verify SSH config alias setup:
# Test SSH config alias
ssh myserver "echo 'SSH alias working'"
Check file watcher logs:
:AsyncWriteDebug # Enable debug logging
:AsyncWriteLogLevel DEBUG
Restart file watcher:
:RemoteWatchStop
:RemoteWatchStart
Symptoms: Terminal won't connect or shows SSH errors
Solutions:
Check SSH connection manually:
ssh user@server # Should connect without password prompt
Verify remote context detection:
:RemoteTerminalList " Shows all terminals and their connection info
Check if connection info is being detected:
:RemoteOpen or :RemoteTreeBrowser:RemoteTerminalNew - it should auto-detect the hostTest with explicit connection:
:RemoteTerminalNew without a remote file openSymptoms: Picker sidebar not showing or displaying incorrectly
Solutions:
Check terminal count:
:RemoteTerminalList " Should show at least one terminal
Recreate the split:
:RemoteTerminalToggle " Hide
:RemoteTerminalToggle " Show again
Verify window configuration:
Symptoms: Keybinds not working in terminal mode
Solutions:
i to enter insert/terminal mode:verbose tmap <C-\><C-\>
:lua print(vim.inspect(require('remote-terminal.config').config.keymaps))
Symptoms: File watcher causing UI blocking or performance issues
Solutions:
Increase poll interval:
:RemoteWatchConfigure poll_interval 10000 # 10 seconds
Check for SSH connection multiplexing:
# Add to ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/control-%r@%h:%p
ControlPersist 10m
# LSP Debugging
:RemoteLspDebug # Show remote LSP client information
:RemoteLspServers # List available LSP servers
:RemoteLspDebugTraffic on # Enable LSP traffic debugging
:LspInfo # Show LSP client information
:LspLog # View LSP logs
# File Operation Debugging
:RemoteFileStatus # Show remote file operation status
:AsyncWriteStatus # Show async write operation status
:AsyncWriteDebug # Toggle async write debugging
# File Watcher Debugging
:RemoteWatchStatus # Show file watcher status for current buffer
:RemoteWatchDebug # Test SSH connection and debug file watcher
:RemoteWatchStart # Start file watching for current buffer
:RemoteWatchStop # Stop file watching for current buffer
# Dependency Checking
:RemoteDependencyCheck # Comprehensive dependency check with detailed report
:RemoteDependencyQuickCheck # Quick dependency status check
# General Debugging
:checkhealth # General Neovim health check
:Mason # Open Mason UI for server management
:MasonLog # View Mason installation logs
Use SSH connection multiplexing:
# Add to ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/control-%r@%h:%p
ControlPersist 10m
Configure SSH keep-alive:
# Add to ~/.ssh/config
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Optimize rsync transfers:
# For large files, consider compression
Host myserver
Compression yes
While mounting remote directories (via SSHFS, etc.) is a valid approach, it has several drawbacks:
This plugin runs language servers directly on the remote machine where your code lives, providing a more responsive experience with full access to project context.
Neovim's built-in remote file editing doesn't provide LSP support. This plugin extends the built-in functionality by:
remote-nvim.nvim (https://github.com/amitds1997/remote-nvim.nvim) - The most VS Code Remote SSH-like solution:
distant.nvim (https://github.com/chipsenkbeil/distant.nvim) - Theoretically addresses latency:
This remote-ssh.nvim (https://github.com/inhesrom/remote-ssh.nvim):
:RemoteTreeBrowser)The key trade-off is between feature completeness (remote-nvim.nvim) and responsiveness (this plugin's local buffer approach).
Want to contribute to remote-ssh.nvim? The Docker test container provides a complete local development environment, allowing you to test and develop plugin features without needing a separate remote server. This environment includes pre-configured language servers, test projects, and sample files to validate all plugin functionality.
# Make the build script executable
chmod +x build-docker.sh
# Build and start the container
./build-docker.sh full
# Or use Docker Compose directly
docker-compose up -d --build
# Copy your SSH key to the container (password: testpassword)
ssh-copy-id testuser@localhost
# Or use the automated setup script
./setup-ssh-keys.sh
# Test the connection (should not prompt for password)
ssh testuser@localhost
Open Neovim and try these commands:
" Open a C++ test file with LSP support
:RemoteOpen rsync://testuser@localhost//home/testuser/test-files/main.cpp
" Or browse the test directory
:RemoteTreeBrowser rsync://testuser@localhost//home/testuser/test-files/
" Try a real-world project (LLVM)
:RemoteOpen rsync://testuser@localhost//home/testuser/repos/llvm-project/clang/lib/Basic/Targets.cpp
" Test TUI session management
:RemoteTui htop
/home/testuser/test-files/ (main.cpp, main.py, main.rs)testuser, passwordless access after key setup# Check container status
./build-docker.sh status
# Connect to container via SSH
./build-docker.sh connect
# View container logs
./build-docker.sh logs
# Stop container
./build-docker.sh stop
# Restart container
./build-docker.sh restart
# Clean up everything
./build-docker.sh clean
Once you have remote files open, verify these features work correctly:
Ctrl+Space or configured trigger)gd or configured keybinding to jump to definitionsK to see type information and documentationCtrl+H and :RemoteTuilocalhost22 (default)testusertestpassword (only needed before SSH key setup)/home/testuser/repos//home/testuser/test-files/Container won't start:
# Check if port 22 is already in use
lsof -i :22
# If port 22 is taken, modify docker-compose.yml to use a different port:
ports:
- "2222:22" # Use port 2222 instead
SSH key setup fails:
# Try manual key copy
cat ~/.ssh/id_rsa.pub | ssh testuser@localhost "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# Or generate a new key specifically for Docker testing
ssh-keygen -t ed25519 -f ~/.ssh/id_docker_test
ssh-copy-id -i ~/.ssh/id_docker_test testuser@localhost
LSP features not working:
# Connect to container and verify language servers are installed
ssh testuser@localhost
clangd --version
pylsp --version
rust-analyzer --version
docs/DOCKER_TESTING.md for comprehensive container detailsContributions are welcome! Please read CONTRIBUTING.md for guidelines.
If you feel so inclined, out of appreciation for this work, send a coffee my way! Buy Me a Coffee Link
This project is licensed under the MIT License - see the LICENSE file for details.