say-hi

hi.sh -> sshrc supercharged


EXPERIMENTAL UNTIL v1.0.0-stable RELEASES

NOTE: Project is in active development, many things are subject to change and this current state is not a representation of final, published quality. This is a hobby project.


tests Linux macOS Windows ssh payload package kcov bashcov OpenSSF Scorecard bash license

Both coverage badges are grey because neither number can be taken at face value, and neither gates anything: kcov loses its DEBUG trap the moment the test harness is sourced, so it counts only what ran while things were loading and reads far too low — common/git_prompt.sh shows 2.56% with seventeen cases passing against it. bashcov reads bash’s own xtrace instead and gets that same file right at 92.68%, but it counts every line of a heredoc body as covered, so hi.sh — which builds the entire remote script out of heredocs — reads far too high, reporting 100% for two connect functions the fast suites never call.

One config directory to rule them all, uniting all shells from all hosts!

Don’t sshush your hosts, say hi!

hi connecting to a container: banner, header, packages check, colored prompt, and the cleanup on exit

Contents

Every target, the same session

The pitch is that hi behaves identically whatever is on the other end — an ssh host, a container, an allocation, a pod — and whatever shell each side runs. One GIF per backend, deliberately varying both sides, and each one configured differently: the line under every GIF names the knob it is showing, so the set reads as a configurable tool rather than one fixed look. The GIF at the top of this README is the exception on purpose — it is the stock defaults, with nothing turned off. A last GIF closes the section without being a backend at all: completion, which answers with every one of them at once. How they are rendered, and what to catch when you regenerate them, is in docs/PACKAGING.md.

ssh, with a permanent install

The target carries its own ~/say-hi, so nothing ships over the wire — hi loads the tree in place and leaves it alone on exit. Client: bash. Showing _HI_HEADER_TIMESTAMP=0 and _HI_HEADER_SYSINFO=0 — set on the box, not the client: a permanent install reads its own config, so this is the demo whose knob lives on the target.

hi over ssh into a host with a permanent ~/say-hi

docker

A debian/bash container, then an alpine box whose only real shell is zsh — hi probes and falls back without being told. Client: zsh. Showing _HI_PROMPT_END_ZSH and _HI_HEADER_CHECK=0 — the same debian target as the GIF at the top, styled differently from the client side.

hi into a debian container, then an alpine zsh-only container

podman

A fish-only alpine container from a fish client: no bash anywhere in the loop. Same session, same code path as docker. Showing _HI_PROMPT_END_FISH — fish’s own prompt separator.

hi from fish into a fish-only alpine container via podman

nomad

A dev agent, one docker-driver job, and hi <alloc-id-prefix> straight into the allocation. Client: bash. Showing _HI_HEADER_GHZ=1 and _HI_HEADER_IDENTITY=0 — the CPU line in GHz, the identity row off.

hi into a nomad allocation by ID prefix

kubernetes

A kind cluster and a bare alpine pod — busybox ash is all it has, which is hi’s aliases-only fallback. Client: zsh. Showing _HI_DISABLE_GIT_STATUS=1 — the same prompt, without the git segment.

hi into a kubernetes pod on a kind cluster

completion, every backend at once

Not a session — the roster the sessions come from. hi <TAB> answers with the Host entries in ~/.ssh/config and every running container, allocation and pod, each tagged with the backend it came from; hi --<TAB> answers hi’s own flags, without probing any backend to do it. Client: fish, for the description column its pager gives every row.

hi TAB listing ssh hosts and containers from every backend, then hi --TAB listing flags

The list stops at eleven rows because fish hands its pager half the screen, so two ssh hosts spill into “…and 1 more row”. That is completion behaving normally, not the GIF cut short.

Requirements

Installation/Usage


Usage: hi foo (just like ssh!)


Reminder — place local only changes after the “# hi-config-end” comment in the local files.

Configuration

Your config lives outside the checkout, in ${XDG_CONFIG_HOME:-$HOME/.config}/say-hi/, and rides along to every host you say hi to in its own small archive — colors, packages and aliases.sh overlay the tree’s copies one file at a time, and settings.sh (what hi --configure writes) has no in-tree counterpart at all. The full picture — the overlay file table, every _HI_DISABLE_* feature toggle, the header-line toggles, and every other environment variable hi reads (_HI_SHELL_PREFERENCE, _HI_PROMPT, _HI_ASCII, _HI_HEADER_GHZ, …) — is in docs/CONFIGURATION.md.

IMPORTANT: Local-only changes MUST stay in ~/.bashrc, ~/.zshrc, ~/.config/fish/config.fish, etc. — anything in ${XDG_CONFIG_HOME:-$HOME/.config}/say-hi/ is copied to every host you say hi to.

How a session actually gets there — what is packed, how it travels, which shell you land in and what is left behind — is How it works there too.

Hostname, username, and group/tag colors

Every username and hostname gets a color deterministically derived from its name — nothing to generate, nothing that can go missing. To pin one instead, add a line to say-hi/misc/colors (username,root,red / hostname,prod-db,yellow / hosttag,desktop,green); hosttag entries match the leftmost tag in a # Tags: ... comment directly above a Host line in ~/.ssh/config. hi --color-preview shows what every ssh host and your user currently resolve to, in their actual colors.

Built from/with/in mind

Docker / Podman containers

hi <name> also works against a running docker or podman container. If <name> isn’t a Host in ~/.ssh/config but is a running container (by name or ID, docker checked first), hi copies its tree in and chainloads load.sh exactly as the ssh path does, for an identical session. No armoring is needed (docker exec -i/podman exec -i pass stdin as raw bytes), and cleanup happens on exit. Podman’s CLI is close enough to reuse the same command shapes. The container needs bash for the full experience; without it hi drops you into the best plain shell $_HI_SHELL_LADDER finds there, with the aliases and a warning.

Nomad allocations

hi <alloc-id> also works against a running Nomad allocation (matched by ID/prefix, after the ssh-host and container checks) — same session, same code path as docker. Since nomad alloc exec has no docker cp/-e equivalent, files stream in with exec -i + cat > and env vars go through a sh -c "export ...; exec ..." wrapper. A multi-task allocation picks its task with hi <alloc-id>/<task>, which becomes nomad alloc exec -task <name>; a plain hi <alloc-id> is unchanged, and completion offers the pairs for any allocation that has more than one task.

Kubernetes pods

hi <pod-name> also works against a running Kubernetes pod (checked last, after ssh/docker/podman/nomad) — same idea again, using kubectl exec with -- separating its own flags from the remote command. Uses whatever context/namespace your kubectl is currently pointed at; a multi-container pod picks its container the same way Nomad’s tasks do — hi <pod>/<container>, which becomes kubectl exec -c <name>. Without the suffix kubectl still falls back to the pod’s first container with a warning, so the suffix is how you say which one you meant; completion offers pod/container for every pod that has more than one.

Windows hosts

hi <target> works against Windows OpenSSH targets too, at whatever level the target supports:

Installing hi on Windows: use WSL. The .deb from the releases page installs into a WSL distribution unchanged — /etc/profile.d/say-hi.sh, /usr/bin/hi, everything as on any Debian — and WSL is where a Windows developer already using ssh/docker/kubectl most likely works.

say-hi and the alternatives

How say-hi compares to sshrc, xxh, kyrat, sshdot and homeshick, which adjacent tools compose with it rather than compete, what actually makes it different, and where another tool is the better choice: docs/ALTERNATIVES.md.

Compatibility

Two questions, because hi answers them at two different moments: can hi land a session on that OS at all, and what shell do you end up in once it has. Both are answered with a table, a legend and what proves each row, in docs/SUPPORTED.md — along with the targets hi reaches and what a “yes” costs it to add one.

The other half of the same question — every OS, shell, runtime, packaging channel and feature weighed and answered no, each with the argument attached — is docs/UNSUPPORTED.md.

Testing

tests/test_runner.sh (reachable as hi --test once installed) runs the suite and prints a colored pass/fail summary; --group fast is what CI runs on every push/PR. The runbook — all four suite groups, the parallel container cases, the lint gate, relaying, _HI_HOME, and why the tests are local-only — is in docs/TESTING.md.

More docs

AI Usage

Heavily inspired by: Dictionarry/Profilarr’s AI Transparency Statement

This started as code written entirely by me, but I have used generative AI to write large parts of it. All of the code here is my responsibility regardless: AI is a tool, not an owner of a project. I have personally understood, reviewed and approved all of the AI-generated code in this repository, and mainline releases carry the same accountability to me as anything I write and publish myself.