Configs for my neovim and alactritty setup
$ rsync -av --exclude=".git" ~/.config/nvim .neovimconfig is copied from this repo, and modified
scripts/osc52/ makes a yank inside vim or neovim on a remote SSH host land in the
local machine's clipboard, using the OSC 52 terminal escape sequence. No daemon, no
port forwarding, no X11 forwarding — the sequence rides the existing SSH pty and the
local terminal writes the clipboard. Tested with kitty and Ghostty.
# current user only (default when not root)
curl -fsSL https://raw.githubusercontent.com/riadafridishibly/configs/main/scripts/osc52/osc52-standalone.sh | sh -s -- --user
# system-wide, all users
curl -fsSL https://raw.githubusercontent.com/riadafridishibly/configs/main/scripts/osc52/osc52-standalone.sh | sudo sh -s -- --system
# see what it would do, change nothing
curl -fsSL https://raw.githubusercontent.com/riadafridishibly/configs/main/scripts/osc52/osc52-standalone.sh | sh -s -- --user --dry-run
# report whether it is installed, sourced and armed
curl -fsSL https://raw.githubusercontent.com/riadafridishibly/configs/main/scripts/osc52/osc52-standalone.sh | sh -s -- --user --check
# remove it again
curl -fsSL https://raw.githubusercontent.com/riadafridishibly/configs/main/scripts/osc52/osc52-standalone.sh | sh -s -- --user --uninstallPiping a URL into a shell runs whatever that URL serves at that moment, and sudo sh
runs it as root. Prefer fetching and checking the digest first, especially for the
system-wide install:
curl -fsSL https://raw.githubusercontent.com/riadafridishibly/configs/main/scripts/osc52/osc52-standalone.sh -o /tmp/osc52.sh
shasum -a 256 /tmp/osc52.sh # compare against a known-good digest
sh /tmp/osc52.sh --userReplacing main in the URL with a commit SHA pins the content, so a later push cannot
change what a host installs.
kitty allows clipboard writes by default. Ghostty asks every time, so set:
clipboard-write = allow
Check the whole path before touching any editor config — run this on the remote host, then paste locally:
printf "\033]52;c;$(printf 'hello-from-remote' | base64 | tr -d '\n')\a"--check reports all three failure modes separately — file missing, file present but not
in runtimepath, and sourced but disarmed by a guard. Pass the same scope you installed with:
sh /tmp/osc52.sh --system --checkscope: system, action: check
vim file: present /usr/share/vim/vimfiles/plugin/zzz-osc52.vim
vim status: ACTIVE - sourced and the yank hook is armed
vim sourced from:
/usr/share/vim/vimfiles/plugin/zzz-osc52.vim
The sourced from: list matters when both scopes are installed — it names the file that
actually loaded, so a stale user-scope copy shadowing the system one is visible.
By hand, from inside vim or neovim:
| Command | Meaning |
|---|---|
:echo exists('g:loaded_zzz_osc52') |
1 = the file was found and sourced |
:echo exists(':OSC52Yank') |
2 = guards passed, the yank hook is armed |
:filter zzz-osc52 scriptnames |
which path it was sourced from |
:verbose autocmd TextYankPost |
shows the zzz_osc52 group and its autocmd |
In neovim the first two also work as :lua= vim.g.loaded_zzz_osc52 and
:lua= vim.fn.exists(':OSC52Yank').
loaded set with no :OSC52Yank means a guard bailed out — most often the session is not
SSH, which is expected when testing locally. Force it with :let g:osc52_force = 1 before
the plugin loads, or just test over SSH.
To prove the escape sequence really reaches the terminal, capture the pty:
printf 'yank-me-please\n' > /tmp/probe.txt
script -q /tmp/cap.bin vim -c 'normal! yy' -c 'qa!' /tmp/probe.txt >/dev/null
grep -c $'\033]52;c;' /tmp/cap.bin # 1 = the sequence was emittedYanking works as your normal user but stops after sudo su or sudo -i. Two separate
causes, usually both at once:
- The plugin is not installed for root.
susetsHOME=/root, so a--userinstall under your own$HOMEis invisible to root's vim. Fixed by installing with--system. sudostripsSSH_TTYandSSH_CONNECTION. Withenv_reseton — the default nearly everywhere — only theenv_keepwhitelist survives, and those two are not on it. The plugin then sees a non-SSH session and disarms itself.
--check run as root names the second cause explicitly:
vim status: sourced, but a guard disarmed it
vim reason: SSH_TTY/SSH_CONNECTION unset as root - sudo stripped them.
vim Fix: rerun the installer with --sudoers, or export OSC52_FORCE=1
Confirm it yourself with env | grep SSH before and after sudo su.
Pick one fix:
# preferred: let the SSH variables through sudo, system-wide
sudo sh /tmp/osc52.sh --system --sudoersThat writes /etc/sudoers.d/10-osc52-env containing:
Defaults env_keep += "SSH_TTY SSH_CONNECTION SSH_CLIENT"
The file is validated with visudo -cf before being installed and the whole sudoers set is
re-validated afterwards, with a rollback if it does not parse. A malformed sudoers file
locks everyone out of root, so --sudoers refuses to run at all when visudo is missing.
--uninstall --sudoers removes it again.
Or, without touching sudoers, force the plugin on for root:
echo 'export OSC52_FORCE=1' >> /root/.bashrc$OSC52_FORCE (and the vimscript g:osc52_force) bypass the SSH check. Root can always
write to /dev/tty, so nothing else stands in the way.
| Scope | vim | neovim |
|---|---|---|
--user |
~/.vim/plugin/zzz-osc52.vim |
${XDG_DATA_HOME:-~/.local/share}/nvim/site/plugin/zzz-osc52.lua |
--system |
$VIM/vimfiles/plugin/zzz-osc52.vim |
/etc/xdg/nvim/plugin/zzz-osc52.lua |
The neovim user-scope target is the site directory rather than ~/.config/nvim, so a
git-tracked neovim config stays clean.
The plugin never assigns vim.g.clipboard and never touches the clipboard option. It
only adds a TextYankPost autocmd that emits one extra escape sequence, so an existing
clipboard provider, vim-oscyank, or a distro config such as LazyVim keeps working — the
worst case is the same text sent twice and the terminal keeping the last copy.
It also bails out early when: already loaded (g:loaded_zzz_osc52), vim-oscyank is
present, the session is not SSH (override with g:osc52_force), there is no writable
/dev/tty, or the user opted out with g:osc52_disable / $OSC52_DISABLE. Yanks into
named registers and deletes are ignored, and payloads above g:osc52_max_bytes (74994,
the tmux buffer limit) are refused instead of silently truncated. tmux and screen get
their DCS passthrough wrapping automatically.
osc52-standalone.sh is generated — edit zzz-osc52.vim, zzz-osc52.lua, or
install-osc52.sh, then regenerate and commit the result:
sh scripts/osc52/build-standalone.sh