Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ portable-pty = { path = "vendor/portable-pty" }
# UTF-16 SDDL input for interprocess security descriptors.
widestring = "1.2"
wmi = { version = "0.18.4", default-features = false }
windows = { version = "0.62.2", features = [
"Win32_System_Com",
"Win32_System_Ole",
"Win32_System_TaskScheduler",
"Win32_System_Variant",
] }
windows-sys = { version = "0.61.2", features = [
"Wdk_System_Threading",
"Win32_Foundation",
Expand All @@ -69,6 +75,7 @@ windows-sys = { version = "0.61.2", features = [
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Pipes",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Input_Ime",
"Win32_UI_Input_KeyboardAndMouse",
Expand Down
6 changes: 4 additions & 2 deletions docs/next/website/src/content/docs/connecting-machines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ssh workbox

`workbox` can be a host from your SSH config. You can also use a target such as `ssh://you@server:2222`.

Multi-machine connections are supported on Linux and macOS clients, connecting to Linux and macOS servers on x86_64 or aarch64. Multi-machine connections are not yet verified or supported on Windows; standalone `herdr --remote` remains supported on Windows. Native Windows servers are not supported as SSH targets. See [Remote attach over SSH](/docs/persistence-remote/#remote-attach-over-ssh) for SSH configuration, authentication, and custom binaries.
Saved SSH machines support Linux, macOS, and Windows clients connecting to Linux or macOS hosts on x86_64 or aarch64, and Windows hosts on x86_64. Windows hosts need a compatible Herdr package installed first. See [Remote attach over SSH](/docs/persistence-remote/#remote-attach-over-ssh) for SSH configuration, authentication, desktop access, and custom binaries.

## Add a machine

Expand All @@ -37,6 +37,8 @@ herdr machine add workbox --label "Build machine" --remote-session agents

Herdr checks both the installed binary and the running server. It starts the requested background server before saving the profile. Compatible client and server versions do not have to match. Missing or incompatible installations go through an approval-based setup. When the running server needs replacement, setup asks before stopping it and its pane processes, then starts the compatible server. The default answer is No. If installation and replacement are both needed, one confirmation covers them. `machine add` does not use experimental live handoff. Cancelling or failing setup leaves the profile unsaved.

On Windows targets with desktop hosting support and an eligible signed-in account, setup offers desktop access before launching: **y** for this start only, **a** to remember approval for this machine/account, or **n** (the default) to continue without desktop access. An existing ordinary server stays running; stop it explicitly or choose another session to switch to desktop access. Background reconnect never starts a desktop server, even when approval is remembered.

Run `herdr` to open the UI. If a local client is already open, added and enabled machines normally appear within a second and connect in the background without changing your selection. An in-progress machine switch finishes before profile changes are applied. The remote server keeps running after setup exits.

## Switch between machines
Expand Down Expand Up @@ -97,7 +99,7 @@ Workspace, tab, pane IDs, and agent names are scoped to one server. Two machines

## Updates and saved data

Saved profiles contain only an opaque ID, label, SSH target, explicit remote session, and enabled state. Herdr does not store passwords, private keys, agent tickets, or SSH control sockets in the catalog. Authentication stays with OpenSSH.
Saved profiles contain only an opaque ID, label, SSH target, explicit remote session, enabled state, and required desktop placement. Remembered launch approval is stored separately on the client. Herdr does not store passwords, private keys, agent tickets, or SSH control sockets in the catalog. Authentication stays with OpenSSH.

The client and server negotiate compatibility rather than requiring identical versions. Saved-machine connections additionally need the server's `surface_interest` and `health_check` capabilities. Older servers without those capabilities show Attention until explicitly updated, even if a standalone attach works. Other missing server methods disable only their corresponding actions.

Expand Down
14 changes: 13 additions & 1 deletion docs/next/website/src/content/docs/persistence-remote.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ herdr --remote ssh://you@server:2222

In this mode, the remote server owns the running panes and sends their terminal content and session state over SSH. Your local Herdr draws the UI, including its sidebar, menus, and theme. Because the client runs locally, Herdr can bridge local desktop features such as image clipboard paste into the remote session by copying the image to a remote temp file and pasting that path.

Windows SSH normally starts processes outside the signed-in desktop. With a desktop-capable Windows Herdr package installed, the normal connection command offers desktop access before starting a server:

```bash
herdr --remote windows-workbox
```

Choose **y** to allow this start once, **a** to remember approval for this SSH target and Windows host/account, or **n** (the default) to continue without desktop access. Herdr uses a temporary Task Scheduler task to launch in the account's single active desktop login, then removes the task. Remembered approval applies to future interactive connections; automatic reconnects only attach and never start desktop servers. To forget remembered approvals, remove `remote-desktop-approvals` from the client's state directory.

An existing desktop server is reused. An ordinary server stays running, with an explanation that switching requires stopping it explicitly or choosing another `--session`. Without a unique active desktop login or desktop-capable package, ordinary SSH mode remains available. Desktop discovery must be able to identify active sessions; a query or account lookup failure is reported rather than guessing which login to use. Desktop hosting does not sign in, unlock Windows, or move existing panes between Windows sessions.

Herdr checks `PATH` and the active managed Windows package; it does not install or update Windows packages. `--remote-desktop` remains available when desktop placement is required: declining or finding an ordinary server fails instead of falling back.

By default, `herdr --remote` uses your local Herdr keybindings for that attach. This keeps local muscle memory even when the remote server has different config. After editing local keybindings, use the UI's `reload config` action to apply them without detaching. Use `--remote-keybindings server` when you want the remote server config instead. Local custom command keybindings are not sent, because those commands would run on the remote host.

For repeat targets, use your SSH config:
Expand All @@ -67,7 +79,7 @@ Then attach with:
herdr --remote workbox
```

Remote attach and saved SSH machines support Linux, macOS, and Windows local clients connecting to Linux or macOS hosts on x86_64 and aarch64, or Windows hosts on x86_64. Local and remote versions do not need to match once both support the stable endpoint generation. On Linux and macOS hosts, Herdr prefers a compatible `herdr` already on the remote `PATH`, then checks common direct, Homebrew, mise, and Nix profile install paths. If no compatible binary exists, interactive runs prompt to install one to `~/.local/bin/herdr`; non-interactive runs fail instead of modifying the host. If `~/.local/bin` is not on the remote `PATH`, Herdr warns after install. Windows hosts must already have a compatible Herdr package with remote host support on `PATH`; remote attach does not install or update it.
Remote attach and saved SSH machines support Linux, macOS, and Windows local clients connecting to Linux or macOS hosts on x86_64 and aarch64, or Windows hosts on x86_64. Local and remote versions do not need to match once both support the stable endpoint generation. On Linux and macOS hosts, Herdr prefers a compatible `herdr` already on the remote `PATH`, then checks common direct, Homebrew, mise, and Nix profile install paths. If no compatible binary exists, interactive runs prompt to install one to `~/.local/bin/herdr`; non-interactive runs fail instead of modifying the host. If `~/.local/bin` is not on the remote `PATH`, Herdr warns after install. Windows hosts must already have a compatible Herdr package with remote host support on `PATH` or in the active managed package; remote attach does not install or update it.

By default, `herdr --remote` runs remote setup and the bridge through a temporary SSH config that includes your SSH config first, then adds fallback keepalive settings. Existing user keepalive settings win. Linux and macOS clients also use a private per-attach control socket for connection reuse; Windows OpenSSH does not. Set `[remote].manage_ssh_config = false` to use plain `ssh` without Herdr's generated config or control socket.

Expand Down
5 changes: 4 additions & 1 deletion docs/next/website/src/content/docs/windows-beta.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ For internal testing, `HERDR_MANIFEST_URL` can point the installer at a custom m
| Native panes through ConPTY | supported |
| Windows Terminal / PowerShell app attach | supported |
| `herdr --remote` and saved SSH machines to Linux/macOS/Windows hosts | supported; Windows requires a compatible package with remote host support on `PATH` |
| Remote agents in the signed-in Windows desktop | offered during remote setup; install a desktop-capable package first |
| Remote clipboard images and image-file drops | supported |
| `cmd.exe` panes | supported |
| Native keyboard and mouse input | supported |
Expand All @@ -49,6 +50,8 @@ For internal testing, `HERDR_MANIFEST_URL` can point the installer at a custom m

Local persistent sessions continue running after the client detaches or its terminal window closes. Servers and pane processes launched through Windows OpenSSH also survive logout; run `herdr` again to reconnect.

Windows OpenSSH starts its normal server in Windows Session 0. Normal remote attach and `machine add` offer desktop access before launching when the SSH account has one active desktop login. Choose once, always for this machine/account, or no (the default). Herdr launches through a temporary Task Scheduler task and removes it after readiness. Existing ordinary servers stay running; switching requires an explicit stop or another Herdr session. See [Remote attach over SSH](/docs/persistence-remote/#remote-attach-over-ssh) for remembered approval and reconnect behavior.

Windows agent process detection scans descendants of the pane shell and recognizes direct agents plus common command wrappers, including npm/Node and Git Bash process chains. It follows Git Bash-launched agents across emulated `exec` boundaries, but it is not the same as Unix foreground process-group detection.

Windows integration installation currently supports Pi, OMP, Claude Code, Codex, GitHub Copilot CLI, Devin CLI, OpenCode, Kilo Code CLI, Droid, Kimi Code CLI, Qoder CLI, and Antigravity CLI. Availability is narrower than on Unix; Herdr hides or rejects integrations whose install format is not supported on Windows.
Expand Down Expand Up @@ -130,7 +133,7 @@ From Windows Terminal, use the same remote command as Linux and macOS:
herdr --remote workbox
```

The target host can run Linux, macOS, or Windows. Windows hosts must already have a compatible Herdr package with remote host support on `PATH`; remote attach does not install or update it. Herdr uses the installed Windows OpenSSH client and your SSH configuration. Windows OpenSSH does not use Herdr's Unix control-socket reuse, so key authentication through Windows `ssh-agent` is recommended to avoid repeated prompts during remote setup.
The target host can run Linux, macOS, or Windows. Windows hosts must already have a compatible Herdr package with remote host support on `PATH` or in the active managed package; remote attach does not install or update it. Herdr uses the installed Windows OpenSSH client and your SSH configuration. Windows OpenSSH does not use Herdr's Unix control-socket reuse, so key authentication through Windows `ssh-agent` is recommended to avoid repeated prompts during remote setup.

Windows updates run through the Windows installer and update the active versioned release path. New terminals and reconnected SSH sessions receive that path; start Herdr there to use the updated client. Compatible running servers keep their panes alive. Restart a server later only when you need server-side changes from the release. Live handoff is Unix-only.

Expand Down
36 changes: 27 additions & 9 deletions src/cli/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::client::endpoint::{EndpointCatalog, ProfileId};

const HELP: &str = "Usage:
herdr machine list [--json]
herdr machine add <ssh-target> --label <label> [--remote-session <name>]
herdr machine add <ssh-target> --label <label> [--remote-session <name>] [--remote-desktop]
herdr machine rename <profile-id> --label <label>
herdr machine remove <profile-id>
herdr machine enable <profile-id>
Expand All @@ -14,7 +14,7 @@ Add prepares the remote Herdr installation and starts its server before saving.
Missing or incompatible installations require approval in an interactive terminal.
Changes apply automatically to open local Herdr clients.
Removing or disabling a machine leaves its remote sessions running.
Saved machines contain only a label, SSH target, explicit Herdr session, and enabled state.
Saved machines contain a label, SSH target, explicit Herdr session, enabled state, and desktop hosting choice.
SSH credentials and key material remain owned by OpenSSH.";

#[derive(Serialize)]
Expand All @@ -25,6 +25,7 @@ struct MachineListRow<'a> {
session: &'a str,
enabled: bool,
selected: bool,
windows_desktop: bool,
}

pub(super) fn run_machine_command(args: &[String]) -> std::io::Result<i32> {
Expand Down Expand Up @@ -66,6 +67,7 @@ fn list(args: &[String]) -> std::io::Result<i32> {
session: &profile.session,
enabled: profile.enabled,
selected: catalog.selected_profile.as_ref() == Some(&profile.id),
windows_desktop: profile.windows_desktop,
})
.collect::<Vec<_>>();
if json {
Expand Down Expand Up @@ -94,13 +96,15 @@ struct AddArgs {
target: String,
label: String,
session: String,
windows_desktop: bool,
}

fn parse_add_args(args: &[String]) -> Result<AddArgs, String> {
let args = super::expand_equals_args(args, &["--label", "--remote-session"]);
let mut target = None;
let mut label = None;
let mut session = None;
let mut windows_desktop = false;
let mut index = 0;
while index < args.len() {
let (name, value) = match args[index].as_str() {
Expand All @@ -111,6 +115,12 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, String> {
index += 2;
(args[index - 2].as_str(), value.clone())
}
"--remote-desktop" if !windows_desktop => {
windows_desktop = true;
index += 1;
continue;
}
"--remote-desktop" => return Err("--remote-desktop can only be specified once".into()),
positional if !positional.starts_with('-') && target.is_none() => {
target = Some(positional.to_owned());
index += 1;
Expand Down Expand Up @@ -141,6 +151,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, String> {
target,
label,
session,
windows_desktop,
})
}

Expand All @@ -149,6 +160,7 @@ fn add(args: &[String]) -> std::io::Result<i32> {
target,
label,
session,
windows_desktop,
} = match parse_add_args(args) {
Ok(args) => args,
Err(error) => {
Expand All @@ -157,25 +169,29 @@ fn add(args: &[String]) -> std::io::Result<i32> {
}
};
let mut catalog = load_catalog()?;
match catalog.add_ssh(label.clone(), &target, session.clone()) {
match catalog.add_ssh(label.clone(), &target, session.clone(), windows_desktop) {
Ok(_) => {}
Err(error) => {
eprintln!("error: {error}");
return Ok(2);
}
}
if let Err(error) = crate::remote::prepare_saved_ssh(&target, &session) {
eprintln!("error: {error}; machine was not saved");
crate::remote::print_saved_ssh_error_hint(&error, &target);
return Ok(1);
}
let windows_desktop = match crate::remote::prepare_saved_ssh(&target, &session, windows_desktop)
{
Ok(desktop) => desktop,
Err(error) => {
eprintln!("error: {error}; machine was not saved");
crate::remote::print_saved_ssh_error_hint(&error, &target);
return Ok(1);
}
};
// Setup can wait for human approval. Do not overwrite catalog edits made meanwhile.
let mut catalog = load_catalog().map_err(|error| {
std::io::Error::other(format!(
"remote prepared, but machine was not saved: {error}"
))
})?;
let id = match catalog.add_ssh(label, target, session) {
let id = match catalog.add_ssh(label, target, session, windows_desktop) {
Ok(id) => id,
Err(error) => {
eprintln!("error: {error}");
Expand Down Expand Up @@ -324,6 +340,7 @@ mod tests {
target: "workstation.coder".into(),
label: "coder".into(),
session: session.into(),
windows_desktop: false,
},
"{args:?}"
);
Expand Down Expand Up @@ -373,6 +390,7 @@ mod tests {
session: "agents",
enabled: true,
selected: false,
windows_desktop: false,
})
.unwrap();
assert!(!encoded.contains("password"));
Expand Down
2 changes: 2 additions & 0 deletions src/cli/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ struct ClientStatusJson {
protocol: u32,
endpoint_protocol_generation: u32,
endpoint_capabilities: Vec<&'static str>,
remote_desktop_host: bool,
binary: String,
session: Option<String>,
}
Expand Down Expand Up @@ -297,6 +298,7 @@ fn client_status_json() -> ClientStatusJson {
crate::protocol::endpoint::PRESENTATION_EFFECTS_FENCE_CAPABILITY,
crate::protocol::endpoint::HEALTH_CHECK_CAPABILITY,
],
remote_desktop_host: cfg!(windows),
binary: current_exe_label(),
session: crate::session::active_name(),
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub(super) fn api_client() -> io::Result<ApiClient> {
target.profile.id.as_str(),
&target.profile.target,
&target.profile.session,
target.profile.windows_desktop,
)
.map_err(|error| {
io::Error::new(
Expand Down
2 changes: 1 addition & 1 deletion src/client/catalog_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ mod tests {
let now = Instant::now();
let mut state = state();
let mut catalog = EndpointCatalog::default();
let id = catalog.add_ssh("Build", "build", "main").unwrap();
let id = catalog.add_ssh("Build", "build", "main", false).unwrap();
let remote = ClientEndpointId::Ssh(id.clone());
catalog.select_ssh(&id);
state
Expand Down
1 change: 1 addition & 0 deletions src/client/endpoint/activation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ fn shell_and_registry_with_source_failure(source_fail_after_write: bool) -> Test
target: "dev@example.com".into(),
session: "main".into(),
enabled: true,
windows_desktop: false,
};
let target = ClientEndpointId::Ssh(profile.id.clone());
shell.set_endpoint_catalog(&[profile]);
Expand Down
Loading
Loading