What hi supports, and what it doesn’t

hi <name> resolves one name through a ladder — an ssh host first, then the container backends — and lands you in the same styled session either way. Everything hi reaches is above Targets weighed and not shipped; every no, with its reason, is from there down.

Legend: ✅ exercised by a suite on every run · 🟡 expected to work, nobody has proven it · ⚠️ works, reduced · ❌ decided against, not pending — see What would change an answer for the one thing that reopens a row.

Contents

What a “yes” costs

A backend is not one function. A docker-compatible CLI is the exception: podman, nerdctl, and finch share docker’s ps/exec/inspect grammar, so they are one arm and a word in _HI_CONTAINER_CLIS (GLOSSARY: HI.51) — a new drop-in costs that word, not a row here. Anything else touches seven places:

  • a row in _HI_BACKENDS (hi.sh), <name>|<what a target resolves as>|<liveness probe>|<predicate>, walked by the dispatch and by scripts/doctor.sh.
  • a predicate beside _hi_is_nomad_alloc: one _hi_probe_is <literal> <cli> <query...> line, which carries the command -v guard, the timeout, and the muted stderr.
  • an arm in _hi_container_cmds filling probe/cp/attach; past that, everything is backend-agnostic.
  • a lister, a run_lister case, and the usage line in common/targets.sh, in its standalone-POSIX dialect — the only file all three completions read and the only one fish can run.
  • a probe in common/header.sh’s _hi_probe_launch, which names the backend itself because hi.sh is never sourced in a session and sharing the roster would cost payload bytes; tests/hi/parse_test.sh checks the two against each other.
  • an e2e suite in tests/targets/, registered in test_runner.sh’s _HI_TESTS table. A suite that can only ever skip is worth less than none.
  • a fixture that stands the target up — so far always a container image.

Then the part everyone else pays, on machines with none of the runtime: _hi_resolve_backend runs every predicate, one background subshell per row, on every hi <target> that is not an ssh host, and common/targets.sh checks every backend on each uncached TAB (GLOSSARY: HI.26) — there the command -v guard is a builtin, so an absent CLI costs nothing and a present one is one parallel lane.

A row earns a yes by being something people actually sit in, not by being reachable.

What ships

Four arms, one of which answers to four CLIs.

target what a name resolves as proven by
ssh host ✅ a Host entry in ~/.ssh/config, or any name ssh will take tests/targets/ssh_test.sh, plus ssh_disconnect_test.sh (cleanup on an abrupt drop), ssh_relay_test.sh (hi again from inside a session), ssh_wire_test.sh (bytes on the wire vs the printed size), and ssh_keep_test.sh (a kept session, in tmux, screen, and zellij)
docker ✅, podman ✅, nerdctl, finch a running container, through the one docker-grammar arm (all four, always on; GLOSSARY: HI.51) tests/targets/docker_test.sh and podman_test.sh - six shell environments each (bash, bash interactive, zsh, fish, dash, busybox sh), plus docker’s compose-alias, read-only-root, and no-writable-tmp cases. nerdctl and finch have no hosted runner, so those suites prove the arm they share
nomad ✅ a running allocation, or alloc/task tests/targets/nomad_test.sh, against a real nomad agent -dev
kubernetes ✅ a running pod, pod/container, ns:pod, ctx:ns:pod tests/targets/kube_test.sh, against a real kind cluster

ssh is checked first and short-circuits the roster, so a name that is both an ssh host and a container name resolves as the ssh host.

Already covered by those rows, so no row of their own:

  • Anything that is a docker or podman container underneath - distrobox, toolbx, devcontainers (under docker’s vsc-<project>-<hash>-uid name, from outside; a devcontainer you already sit in has no client to say hi from), compose services under docker and podman (hi web resolves the com.docker.compose.service label; the same service in two projects resolves to neither, on purpose), and any remote docker context, since the arm shells out to whatever docker is on $PATH. Sharing your real $HOME costs nothing: hi writes to no login file on a target.
  • Anything that ends in a real OpenSSH connection - AWS SSM, gcloud compute ssh, fly ssh console, Azure Bastion, multipass, Vagrant, Codespaces, Lima/Colima, OrbStack, Tailscale SSH, Teleport (tsh). A ProxyCommand is still OpenSSH, which is all hi’s ControlMaster multiplexing (_hi_ctl_open) needs, and why mosh and Eternal Terminal cannot be ridden. Most emit the Host block themselves: vagrant ssh-config, gh codespace ssh --config, limactl show-ssh --format config <vm>, tsh config; OrbStack writes ~/.ssh/config on its own.

The target’s OS

Can hi land a session there at all? ci.yml calls every workflow named below on each push to main and same-repository pull request.

target OS result proven by
Linux, glibc (Debian/Ubuntu/Fedora/Arch…) ✅ full session tests/targets/ssh_test.sh, on Debian bookworm; install_methods_test.sh on Fedora too
Linux, musl + busybox (Alpine…) ✅ full session with bash installed, ⚠️ aliases-only without ssh_test.sh, on Alpine 3.24; the client half by ci.yml’s fast suites (Alpine client), in an Alpine container
macOS ✅ full session, client included — it ships bash 3.2 ssh_test.sh’s bash-3.2 target, and ci.yml’s fast suites (macos-latest): the fast suites, then (pushes and same-repo PRs) Apple’s /bin/bash 3.2 as the client into the runner’s own sshd - a one-shot command, a full session whose header says macOS, and --doctor
WSL ✅ full session — it is Linux, and the package layout installs into it unchanged .github/workflows/windows-e2e.yml’s wsl-suites job: the fast suites inside an Ubuntu WSL distribution, sharded, and on the first shard the --prefix package layout, then hi into it from Git Bash
Windows, with Git Bash/Cygwin/MSYS2 on PATH ✅ as a client under Git Bash; 🟡 as a target, the same code path as any ssh host with bash .github/workflows/windows-client.yml (the fast suites under Git Bash, x64 and arm64), and Git Bash as client and target in windows-e2e.yml (git-bash-target: sshd’s DefaultShell set to Git Bash)
Windows, stock OpenSSH (cmd.exe/PowerShell) ⚠️ plain PowerShell session, no hi styling — a deliberate fallback, not a failure windows-e2e.yml’s stock-openssh job
*BSD, Solaris/illumos ✅ FreeBSD and OpenBSD, full session, client included; OpenBSD with the bash package (LibreSSL’s openssl stands in for base64); 🟡 the rest, which share that userland .github/workflows/freebsd-e2e.yml and openbsd-e2e.yml: the fast suites plus a loopback session, in a VM
NAS: Synology DSM, QNAP QTS, TrueNAS SCALE/CORE, Unraid 🟡 full session expected - DSM and QTS ship bash beside a busybox sh with the base64 and mktemp the bootstrap needs; SCALE is Debian, Unraid is Slackware with bash as its shell, CORE is FreeBSD with a bash in base the Alpine, glibc, and FreeBSD rows above prove each shape; nobody has run it on an appliance, and the local-install recipe is unverified
OpenWrt (and other busybox routers) ⚠️ aliases-only — busybox ash, no bash, base64 and mktemp present; opkg install bash makes it a full session. /tmp is RAM, and one payload a connect is fine there 🟡 the same shape as Alpine’s bash-less case in ssh_test.sh; not run on a router
Termux (Android) 🟡 as a client, bash and coreutils base64 are there; install as usual (the link lands in ~/.local/bin, which Termux puts on $PATH). 🟡 as a target, over Termux’s own sshd (port 8022): full session, bash is Termux’s shell — the adb row is the other direction and stays a no
any of the above behind sshd ForceCommand, or a command= key ⚠️ the host’s own session — the forced program — after a line saying hi’s bootstrap never ran; a forced program that exits non-zero and prints nothing gets the PowerShell notice instead ssh_test.sh’s two forced-command cases
any of the above with an rbash login shell, or MaxSessions 1 ✅ full session - sh has no / in its name, so rbash runs the bootstrap unrestricted (not a boundary hi respects, SECURITY.md); the bootstrap’s channel closes before the session’s opens ssh_test.sh’s rbash and maxsessions1 cases

A local install on a NAS

Untested — drafted from the install path, hence the NAS row’s 🟡; what it gets wrong is a bug report.

A tree on the NAS is for its own shells: ssh nas and the appliance’s terminal get the header, prompt, and aliases without a hi in front. A hi from your laptop still sends the payload, as to every ssh target. Two rules set the shape:

  • Not scripts/install.sh --prefix. Packaging mode writes /usr/bin/hi and /etc/profile.d/say-hi.sh (RELEASING.md records the two paths as hardcoded), and appliance firmware owns both: a DSM or QTS upgrade rewrites the system partition and takes them with it.
  • The home directory on the data volume survives upgrades. DSM keeps it at /volume1/homes/<user> once the user home service is on (without it there is no $HOME, and no hi session at all), QTS at /share/homes/<user>; SCALE, Unraid, and CORE have ordinary homes.

So the recipe is the README’s plain in-place install, done on the NAS as the user you ssh in as (a release tarball where git is not a given - on DSM it is the Git Server package), and everything it writes (settings.sh, the ~/.local/bin/hi link) stays on the data volume. On DSM a non-admin login lands in /bin/sh and never reads ~/.bashrc, so check which rc file that account reads before expecting the install’s line to load.

The shell you end up in

session shell result note
bash ≥ 3.2 ✅ full: header, prompt, git status, aliases, editor configs 3.2 is the floor because macOS still ships it
zsh ≥ 5.5 ✅ full common/zsh.zsh, and common/_hi on $fpath for completion, which the rc’s own compinit registers; the floor is tests/lint/dialects_test.sh’s pinned 5.5.1 image (RHEL 8’s)
fish ≥ 3.4 ✅ full common/config.fish; the same suite parses it in pinned 3.4 (floor) and 4.x (ceiling) images. An older fish gets a one-line notice, and a session the next shell down
sh/dash/ash (no bash on the target) ⚠️ aliases and a colored user@host cwd prompt, with a warning saying so no header and no git segment - those need bash
nushell, ion, elvish, xonsh, tcsh, PowerShell ❌ decided against, not pending why. You still get a session: the best of $_HI_SHELL_TREE the target has, or with no sh at all, plain PowerShell

A shell framework loads normally on a target ✅ — oh-my-zsh, powerlevel10k, starship, bash-it, oh-my-bash, tide, powerline-go, fzf, zoxide, direnv, atuin, and mise, each in tests/targets/framework_test.sh. What hi does alongside each is INTEGRATIONS.md.

Targets weighed and not shipped

Everything weighed and left off the roster above. Each would need everything in What a “yes” costs.

target why
systemd-nspawn / machinectl machinectl shell goes through systemd-machined, so it wants root or a polkit prompt on the host, and the few people who sit in an nspawn container long enough to want their aliases do not outweigh another probe on every TAB for everyone else: ideal targets, failing audience
WSL (wsl -d <distro>) reachable only from a Windows client, and a distribution is a machine you install say-hi into (the OS table)
crictl / CRI-O a node-level debugging tool, not a place people sit; the session you want is the pod, which the kubernetes row resolves. tests/targets/kube_test.sh uses crictl to preload images — about the right relationship to it
Apptainer / Singularity HPC containers are mostly run-to-completion jobs, so there is usually nothing to exec into; where there is, the culture is batch schedulers and srun
Proxmox pct enter LXC underneath, reachable only from the PVE node as root — the lxc row below with a narrower door
FreeBSD jails (jexec), illumos zones (zlogin) host-local and root-only: you ssh to the host first, where hi is already running. The transports are also unlike the container arms’ — no unprivileged listing, no unprivileged liveness probe
chroot no isolation worth the name, nothing to enumerate, root-only to enter, and hi’s disposable tree lands inside the chroot anyway
adb shell (Android), the closest call here mechanically the best fit on this page: adb shell/adb push/adb devices map onto probe/cp/attach almost exactly, and the CLI is one static binary. The other end fails: Toybox with no bash and no package manager to get one, so every session lands in the aliases-only tier by construction, and $HOME is /data/local/tmp at best. hi would reach it and have almost nothing to do there
AWS ECS Exec (aws ecs execute-command) a real exec shape with a real audience, and a name hi cannot take: a task is a cluster/task/container triple, not one word. It also needs the Session Manager plugin beside the CLI, so command -v aws would not be honest about whether the backend works
Slurm (srun --pty bash) srun allocates rather than attaches: hi <job> would queue a job on a scheduler, which nothing else on this page does. The machine people want styled is the login node they submit from, already an ssh host
Docker Swarm services, Azure Container Instances (az container exec), systemd-run / portable services none has shown an audience that sits in it: Swarm is largely superseded by the kubernetes row, ACI is run-a-container-and-go, systemd-run launches a unit rather than being a place to find one
Talos Linux and other shell-less immutable distributions no shell to style, by design: the node exposes an API, not a login, and talosctl has no exec-a-shell verb because there is no /bin/sh. Where such a node runs pods, the kubernetes row answers
Serial consoles (picocom, virsh console), telnet no file transfer channel at all, disqualifying as no other row is: hi’s first move is landing $_HI_PAYLOAD on the far end, and a serial console offers nothing to land through short of typing base64 at a getty
WinRM / PowerShell Remoting the answer the OS table gives stock Windows OpenSSH: PowerShell can neither source hi’s POSIX payload nor run its fallback ladder; Git Bash on PATH is the supported shape
lxc / incus (and LXD) the closest shape to a fit: a full system container running a real distro, and lxc exec <name> -- <cmd> is an ordinary probe/cp/attach triple. Two things decide it. The suite could only ever skip: every other backend suite stands its target up from a container image; LXD and Incus want a real daemon and a storage pool on the runner. And the door is already open: a system container people sit in is either running sshd, which the ssh row answers, or a machine you install say-hi into. That leaves another fork on every hi <target> and every TAB, charged to everyone, to save an ~/.ssh/config entry

Every one of these still works from the other side: ssh into the host and run hi there if say-hi is installed, or accept the host’s own shell.

Shells hi does not style

Each would need its own rc in common/ (prompt, aliases, completion) plus a tier in the fallback ladder in hi.sh’s _hi_remote_suffix and load.sh’s load(). Using one of these as a login shell still works - only the session shell is limited.

shell why
elvish its own language, so the prompt and aliases would be a second implementation to keep in sync forever, for an audience hi has no evidence of. A common/rc.elv is what it would take, and nobody has asked
xonsh Python — a third implementation, on elvish’s terms
tcsh/csh different rc syntax and no $ENV equivalent, so there is no hook to land on: it would need its own rc and its own delivery mechanism
nushell, ion not POSIX, so neither can source any of common/
ksh/mksh they land in the sh tier like any bash-less shell — aliases and the colored prompt, no header. A ksh tier for a live git segment is not worth a second POSIX implementation
PowerShell not a POSIX shell; the greeting hi prints there is the whole extent of it

What would change an answer

A “no” above is closed, not permanent, and the one thing that reopens it is the same in every section: evidence of people sitting in it — who is in these, how often, and what they do today instead — enough to be worth what a yes costs everyone who has never heard of it. A new exec CLI, a cleaner API, or an easier integration moves nothing, because nothing here is a “no” for being hard.

One proposal about hi itself was declined: a bash 4 floor for the client — the 3.2 plumbing is tested on both ends, and a split floor is two dialects in one tree.


This site uses Just the Docs, a documentation theme for Jekyll.