Your config lives outside the checkout, in
${XDG_CONFIG_HOME:-$HOME/.config}/say-hi/ ($_HI_CONFIG_DIR). colors,
packages there override the tree’s copies, one file at a
time - anything you haven’t overridden keeps tracking the default the tree
ships, so hi --update still delivers changes to the rest. aliases.sh is the
one that adds rather than replaces, loading after the tree’s own so yours win.
settings.sh has no in-tree counterpart at all: hi --configure only ever
writes it here.
| overlay file | overrides | what it is |
|---|---|---|
~/.config/say-hi/settings.sh |
- | what hi --configure writes |
~/.config/say-hi/colors |
misc/colors |
your color pins |
~/.config/say-hi/packages |
misc/packages |
what the package check looks for |
~/.config/say-hi/aliases.sh |
- | your own aliases, sourced after misc/aliases.sh and misc/personal.sh so yours win - additive, never a replacement, and in the same POSIX+fish subset |
This is what keeps configuring say-hi from dirtying the checkout (so
hi --update’s git pull keeps applying cleanly), and why the tree never has
to be writable at all - it can be root-owned, installed by a package manager.
All of it rides along to every host you say hi to, in its own small archive.
Want history on it? hi --overlay-init makes ~/.config/say-hi a git repo in
place: from then on hi --configure commits its own settings writes,
hi --doctor reports the commit count, and a push remote is one
git remote add away. Entirely optional. (Keeping the same directory in chezmoi
or yadm works just as well - see ALTERNATIVES.md.)
Everything in the tables below is an environment variable, checked where it’s
used. hi --configure writes your answers to ~/.config/say-hi/settings.sh,
which every shell sources ahead of common/paths.sh - a plain #!/bin/sh script of
export NAME=value lines, valid in sh, bash, zsh and fish alike. You never have
to use hi --configure: exporting any of these by hand works just as well, and
takes precedence for that shell. The one exception is marked read-only in its
row: common/paths.sh derives it on every source, so an exported value never
lasts.
hi.sh runs on the client, tars say-hi/ and sends it to the target, which
unpacks it into a /tmp directory. $_HI_PAYLOAD at the top of hi.sh is
the authoritative allow list — no .git, scripts/, tests/, docs/ or
CI. Your overlay (the table above) follows in a second,
much smaller archive, landing in a config/ of its own so your aliases.sh
stays additive. A target that already has its own say-hi gets neither: hi
loads that tree in place and it reads its own overlay.ARG_MAX says it is. Every shell file is comment-stripped on
the way into that archive — the checkout keeps its comments, the wire does
not, which is about 40% of it; set _HI_KEEP_COMMENTS=1 to ship the tree
verbatim when you need to read the real source on a target.hi prints the size of on connect, and what
the payload badge measures — for a default configuration, since a client
whose overlay turns off the editor overrides or the OSC 52 clipboard sends
less. Read it as the per-session wire cost, not as the package badge beside
it: that one is what a release downloads and what it occupies on disk, which
is the larger figure, scripts/ and the docs shipping in a package and
never over the wire.load.sh prints the header, appends hi’s shell configs to
the host’s own rc files, and drops you into your login shell when hi
styles it (bash, zsh, fish), else the best the target has of
$_HI_SHELL_TREE — fish > zsh > bash > dash > ash > sh.
_HI_SHELL_PREFERENCE is that rule as a setting. Where there is no bash at
all the choice comes from $_HI_SHELL_LADDER, that same list with bash
taken out.load.sh’s trap strips those additions back out and the /tmp
directory is removed.hi <target> 'some command' skips the session and runs the command there
instead, the way ssh does.The bootstrap is plain POSIX sh, so a target with no bash still gets a
session — the best plain shell it has, with the aliases loaded, rather than the
full load.sh. For ssh targets hi first checks, over the same connection so it
costs no extra authentication, whether a permanent say-hi is already there; if
so it uses that in place and copies nothing. It does not assume ~/say-hi: the
check reads the _HI_HOME line install.sh wrote into that target’s login rc
files (or /etc/profile.d for a packaged install), then falls back to the home
directory, and finally to the places an install lands when nothing declared it —
~/.local/share, /usr/local/share, /opt, /usr/share and Homebrew’s
default keg prefixes, which is what finds a brew installed target that never
had its shells wired up. A tree installed anywhere is still found and reused.
hi --doctor prints the wire size and the unpacked size, labeled.
Each is on by default; set it to 1 to turn that piece off.
| variable | turns off |
|---|---|
_HI_DISABLE_HEADER |
the whole connect/disconnect header, every line of it |
_HI_DISABLE_PROMPT |
the colored user@host prompt, leaving your shell’s own |
_HI_DISABLE_PERSONAL |
personal shell settings - history size, keybindings, completion tweaks |
_HI_DISABLE_GIT_STATUS |
the git segment in the prompt |
_HI_DISABLE_EDITORS |
the vim/nano config overrides |
_HI_DISABLE_ALIASES |
the personal aliases in misc/personal.sh - not the editor and hi_copy aliases misc/aliases.sh installs, which are the product. Setting it also keeps personal.sh off the ssh payload entirely |
_HI_DISABLE_OSC52 |
the OSC 52 clipboard - yanks in vim and the hi_copy alias |
_HI_DISABLE_LOCAL |
all of the above on this machine only - hi still styles the hosts you visit |
_HI_DISABLE_LOCAL is the odd one out: “leave my own machine alone, but give me
hi everywhere I connect to”. It’s told apart from a real session by
_HI_REMOTE_SESSION, which load.sh exports on a target and a local shell’s
own rc never does.
_HI_DISABLE_OSC52 turns off the one feature that reaches back through the
connection: a yank in vim on a target, or anything piped into hi_copy, is
base64’d into an
OSC 52
escape and written to the tty, so your local terminal emulator - not the host -
puts it on your clipboard. No X11 forwarding, no clipboard daemon, nothing
installed on the target. Only the unnamed register is sent, so "ay stays
local. Terminal support varies (tmux needs set -g allow-passthrough on; zellij
handles OSC 52 itself, so under $ZELLIJ the escape goes through raw and
unwrapped), which is why it’s a toggle like everything else; shells/osc52.sh
is the whole implementation if you want to read what gets emitted.
Every username and hostname resolves to a color derived from its own name, so an
unpinned host looks the same from every machine you say hi from - nothing to
generate, nothing that can go missing. Pin the ones that matter in
~/.config/say-hi/colors: username,root,red, hostname,bastion,yellow, or
hosttag,prod,red to color every host carrying a # Tags: prod comment above
its Host line in ~/.ssh/config. A pin always beats the hash.
hi --color-preview answers what that adds up to - every host in your ssh
config and every user it knows of, drawn in the colors themselves, each row
naming the rule it matched:

hi grafts its rc block into the target’s ~/.bashrc (and ~/.zshrc, and fish’s
config) on connect, and strips it on exit. The block is grafted once and
removed by whoever leaves first - not by whoever put it there. So of two
overlapping sessions to one host, the first to exit takes the block away from
the one still running.
Nothing you are already using breaks. A running shell read its rc when it
started, the session trees are per-mktemp so neither session can delete the
other’s, and the graft is guarded on $_HI_HOME so it could never source a
stranger’s tree anyway. What you lose is a shell started afterwards inside
the surviving session - su, a nested login - which comes up
bare, exactly as if you had ssh’d in without hi.
This is deliberate rather than unnoticed. Refcounting the graft is the
alternative, and a refcount has to live somewhere on the target that survives a
crashed session - persistent state on a machine hi promises to leave as it found
it (SECURITY.md’s footprint section). Reconnecting is the
workaround, and tests/load/load_test.sh pins the behaviour so it cannot change
by accident.
Each is on by default; set it to 0 to hide that line. All are ignored when
_HI_DISABLE_HEADER=1.
| variable | hides |
|---|---|
_HI_HEADER_BANNER |
the ~~~ Connected [host] ~~~ line, on connect and disconnect |
_HI_HEADER_TIMESTAMP |
the date/time line |
_HI_HEADER_SYSINFO |
the OS / CPU / RAM line |
_HI_HEADER_IDENTITY |
the git identity / containers / ssh key line |
_HI_HEADER_CHECK |
the installed-packages check (misc/packages) |
| variable | default | what it does |
|---|---|---|
_HI_MAX_WIDTH |
80 |
terminal columns the header and banner are drawn to |
_HI_HOME |
derived | the parent of your say-hi directory - everything resolves $_HI_HOME/say-hi. Each entry point derives it from its own path when unset; set it to override |
_HI_TARGETS_TTL |
5 |
seconds hi <TAB> reuses its target list for; 0 disables the cache |
_HI_PROBE_TIMEOUT |
2 |
seconds any one backend CLI gets, during completion and in the header |
_HI_SSH_CONFIG |
~/.ssh/config |
read-only: where ssh hosts and their # Tags: comments are read from. Derived from $HOME by common/paths.sh every time it is sourced, so exporting your own value does not survive - point $HOME at another tree if you need a different config |
_HI_ASCII |
by locale | 1 forces ASCII stand-ins for the banner/prompt/packages glyphs (^ ok x for ↑ ✓ ✗), 0 forces the glyphs; unset asks the locale, so a LANG=C target degrades cleanly instead of printing mojibake |
NO_COLOR |
unset | not hi’s variable but the convention: any non-empty value renders everything - header, prompts, git segment - without color, and hi ships your client-side choice to the target next to _HI_ASCII |
_HI_PROMPT |
unset | starship hands the prompt to starship when the target has it, keeping hi’s header and aliases. Never auto-detected, and a target without starship silently keeps hi’s own. hi does not ship starship - a multi-MB binary against a ~48KB payload |
_HI_SHELL_PREFERENCE |
login + $_HI_SHELL_TREE |
which shell a session runs in: an ordered list of bash/zsh/fish, plus login for “your own login shell”. The default tail is common/core.sh’s $_HI_SHELL_TREE (fish zsh bash dash ash sh) filtered to the shells hi styles, i.e. fish zsh bash - the same list hi.sh’s no-bash $_HI_SHELL_LADDER is cut from, so the two orderings cannot disagree. First one installed on the target wins; bash is the floor, since that is what load.sh needs to run at all. |
_HI_PROMPT_END |
per shell | the character each prompt ends with, when you want the same one everywhere; the three below win over it |
_HI_PROMPT_END_BASH |
\$ |
bash’s prompt separator (\$ is bash’s own escape for “$, or # for root”) |
_HI_PROMPT_END_ZSH |
> |
zsh’s prompt separator - zsh prompt escapes work here, so %# behaves as it does anywhere else in PS1 |
_HI_PROMPT_END_FISH |
\| |
fish’s prompt separator; root still gets # regardless |
_HI_TERM_FALLBACK |
1 |
on ssh targets missing a terminfo entry for your TERM (ghostty’s xterm-ghostty, typically), swap it for xterm-256color before the session starts; 0 keeps the original TERM |
_HI_HEADER_GHZ |
0 |
1 shows the header’s CPU line as x.xxx/x.xxx GHz instead of the default whole-MHz pair; ignored when _HI_HEADER_SYSINFO=0 |
_HI_PACKAGES_MIN_PRIORITY |
1 |
the lowest misc/packages priority the header’s check will print, and the main dial on how long that check is. The file ranks every entry 0-5, and every rank reports what is missing as well as what is there - a target you visit often is where a nudge to install your own preferred tools belongs - so this is what decides how far down that list you want to hear about. On a well-equipped machine: 1 is what ships and drops the trivia tier (about ten lines), 0 puts it back and prints everything (a dozen), 2 drops the optional extras too (about four), 3 leaves your favorites and what your workflow depends on (two), and above 5 the check prints nothing at all rather than a blank line. Rank 4 is the one that behaves differently by design: it is silent when those tools are present and speaks only when they are not, so a bare target still says what it is missing at any floor up to 4. hi --configure asks for this one with a live preview - it re-renders the real check at each value you type, so you pick the length you want by looking at it. hi --packages-preview marks the ranks it silences below floor and counts them under the legend, so the setting is legible before you connect anywhere |
_HI_ENABLE_FISH_ALIAS_ABBR |
0 |
fish only, off by default: 1 gives every alias hi defines a real abbr, so it expands to the full command on the line before you run it - it rewrites what your command line and history literally say, hence opt-in (hi_abbr_aliases does the work and is callable by hand in any fish shell). Not in the _HI_DISABLE_* table above since it’s fish-specific, not one of core.sh’s shared toggles |
_HI_TARGETS_TTL and _HI_PROBE_TIMEOUT exist because completion runs on
every TAB and the header runs before you get a shell: a docker daemon
that’s down or a kubectl pointed at a dead cluster would otherwise hang there
with no upper bound.