DEV Community

Cover image for Your AI coding agent can read your SSH keys. I built a Neovim plugin that won't start it unprotected
Xynorash
Xynorash

Posted on Edited on

Your AI coding agent can read your SSH keys. I built a Neovim plugin that won't start it unprotected

If your editor can launch a coding agent, that agent runs with your privileges: your ~/.ssh, your cloud credentials, your git history. One day I caught myself asking what exactly stops it from touching all that. The answer was "nothing", which wakes you up faster than coffee. So I wrote a plugin that makes my editor refuse to start the agent unprotected.

It is one of nine standalone plugins I extracted from xynovim, my Neovim config for Rust. All are MIT, all need Neovim 0.12+, and you can install any of them without the rest of the config. Splitting them out taught me more than writing them did (my ego has not fully recovered).

xyno-sandbox.nvim: an agent that refuses to start unprotected

The plugin runs a command-line agent inside bubblewrap: a read-only filesystem except the project and a scratch directory, credential folders such as ~/.ssh and ~/.aws replaced with empty ones, secret-looking environment variables removed, and git directories locked read-only.

The design rule is that it fails closed. If no sandbox can be created, the agent does not start, and Windows and macOS are refused rather than silently run without one.

It is also honest about its limits. It is a denylist on a read-only view, the network stays open, and the agent can still read its own token file. It is a seatbelt, not a security boundary against a determined attacker, and the README says so. Yes, I put that in my own README. Marketing departments hate this one trick.

xyno-guard.nvim: auto-save with a conscience

Auto-save plugins are happy to write any buffer. In Rust that goes wrong in three ways: a save lands in ~/.cargo/registry or the standard library sources, a save overwrites a file that git checkout or a formatter just changed on disk, or Neovim stops on a blocking W12 prompt.

The guard records each file's mtime when it is read or written, and refuses to save if disk has moved on. Dependency and standard-library buffers are set to readonly, so auto-save never touches them (:set modifiable still lets you edit one on purpose). It also re-checks at write time, because some auto-save plugins write up to a second after their condition check. It works with any auto-save plugin that takes a condition function. This one was born from pure annoyance, the most renewable energy source in software.

xyno-history.nvim: Local History on plain git

JetBrains has Local History, and I missed it. (Yes, I shamelessly borrowed the idea. JetBrains, if you are reading this, I still use your IDEs every day. Please be gentle.) Every throttled save becomes a commit on a private ref, refs/lochist/<branch>, with the full worktree as its tree. Your index, stash and branches are never touched.

Two details mattered most. Each Neovim process builds its snapshot with its own private index file and updates the ref with a compare-and-swap, so several instances saving at once do not overwrite each other. And because the snapshots are ordinary git objects, you can recover without the plugin: git log refs/lochist/main and git restore --source=refs/lochist/main. Saves are throttled to one snapshot per minute, and size caps (10 MB per file, 200 MB per snapshot by default) keep a stray binary from bloating the repository.

{
  "xynorash/xyno-history.nvim",
  event = "VeryLazy",
  opts = {},
}
Enter fullscreen mode Exit fullscreen mode

xyno-clipboard.nvim: yanks that reach you

Over SSH or inside tmux, a yank lands on the wrong machine. This provider copies with OSC 52 and, when wl-copy is available, also syncs the local Wayland clipboard. It installs only when $TMUX, $SSH_TTY, $SSH_CONNECTION or $HERDR_PANE_ID is set, and falls back to scanning up to 16 parent processes for herdr, so a plain local session is left alone. Not the sexiest plugin of the nine, but my yanks finally arrive where they are going.

The Rust toolkit

The other five get a list instead of a section each (I know, I'm so lazy!). They share one base, xyno-rust.nvim, and install in this order:

  • xyno-rust.nvim: a per-project Cargo context shared with rust-analyzer and bacon-ls, a targets picker, an HTTP route picker
  • xyno-new.nvim: new binaries, libraries, workspaces and egui apps
  • xyno-imports.nvim: auto-import with ranked candidates and a per-project memory
  • xyno-crates.nvim: crates.io search, cargo add, feature toggling
  • xyno-macros.nvim: completion inside derive and helper attributes, macro_rules! signature help, :MacroExpand

lazy.nvim installs the dependencies for you. The plugins page has a complete spec.

What splitting taught me

The parts that looked independent were not. Extracting a plugin forces you to write down what it depends on, and a few things I thought were self-contained quietly needed something else. Writing a README for each also showed me which behaviours I could not explain in a sentence, and those were usually the ones that needed another look. (I started this config as a "quick" project. Narrator: it was not quick.)

Honest limits

  • Every plugin needs Neovim 0.12+.
  • They are developed and tested on Linux. Windows and macOS are unverified for most of them, and xyno-sandbox and xyno-clipboard are Linux only.
  • Test counts vary: xyno-history has 65 specs and xyno-guard has 39, run with nvim -l tests/run.lua.
  • All are at v0.1.0 (xyno-sandbox at v0.1.1), so expect rough edges.

GitHub logo xynorash / xynovim

xynovim: a personal Neovim 0.12 configuration for Rust on LazyVim: instant rust-analyzer and clippy diagnostics, nvim-dap debugger, test UI with coverage, git safety net. Inspired by JetBrains tools. Developed on Linux; Windows passes the portable parts in CI, macOS is not reliably green.

xynovim

A personal Neovim configuration for Rust, built on LazyVim and inspired by JetBrains' tools. Instant diagnostics, a debugger and test UI, a git safety net, and a test suite you can run yourself.

ci neovim 0.12 license tests

Website: https://xynorash.github.io/xynovim/ (with guides to the workflows below)

xynovim editing the netwatch demo crate: Island Dark theme, inlay hints, sticky scope from treesitter-context, statusline with git branch, server state and clippy state

xynovim is a Neovim 0.12 configuration for Rust development. It adds instant diagnostics (rust-analyzer while you type, clippy through bacon-ls half a second later), a debugger (nvim-dap with codelldb), a test UI with cargo-nextest and coverage in the gutter, Cargo workspace, feature and crate helpers, and a git safety net (Local History, lazygit, diffview) to a LazyVim base.

Quick start

git clone https://github.com/xynorash/xynovim ~/.config/nvim
~/.config/nvim/scripts/bootstrap.sh
Enter fullscreen mode Exit fullscreen mode

Then start nvim in a Cargo project (or in demo/netwatch) and run :checkhealth xynovim. This replaces your current Neovim configuration, so move ~/.config/nvim aside first if you have one. Prerequisites and the Windows and macOS status are under Install…

If you try one, I would like to hear what breaks. I read every comment. Tell me what breaks, I have thick skin and a very good test suite. Do you run your coding agent in a sandbox, or trust it with your home directory?

Top comments (7)

Collapse
 
alexshev profile image
Alex Shev •

This is the right framing: secrets access is an execution capability, not an ambient convenience. The strongest version makes the policy visible before launch—what paths are denied, what credentials are available, and how an exception is approved—so the agent and operator can reason about the boundary before an incident.

Collapse
 
xynorash profile image
Xynorash •

Agreed. Right now xyno-sandbox only does the enforcement part: denied paths, stripped env vars, and it won't start if the sandbox fails. Showing that policy before launch, and a logged way to approve exceptions, is the missing half. I'll open an issue for it. Thanks!

Collapse
 
mateo_ruiz_6992b1fce47843 profile image
Mateo Ruiz •

The fail-closed approach is the part I appreciate most here. A sandbox that silently falls back to unrestricted execution can create a false sense of security, especially when agents inherit the developer's privileges. One additional layer worth considering is restricting network access or routing it through an explicit allowlist, since filesystem isolation alone doesn't prevent an agent from sending accessible project data elsewhere. The README's clarity about the limitations is equally important: developers need to understand exactly which risks the sandbox reduces and which ones remain. Great example of treating agent execution as a security boundary rather than just another editor feature.

Collapse
 
xynorash profile image
Xynorash •

Thanks! You’re right that filesystem isolation alone doesn’t stop data leaving over the network. The network is still open today, and the README says so. An allowlist is the obvious next layer, so I’ll add it to the issue.

Collapse
 
reidmarlow profile image
Reid Marlow •

Running the agent under a dedicated user or subuid with an empty home directory is the cleanest seatbelt I've found on Linux. The moment an agent has read access to your actual home directory, path blacklists always leak through temp files, shell history, or subshells. Swapping out ~/.ssh and ~/.aws with empty mounts inside bubblewrap is a solid middle ground when you can't run a full separate user.

Collapse
 
xynorash profile image
Xynorash •

Agreed, a dedicated user with an empty home is cleaner. Blacklists leak, and I say so in the README. The empty mounts are the middle ground for when you can’t set up a separate user. Have you found a way to make the dedicated user easy to launch from the editor?

Collapse
 
xynorash profile image
Xynorash •

I wrote this because I asked myself what stops an agent from reading my ~/.ssh, and the answer was nothing. How do you handle coding agents with access to your home directory?