Skip to content

[audit] ssh_hosts() doesn't follow Include directives in ~/.ssh/config #347

Open

Description

@stanfish06

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
end

This 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 Include lines (^%s*[Ii]nclude%s+(.+)) the same pass finds Host lines, and recursively read each included path.
  • Include values 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 relative Include path in ssh_config is 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-~/.ssh location).
  • Guard against include cycles (Include can reference config itself, 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/readfile already silently no-op on a missing file, so a glob that matches nothing is naturally fine; just don't error if Include points 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions