Repository navigation
[audit] ssh_hosts() doesn't follow Include directives in ~/.ssh/config #347
Description
What
ssh_hosts() in lua/config/server.lua:452-469 only reads and parses the top-level ~/.ssh/config file for Host entries. It does not follow Include directives, which OpenSSH's ssh_config has supported since 7.3 (2016) and which are now a common pattern — e.g. ~/.ssh/config.d/*.conf, 1Password's SSH agent config injection, work/personal host-list separation, etc.
Where
lua/config/server.lua:452-469:
local function ssh_hosts()
local hosts, seen = {}, {}
local cfg = vim.fn.expand("~/.ssh/config")
if vim.fn.filereadable(cfg) == 1 then
for _, line in ipairs(vim.fn.readfile(cfg)) do
local names = line:match("^%s*[Hh][Oo][Ss][Tt]%s+(.+)")
if names then
for h in names:gmatch("%S+") do
if not h:find("[*?!]") and not seen[h] then
seen[h] = true
table.insert(hosts, h)
end
end
end
end
end
return hosts
endThis feeds complete_ssh_hosts(), which backs completion for <leader>sr and :NvimRemote (per the module's own doc comment at the top of the file).
Why it matters
Anyone whose ~/.ssh/config uses Include (directly, or indirectly via a tool that manages SSH config, like 1Password, Tailscale SSH, or a dotfiles setup that splits per-host blocks into fragments) will find none of those hosts appear in the hop picker's completion — silently. There's no error or notification; the host list is just incomplete. Since this is the only place host names are discovered for the remote-hop workflow, it's easy to not notice until you specifically go looking for a host that happens to live in an included fragment.
Recommended action (needs judgment, not a one-line fix)
- Parse
Includelines (^%s*[Ii]nclude%s+(.+)) the same pass findsHostlines, and recursively read each included path. Includevalues can contain shell-style globs (e.g.config.d/*.conf) and can be relative to~/.ssh/—vim.fn.glob()with{ expand_relative = true }relative to~/.ssh/would need to be anchored correctly, since a relativeIncludepath inssh_configis resolved relative to~/.ssh, not the including file's directory (which only matters if includes ever nest more than one level deep from a non-~/.sshlocation).- Guard against include cycles (
Includecan referenceconfigitself, directly or via a glob) — track visited absolute paths. - Decide on an include-depth or total-line budget, since OpenSSH itself doesn't bound recursion but a config shouldn't be able to make this hang.
filereadable/readfilealready silently no-op on a missing file, so a glob that matches nothing is naturally fine; just don't error ifIncludepoints at something unreadable.
This isn't a one-line fix since it needs a decision on cycle/depth handling, hence filing as an issue rather than a mechanical PR.
Activity