nixremote

A window from the other machine, right here — not a stream, an app.

A home-manager module around waypipe: an ordered LAN-first/overlay-fallback address cascade, hardware-encoded video by default, best-effort audio routing that follows wherever you actually are, and cleanup for the sessions waypipe itself can't clean up. One option block per peer — no hand-written fish functions, no manual ~/.ssh/config edits.

Status: pre-alpha, verified through the real dispatch path

GPU forwarding, hardware encoding, audio — all live-tested

Not assumed from the docs: a real vkcube and a real Firefox window, GPU selected on the remote end, verified rendering on screen. Hardware H.264 encoding confirmed via a real encode queue on the sending GPU — and confirmed to matter: on an actual video-playing forward, software frame compression cost ~90% of one CPU core (the single largest consumer on the box, ahead of the app itself) and was a reproduced cause of audio stutter in a separate real-time audio tunnel running alongside it. Hardware encoding dropped that to ~6% for the identical workload — why it's the default, not an opt-in.

!

A real course correction, kept in the docs on purpose

The first design assumed a third-party SSH client's documented EnableWaypipe feature would handle waypipe's lifecycle. It doesn't exist in the version this was actually built against — every invocation was silently accepted and did nothing, and the "working" forward was really just an app running invisibly on the remote machine's own screen. Clean exit codes were never proof of anything; only watching the actual pixels caught it. Rebuilt around waypipe's own ssh mode, which needs no third-party client at all.

!

No test suite yet

Every claim on this page was verified live, by hand, against two real machines — not by an automated suite. The address cascade's fallback behavior, orphan reaping, and audio routing have each been forced into their failure path at least once and watched recover; none of that is codified as a test yet.

What it does

Address cascade

Declare an ordered list of candidate addresses per peer — a fast LAN IP first, an overlay/VPN address as fallback. Rendered into native OpenSSH Match ... exec blocks with a bundled TCP-probe script, so a plain ssh or the generated wrapper resolves to whichever address actually answers, with no client-side logic of its own.

Hardware video encoding

video = "h264" by default (also vp9, av1, or none) moves motion content off CPU compression and onto the sending GPU's dedicated encode engine — confirmed live to be the difference between a usable forward and one that visibly starves everything else running alongside it.

Audio that follows you

waypipe forwards Wayland only — audio has nothing to do with it, so a forwarded app's sound plays out of the peer's own speakers by default. audio.localAddress has every launch resolve which of the peer's PipeWire-mesh-mirrored sinks corresponds to your current default output, and routes there instead. Best-effort: no mesh, no match, no problem — it just falls back to today's behavior.

<app>@<peer> dispatch

Optional fish integration: firefox@some-peer works for any app name, matching the tmux@<host> / zellij@<host> convention already in use here — no need to pre-declare every app you might ever forward.

Orphan reaping

waypipe's own cleanup only works when the forwarded app exits on its own — killing the local wrapper (or a dropped network path) leaves the remote side running, a known upstream limitation. nixremote-reap-<peer> finds and kills exactly the orphaned process trees left behind, safe to run any time.

No AUR, no pacman, no dependency

Package provisioning comes straight from nixpkgs via home.packages. Only nixpkgs + home-manager required — usable on any Wayland-capable, home-manager-managed system, not just an Arch/nixarch box.

Get started

1. Add the flake input

inputs.nixremote.url = "github:julian-corbet/nixremote-corbet-ch";

2. Import and declare a peer

imports = [ inputs.nixremote.homeManagerModules.forward ];

nixremote.forward.some-peer.addresses = [
  { address = "192.168.1.10"; }
  { address = "100.64.0.10"; probeTimeoutMs = 500; }
];

3. One manual, one-time step

This module deliberately never touches ~/.ssh/config itself — add this as its first line, once, on each machine:

Include ~/.ssh/conf.d/nixremote.conf

Commands

waypipe@some-peer firefoxForward a native window from some-peer, wherever it currently answers.
firefox@some-peerSame, via the optional fishDispatch integration — any app name, no pre-declaration.
nixremote-reap-some-peerFind and kill orphaned remote process trees left by a killed local wrapper. Safe on demand.

Roadmap (planned, not yet built)

A merged local/remote app library

Invoke an app by name and it transparently runs wherever it actually lives — local if present, else forwarded from whichever peer has it — without the caller needing to know or care which. Whether this is actually needed in practice is genuinely open.

Declarative known_hosts pinning

A programs.ssh.knownHosts-style option, if the standard first-connection trust prompt (now consolidated to once per peer alias) turns out to be more friction than it's worth.

A NixOS-module mirror

For parity with how sibling projects export both layers, if a system-level piece of this ever turns out to be needed — nothing here currently requires root.

Integration tests

Exercising the address cascade against simulated network-partition scenarios, not just eval-time type checks.