Releases: hang-in/sshc
Release list
0.13.1 — 2026-06-26
Release Notes
Single-key UX addition. The auto-probe model from v0.3 (probe at
startup + after edits, never on a timer) stays — r just adds
the explicit "refresh the dots without touching anything" trigger
that the picker-and-go workflow was missing between auto-rounds.
Added
-
rre-probes every host's TCP reachability. Previously
the only way to update the list's reachability glyphs was to
edit something (Enter→ save) or to usegfor the
selected host only. Nowrtriggers
ProbePool::refresh(&app.hosts)— same call path the
SaveState / EditConfig actions already use — and emits a
status hint "probing N host(s)…". Empty host list is a
no-op (no action, no status noise).The choice of
rreuses the slot vacated when v0.6 dropped
the oldr(manage-mode reconnect) for inline parity.This is intentionally not a background polling timer:
- Repeated SYNs from one client to many hosts can be
interpreted as a port scan by firewall / IDS and trigger
fail2ban-style automatic blocks. - sshc is a picker, not a dashboard — anti-feature 4
explicitly rules out "daemon / always-on process".
- Repeated SYNs from one client to many hosts can be
Tests
test_r_key_emits_refresh_reachability_with_status_hint—
three hosts,rfires the action and the status reads
"probing 3".test_r_key_on_empty_host_list_is_noop— empty list →
no action, no status.
Install sshc 0.13.1
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.13.1/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.13.1/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.13.1
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.13.0 — 2026-06-26
Release Notes
Mixed cycle. One deps swap (G1 toml_edit removal) takes another
chunk off the binary, and two UX gaps from v0.10 / v0.12 close
out (G2 preview Forwarding section, G3 doctor's
ProxyCommand %h/%p/%r substitution). README + README.ko
caught up to four pieces of v0.8-v0.12 surface that hadn't been
documented.
Added
-
Preview panel shows typed Forwarding summary + first entry
(G2). v0.10 G1 promoted the three*Forwarddirectives to
typed Vec; v0.12 R3 made them editable through a list
modal. The v0.6 preview pane never caught up — Forwarding
configuration was invisible until you opened the form. v0.13
G2 adds:Forward: L:2 R:1 D:0 8080 localhost:80Local / Remote / Dynamic counts on one row, first entry on
the next so the panel shows content, not just a count.
Section is omitted entirely when every Vec is empty
(no row, no blank line gap) — same shape as the existing
Extra handling. -
doctorsubstitutes%h/%p/%rin ProxyCommand
tokens before walking PATH (G3). v0.10 G4 surfaced
"ProxyCommand binary not on PATH" but skipped any token
containing%or$. The most common shape —
ProxyCommand nc %h %por
ProxyCommand corp-helper %h %p— fell exactly into that
skip, so the check passed silently when it should have
fired. v0.13 G3 narrows the skip:$still skips (shell variables — we don't shell out).%h/%p/%rget substituted from the host's typed
fields (with sensible fallbacks: alias for hostname,
"22" for port,$USER/$USERNAME/ "user" for user).
%%becomes a literal%.- After substitution, if any
%is still in the token
(anything we don't model —%C,%u,%L) we skip.
Modelling the full OpenSSH substitution set would creep
into anti-feature 5 (completeconfig(5)parser); the
three we cover are the ones that actually appear in real
ProxyCommand strings.
Changed
-
toml = "0.5"(dropstoml_edittransitive) (G1).
cargo bloat on v0.12.0 rankedtoml_editat 167 KiB of
.text— 11.7% of the binary — and v0.10 G1 / v0.12 R3
showed sshc never needs format-preservation (state.toml is
rewritten from scratch every save). Going back to
toml = "0.5"(pre-toml_editsplit) drops the whole
format-preserving layer.Measured impact on macOS arm64 release binary:
Phase Bytes Δ vs prior v0.12.0 (master) 2,729,456 — v0.13.0 (after toml swap) 2,470,688 −258,768 (−9.5%) cargo-dist artifact (per-platform .tar.xz / .zip) deltas
vs v0.12.0 land at tag-push time and are reported in the
GitHub Release notes.One internal schema reorder accompanied the swap:
MemorySection::sort_axis(scalar) moved BEFORE
recent: Vec<RecentEntry>(array of tables). toml 0.5
enforces the TOML spec's "scalar fields before tables"
rule; toml 0.8's format-preserving emitter had silently
reordered when v0.12 G3 appended sort_axis after recent.
#[serde(default)]on every field means existing v0.12
state.toml files load cleanly regardless of where their
keys land. -
README + README.ko: four gap fixes (R1).
- Windows ARM64 (
sshc-aarch64-pc-windows-msvc.zip) artifact
added to the Windows install section — it's been shipping
since v0.8 but was undocumented. - Hero / 소개 paragraph quotes the v0.12.0 measured release
sizes (macOS arm64 ~810 KB, Linux x64 ~1.16 MB, Windows
x64 ~1.10 MB) so the "small single binary" claim has
concrete numbers. - Form section splits IdentityFile multi-value into its own
paragraph and adds the in-modal keymap table covering
Shift+↑/↓ reorder + the IdentityFile candidate cycle
that lives in edit mode. vkeymap row rewritten to clarify it opens the
resolvedssh -G <alias>output for debugging
Include / Match ordering — the v0.6 wording sounded like
sshc was validating the alias itself.
- Windows ARM64 (
Internal
substitute_known(token, host) -> Option<String>helper
isolates the OpenSSH-style marker substitution. Returns
Noneon any unknown%xso the caller cancontinue
without branching on the string contents. Five new tests.- Two new preview tests (
renders_with_multi_forwarding_*/
renders_without_forwarding_omits_section) +
render_text(term)test-helper that walks the TestBackend
buffer into aStringfor substring assertions. - toml downgrade: a one-line Cargo.toml change. No
src/state/mod.rsAPI surface moved —from_strand
to_string_prettyare stable from 0.5 onwards.
Migration
- state.toml: no migration.
#[serde(default)]on every
field means missing keys (and reordered keys) load
cleanly. - v0.13 binaries running on a v0.12 state.toml file will
pick up the v0.12sort_axisvalue verbatim; the next
save reorders the keys so the file conforms to toml 0.5's
scalar-before-table requirement.
Out of scope (carried into v0.14+)
- nucleo_matcher → smaller fuzzy (69 KiB candidate).
- ureq surface trim (118 KiB candidate; v0.9 R7 already did
the rustls→native-tls win). - K/J as Shift+↑/↓ reorder alias.
- Help modal Shift+↑/↓ + S persistence note.
- doctor:
SSHC_NO_OSC52+ Wayland-no-display hint.
Install sshc 0.13.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.13.0/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.13.0/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.13.0
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.12.0 — 2026-06-26
Release Notes
UX round on top of v0.11's size win. Three goals; all delivered.
The v0.10 ForwardingListModal was extracted into a reusable
ListEditModal and the IdentityFile row finally joins the
forwarding triple as a multi-entry list editor. Sort axis
persistence closes a v0.10 G5 regression where every fresh
sshc started on AliasAlpha.
Added
- IdentityFile multi-value with the same list-modal UX as
forwarding (G1). OpenSSH lets a host carry several
IdentityFilelines and tries them in order; v0.11 and
earlier kept only the last typed entry (Option<PathBuf>)
even though the parser saw every line. v0.12 promotes
Host::identity_filetoVec<PathBuf>end-to-end:- Parser pushes per occurrence; serializer emits one
IdentityFile <path>line per entry in declaration order. - The form's IdentityFile row is now a summary cell; Enter
opens aListEditModalwith kindIdentityFile { candidates }
wherecandidatesis the~/.ssh/*private-key path Vec
the form discovered up-front. - The v0.7.1
↑/↓candidate picker moved INSIDE the modal's
edit mode — same shortcut, just one level deeper so the
multi-entry case has a single home. - Per-entry validation (the v0.7.2 shell-metacharacter
forbid list + Windows backslash exception) moved to the
modal so the form-level guardHostForm::validateno
longer needs an IdentityFile branch.
- Parser pushes per occurrence; serializer emits one
Shift+↑/↓reorder insideListEditModal(G2). Power
users with several forwarding or IdentityFile entries can
now move them up/down without delete + re-add. OpenSSH treats
declaration order as meaningful (keys tried in order,
forwardings opened in order), and v0.10 G1's modal only
supported add/edit/delete. v0.12 closes the gap. Top entry
Shift+↑and bottom entryShift+↓are guarded no-ops; on
the "+ add" pseudo-row Shift is ignored and the key falls
through to plain navigation.- Sort axis persistence in
state.toml(G3). v0.10 G5's
Sshortcut was session-only; every freshsshcre-started
onAliasAlpha. v0.12 adds asort_axisfield to
MemorySection.App::newloads it;App::cycle_sort_axis
writes it back (best-effortstate::save— failures don't
surface a sticky error because the cosmetic preference
doesn't merit one).#[serde(default)]lets pre-v0.12
state.toml files load withAlias— no migration needed.
Changed
ui::forms::forwarding_list→ui::forms::list_edit. Type
renameForwardingListModal→ListEditModal. New
ListKindenum carries the per-kind title / validate /
reject-hint / candidate set so the modal stays one type as
new list kinds land. The R1 refactor was pure (every v0.10
forwarding test passed verbatim) and isolates the surface
area changes that R3 introduced.FormPayload::Host::identity_file: String→Vec<String>.
app::forms::build_hostsignature follows; the v0.11 single
Option<PathBuf>wrap shim is gone.- README + README.ko: keymap notes for
Spersistence and
IdentityFile list modal.
Internal
MemorySection::sort_axis: SortAxisPersistedlives in
state::schema(no UI deps — R-G6). Two pure conversion
functions (SortAxis::from_persisted/to_persisted) live
inapp::modso the boundary holds in both directions.- Round-trip test:
test_round_trip_multi_identity_file_preserves_order—
3 IdentityFile lines round-trip through serializer + parser
with order intact. - New
ListEditModaltests cover IdentityFile validate
(positive + rejection), candidate cycle in edit mode, and
the forwarding-edit-mode arrow no-op (regression guard). - Reorder tests cover the swap + cursor follow, the top-entry
no-op, and the "+ add" row fall-through. - Sort axis persistence tests cover load on new and round-
trip through three cycles.
Migration
- v0.12-written sshc.conf files with multiple
IdentityFile
lines on a host: if you roll back to v0.11 (or earlier) and
re-save the host, v0.11 keeps only the last typed entry
(the same trade-off v0.10 G1 introduced for the forwarding
triple). v0.12 reads its own format and v0.11's identically. - state.toml: no migration.
sort_axisdefaults via
#[serde(default)]on missing key.
Out of scope (carried into v0.13+)
- Further size recovery candidates (
toml_edit165 KiB,
ureq118 KiB). - Forwarding entries on the preview panel.
- doctor: variable-substituted
ProxyCommandsanity
(%h/%pexpansion). - doctor:
SSHC_NO_OSC52+ Wayland-no-display hint. - K/J keys as reorder alias.
Install sshc 0.12.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.12.0/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.12.0/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.12.0
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.11.0 — 2026-06-26
Release Notes
Single-goal size recovery cycle. v0.10's G2 entry promised
Linux/Windows artifact reductions of -600 to -800 KB from
disabling arboard's image-data feature, but cargo-dist
came back with the opposite shape — every platform's artifact
grew (Linux x64 by +164 KB). v0.11 corrects course by going
after the actual top consumer measured with cargo bloat.
The user-facing surface — keymap, forms, doctor checks — is
unchanged.
Changed
-
env_loggerswitched todefault-features = false(G1).
cargo bloat on v0.10.0 (macOS arm64 release) ranked the regex
family —regex_automata(264 KiB) +regex_syntax(169 KiB)aho_corasick(114 KiB) — as 547 KiB of.text, 26.7% of
the binary. The chain entered sshc transitively via
env_logger 0.11 → env_filter → regex.
sshc never used wildcard or regex patterns in RUST_LOG: every
log::warn!/log::error!call site sits under thesshc::
module prefix, and the README never documented a wildcard
shape. Turningdefault-featuresoff dropsregex,
humantime, andauto-color.RUST_LOG=sshc=debugstill
parses (prefix matching is in the slim path); regex patterns
silently don't apply.Measured deltas (this CHANGELOG entry quotes only measured
numbers, never predicted — see v0.10's R6 retrospective for
why):macOS arm64 release binary (host machine):
Phase Bytes Δ vs prior v0.10.0 (master) 3,982,280 — v0.11.0 (after env_logger default-off) 2,727,504 −1,254,776 (−31.5%) cargo-dist artifact (per-platform .tar.xz / .zip) deltas vs
v0.10.0 land at tag-push time — see GitHub Release notes for
the table. If they don't shrink, an erratum will follow.
Internal
- env_logger / env_filter still ship as ~9 KiB each in the
slim path. Thelogfacade and seven call sites are
unchanged. - No source code touched in v0.11. Single Cargo.toml line edit.
Out of scope (carried into v0.12+)
- Further size recovery candidates (
toml_edit165 KiB,
ureq118 KiB,nucleo_matcher69 KiB) — only consider if
user feedback prioritises additional binary slimming over
feature work. - Sort axis state persistence (v0.10 G5 carryover).
- IdentityFile multi-value via the v0.10 ForwardingListModal
pattern. - Forwarding list reorder (↑/↓).
- doctor:
SSHC_NO_OSC52+ Wayland-no-display combination
sanity.
Install sshc 0.11.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.11.0/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.11.0/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.11.0
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.10.0 — 2026-06-26
Erratum (added post-release, 2026-06-26) — the G2 entry below
predicted Linux/Windows artifact reductions of -600 to -800 KB
from disablingarboard'simage-datafeature. cargo-dist's
actual output shows the opposite: every platform's artifact grew,
because the new G1/G3/G4/G5 code (forwarding modal, OSC 52 escape
builder, ProxyCommand walker, sort axis, base64 dep) added more
than the image decoders saved. Measured deltas vs v0.9.0:
Target v0.9.0 v0.10.0 Δ macOS arm64 1086 KB 1103 KB +16 KB macOS x86_64 1216 KB 1233 KB +17 KB Windows ARM64 1478 KB 1492 KB +13 KB Windows x64 1544 KB 1568 KB +24 KB Linux aarch64 1213 KB 1376 KB +163 KB Linux x86_64 1351 KB 1515 KB +164 KB The
image-datastrip is still correct in principle (sshc never
reads image clipboards) but the net size win didn't materialise.
v0.11 will revisit withcargo bloatto find the real footprint.
Release Notes
Surface-polish round on top of v0.9. Five goals, all delivered;
two of them (G1 + G2) close lingering v0.9 trade-offs (typed
forwarding was single-entry; arboard pulled in image decoders sshc
never used).
Added
-
Multi-entry forwarding via a list modal (G1). v0.9 G5
introduced typedLocalForward/RemoteForward/
DynamicForwardform rows but only stored one entry per kind.
OpenSSH lets a host carry several of each; v0.9 cascaded
duplicates into the freeformextrablock as a workaround. v0.10
upgrades the model end-to-end: each field is nowVec<String>,
the parser pushes per occurrence in declaration order, the
serializer emits one line per entry, and the form row opens a
small list modal (Enter to edit /dto delete / Esc to return).
Validation per kind matches v0.9 G5 — modal rejects garbage on
the Enter that tries to add it. -
Scycles the host list sort axis (G5). lazyssh'ss/S
parity, butsis sshc's ssh-connect key, so v0.10 takesS
(Shift+s) only — one direction, three axes. Cycles
alias → recent → reachability → alias. Favorites still float
to the top regardless. Status bar shows the new label after each
press. Session-only — not persisted across sshc invocations. -
OSC 52 clipboard fallback (G3). v0.9 G4's
ccopy lived
directly onarboard, which silently failed on Wayland w/o
display, in remote SSH sessions, or in tmux without
set -g set-clipboard on. v0.10 chains: try arboard first, then
emit the OSC 52 escape (ESC ] 52 ; c ; <base64> ESC \) to
stdout so modern emulators (kitty, iTerm2, foot, alacritty,
wezterm) can write the user's host clipboard. Status hint shows
copied: … (osc52)when the fallback fires so the user knows
which path won.SSHC_NO_OSC52disables the fallback for users
who want to keep their terminal output clean. -
ProxyCommandPATH sanity check in doctor (G4). For every
host parsed out of~/.ssh/config(Include chain followed),
pull theProxyCommandfirst token and look it up on$PATH
(PATHEXT on Windows). When the binary isn't found, doctor
surfaces a single WARN line aggregating offenders by host
count, e.g.[WARN] proxy commands not on PATH — 'my-corp-helper' (3 hosts)Variable-laden tokens (
%h,$JUMP) are skipped — we can't
resolve those without ssh's own substitution layer. Clean
configs see no extra line.
Changed
- arboard
image-datafeature off (G2). sshc only ever writes
text viaset_text. Strippingdefault-featuresand re-opting
into justwayland-data-controlremoves theimageandtiff
decoder crates from the transitive graph (verified via
cargo tree --target x86_64-unknown-linux-gnu). macOS arm64
release size barely moves because LTO had already DCE'd the
unused code path on that target; Linux and Windows artifacts
drop noticeably (-600 to -800 KB depending on platform). New
download sizes will land in this release's GitHub artifacts.
Internal
- New module
src/ui/forms/forwarding_list.rs(~280 LOC plus 7
unit tests). Owns its own validate-per-kind helpers; the v0.9
copies inhost_formwere dead-code removed. - New module
src/exec/clipboard.rs(copy_to_clipboard+
OSC 52 escape builder).App::copy_ssh_command_for_selected
now routes through it; no in-flight v0.9 G4 surface area
changed. Host::local_forward/remote_forward/dynamic_forward
promoted fromOption<String>toVec<String>. All 8 host
literal fixtures (model, app/tests, ui/list, ui/preview,
ui/modal, inline_app, probe, examples) updated.FormPayload
andbuild_hostfollow.App.sort_axisis new state with aSortAxisenum cycling
through three keys.apply_filterrewrites the comparator to
use it as the secondary key behind favorites and (when present)
the fuzzy score.- New dep:
base64 = "0.22"(OSC 52 payload encoder, ~+10 KB
binary impact, scoped toclipboard.rs). - Two arboard dep cleanups removed
imageandtifftransitive
pulls.
Migration
- v0.10-written sshc.conf files with multiple
LocalForward/
RemoteForward/DynamicForwardlines on a single host:
if you roll back to v0.9 (or earlier) and re-save that host,
v0.9 will keep only the last such line per kind (the cascade
workaround was last-wins). The data still survives in the
extrablock from the read side, but the typed form surface
drops it. v0.10 reads its own format and v0.9's format
identically.
Out of scope (carried into v0.11+)
- Sort axis persistence in
state.toml— v0.10 is intentionally
session-only; user feedback decides v0.11. - ↑/↓ reorder inside the forwarding list modal — add/edit/del
only for v0.10. - IdentityFile multi-value — same shape as the forwarding work;
v0.11 candidate. - Hand-rolled clipboard backend (G2 alternative path that wasn't
needed — arboard with image-data off was enough).
Install sshc 0.10.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.10.0/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.10.0/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.10.0
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.9.0 — 2026-06-26
Release Notes
Feature round + a measurable size win after eight v0.8.x patches.
Three sources fed v0.9: operational hardening leftover from the
v0.8 cycle, UX ideas borrowed (selectively) from Adembc/lazyssh,
and a long-deferred dependency cleanup.
Added
- doctor surfaces CRLF in
~/.ssh/config(G1). When the user
copies a config off a Windows host or saves it through an editor
whosefiles.eolis CRLF, OpenSSH treats\ras part of an alias
token and everyHostmatch silently fails. doctor now scans the
first 100 lines and emits aWARNwith the exacttr -d '\r'
fix. Clean configs see no extra line. - doctor surfaces nested
Includescope (G2). Pre-v0.8.4
installs (and any user who hand-edits the~/.ssh/configblock
sshc injected) can end up with theIncludeline nested under a
Host <pattern>stanza. OpenSSH then only fires sshc.conf for
that alias, making every sshc-managed host invisible to
ssh <other-alias>. doctor identifies the offending stanza, names
it in the warning, and suggests addingMatch alldirectly above
the Include — or re-runningsshc -m→ion v0.8.4+ which
emits the terminator automatically. - Sticky-on-error status messages (G3). The single most painful
failure mode of the v0.7–v0.8 cycle was status messages getting
overwritten by the modal-close redraw before the user could see
them. SplitStatusMessageintoInfo(v0.6 transient, 3 s
timeout) andError(stays visible until the user's next
keystroke). All seven real failure paths (apply_form, persist,
ssh -G, include injection, etc.) now useError; routine
confirmations stayInfo. ccopies a one-line ssh command (G4). Pressingcon a
selected host runsssh -G <alias>, reduces the dump to
ssh user@host -p port -i key(dropping defaults like port 22 or
empty identityfile), and pushes the line onto the system
clipboard viaarboard. Useful when sharing a connection string
with someone who doesn't have your~/.ssh/config. Clipboard
failures (Wayland w/o display, etc.) surface as a sticky Error so
the line isn't silently dropped.- Typed Forwarding form fields (G5). The add/modify host form
grows from 7 to 10 fields with a dimmed─── Forwarding ───
section header between Tags and the freeform Options field, and
a─── Advanced ───header above Options.LocalForward,
RemoteForward, andDynamicForwardare now typed
Option<String>values onHost, parsed and serialized
round-trip. Loose validation matches OpenSSH's
[bind:]port host:hostport(Local/Remote) and[bind:]port
(Dynamic) shapes; deep validation is left to ssh on connect
(anti-feature 5: no fullconfig(5)parser). Multi-directive
hosts keep their extras in the free-formextrablock so the
round-trip is lossless. gTCP reachability probe (G6). Distinguishes "host is down"
from "ssh config is wrong" without spawning ssh.gresolves the
selected alias throughssh -G(reusing thev/ccache),
attempts a raw TCP connect to the hostname:port endpoint with a
2-second budget, and reports either a sticky✗ TCP unreachable
error or a transient✓ TCP reach: host:port (N ms).nc -z
semantics — reachability ≠ authentication, anti-feature 1 stands.- Windows ARM64 release artifacts (G8). cargo-dist now produces
sshc-aarch64-pc-windows-msvc.zipalongside the existing
x86_64 Windows artifact. The PowerShell installer routes
ARM64 hosts to the matching binary automatically.
Changed
- HTTP/TLS: ureq + native-tls, explicitly wired (G7). v0.8.0
shipped ureq with default rustls + webpki-roots after the
initial native-tls attempt died with "no TLS backend is
configured". v0.9 closes that loop by handingAgentBuilderan
explicittls_connector— the step v0.8 R6 missed. The release
binary on macOS arm64 shrinks from 5.95 MB (v0.9 R6, with
arboard + new exec/ surface) to 3.76 MB (-2.18 MB), well
under the v0.7-era baseline of 3.0 MB even with everything v0.8
added on top. Dropping ring as a transitive dependency also
unblockedcargo check --target x86_64-pc-windows-msvcfrom
macOS hosts (broken since v0.8 R6 — see the
docs/WINDOWS_DEBUG_HANDOFF.mdrant for context).
Internal
host_formrow layout split into a smallRowenum so section
headers (Forwarding,Advanced) participate in the layout
without confusing Tab routing.FormPayload+FormOutcomepick up#[allow(clippy::large_enum_variant)]
rather than the boxing dance — both are stack values with
one-call lifecycles.- All 8
Hostliteral fixtures (model, app/tests, ui/list,
ui/preview, ui/modal, inline_app, probe, examples) gained the
three new forwarding defaults. - New module
src/exec/tcp_reach.rshouses the G6 reachability
helper. Network surface stays insrc/exec/*. - doctor's
Pathimport is no longer cfg(unix)-gated (G2 uses it
on every platform). ~/.cargo/bin/sshc 0.9.0doctor smoke run: 7 PASS lines, including
the update check landing through native-tls.
Out of scope (carried into v0.10+)
- Multiple Forwarding directives per host (G5 takes the last and
cascades the rest intoextra; round-trip is preserved). - Self-built SSH client, SCP, key deployment — anti-features 1 + 2
still stand; lazyssh's roadmap items in that direction are
intentionally not mirrored. - Always-on / startup update check — anti-feature 4 stands.
gfor
reachability is the user-driven probe; the GitHub Releases call
fires only inside--doctor.
Install sshc 0.9.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.9.0/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.9.0/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.9.0
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.8.4 — 2026-05-21
Release Notes
Hotfix for a load-bearing inject_include bug present since v0.4.0.
Affects both macOS and Windows. No behavior change for users whose
~/.ssh/config was empty when sshc first ran setup.
Fixed
-
Includedirective appended bysshc -m'sicould end up
nested inside the user's lastHoststanza, making sshc-managed
hosts silently invisible tossh <alias>. OpenSSH terminates a
Hostblock only on the nextHost/Matchdirective or EOF —
blank lines and comments don't end a stanza — so anInclude
line appended to a config that ends with aHostblock became a
conditional Include scoped to that last alias.ssh -vvshowed
Reading configuration data .../sshc.conf(parse time), but
Applying options for <sshc-alias>never fired for any sshc host.Fix:
inject_includenow emits three lines instead of two, with
Match allbetween the comment header and theInclude:# Added by sshc; do not remove. Match all Include ~/.ssh/config.d/sshc.confMatchis a stanza header in its own right, so the preceding
Hostblock is closed;Match allmatches every connection
unconditionally, so theInclude(which now belongs to this
block) fires for every alias. Restores the "append-only,
unconditional include" semantics the user expects.Users on existing v0.7.x / v0.8.0–v0.8.3 installs whose
~/.ssh/configends with aHoststanza should either re-run
sshc -m→iafter deleting the existing Include line, or
manually add aMatch allline directly above the sshc-managed
Include block in~/.ssh/config. New installs onto an empty
~/.ssh/configsee no change in behavior.
Internal
- New regression tests:
test_inject_emits_match_all_terminator_before_include—
assertsMatch allis the line immediately beforeInclude
afterinject_includeruns over a config that ends with a
Hoststanza.test_inject_idempotent_with_terminator— confirms calling
inject twice still yields exactly oneIncludeline and one
Match allline.
is_include_presentis unchanged; it scans for anyInclude
directive line-by-line regardless of position, so the new
three-line block is detected as already-present on the second
invocation.
Install sshc 0.8.4
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.8.4/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.8.4/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.8.4
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.8.3 — 2026-05-21
Release Notes
Windows usability hotfix on top of v0.8.2. No behavior change on
macOS / Linux.
Fixed
-
sshc.confis now usable bysshon Windows. v0.8.2 made the
save succeed (theaform actually wrote bytes to disk), but
Windows OpenSSH then refused to read the result with
Bad owner or permissions on C:\Users\<u>\.ssh\config.d\sshc.conf.
The cause:set_owner_only_permswas a#[cfg(not(unix))]no-op,
so newly-writtensshc.confinherited the parent directory's DACL
— typicallyBUILTIN\UsersorAuthenticated Userswith read
access — andssh.exerejects any trustee broader than the owner.v0.8.3 replaces the no-op with a real DACL writer that mirrors
Unixchmod 0600intent: three explicit ACEs (current owner +
NT AUTHORITY\SYSTEM+BUILTIN\Administrators), no inheritance,
PROTECTED_DACL_SECURITY_INFORMATIONso parent ACEs can't drift
back in. Implementation goes throughwindows-sys
GetNamedSecurityInfoW→SetEntriesInAclW→
SetNamedSecurityInfoW. Owner SID is not modified.Verify on Windows after upgrading:
sshc -m→a→ fill, Enter,
thenssh -G <alias>no longer prints the "Bad owner" error and
icacls $HOME\.ssh\config.d\sshc.confshows only SYSTEM /
Administrators / your user account.
Internal
-
test_editor_command_constructionnow pinsEDITOR=vimfor the
duration of the test and restores the original value on exit.
v0.8-era runs on Windows withEDITORunset fell through to
notepad.exe, which doesn't accept the+42arg the test
asserts. The assertion itself wasn't catching a real regression
on those hosts — it was tripping on the environment default. -
Cargo.tomlwindows-sysfeatures list adds
Win32_Security_Authorization(transitively required by
SetEntriesInAclW/SetNamedSecurityInfoW).
Install sshc 0.8.3
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.8.3/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.8.3/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.8.3
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.8.2 — 2026-05-21
Release Notes
Windows hotfix over v0.8.1 — actually fixes the silent a (add host)
data loss. The v0.8.1 changelog blamed PathBuf normalization
(NFC/NFD on non-ASCII home directories) and unified the cached path
on both sides of the filter; that change was internally correct but
did not fix the bug. The real cause was one layer deeper, in
with_locked_write. No behavior change on macOS / Linux.
Fixed
-
Manage-mode
a(add host) on Windows actually persists the new
host tosshc.confnow.crate::storage::with_locked_write
acquired an exclusiveLockFileExon its first handle and then —
before handing the buffer to the mutator — opened a second
File::open(path)to read the existing content. Windows file
locking is mandatory, so the second open inside the locked range
returnedERROR_LOCK_VIOLATION(os error 33) and the writer
bailed out withStorageError::ReadFailedbefore any bytes were
written. The error did surface as astatus_messagefrom
apply_form, but the modal-close redraw overwrote it almost
immediately and the user only saw an empty file.Unix
flockis advisory and silently permitted the second open,
which is why every pre-v0.8.2 release looked fine on macOS / Linux
while consistently failing on Windows from v0.7 onward.Fix: read the existing content from the already-locked handle via
seek(SeekFrom::Start(0)) + read_to_stringinstead of opening a
second handle. One handle, one lock, no violation. Added a
cross-platform regression test
(app::tests::test_apply_form_add_host_writes_through_locked_writer)
that drivesapply_formend-to-end against a temp path so the
same class of regression can't slip back in silently.
Note on v0.8.1
The v0.8.1 patch (path-cache unification in persist_sshc_conf) is
still in place and still correct as defense-in-depth — it just
wasn't the root cause. If you upgraded to v0.8.1 expecting the a
fix to land and saw the same empty-file behavior, v0.8.2 is the
release you actually want.
Install sshc 0.8.2
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.8.2/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.8.2/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.8.2
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
0.8.1 — 2026-05-20
Release Notes
Windows hotfix over v0.8.0 — fixes a long-running silent data loss
in the add-host form. No behavior change on macOS / Linux.
Fixed
-
Manage-mode
a(add host) silently wrote an emptysshc.confon
Windows when the user directory had non-ASCII characters in its
path (e.g.C:\Users\사자). The form looked like it submitted —
modal closed, no error message — but the new host never appeared
in the list, and the on-disksshc.confended up empty. The
symptom traces back topersist_sshc_confinsrc/app/forms.rs:
it filtered which in-memory hosts to serialize by comparing
host.source_fileagainst a freshly recomputed
crate::storage::sshc_conf_path(). Thesource_filefield was
set at host-build time from the App::new-cached copy, so on
Windows the twoPathBufs could differ in normalization (NFC vs
NFD on the home-directory component) even though the underlying
bytes were equivalent. The filter then matched zero rows and the
serializer wrote an empty file.This was actually present since v0.7-era (when manage mode add
first landed), but only Windows users with non-ASCII paths could
trigger it; v0.7.2 (backslash) and v0.7.3 (rename lock) fixes
cleared the visible failures one step earlier in the pipeline,
so the data-loss path stayed hidden until v0.8.Fix:
persist_sshc_confnow uses the cachedself.sshc_conf_path
for both the filter predicate and the write target, so the two
comparisons sit on the samePathBufinstance regardless of
platform normalization quirks.
Install sshc 0.8.1
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/hang-in/sshc/releases/download/v0.8.1/sshc-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/hang-in/sshc/releases/download/v0.8.1/sshc-installer.ps1 | iex"Install prebuilt binaries via Homebrew
brew install hang-in/tap/sshcDownload sshc 0.8.1
| File | Platform | Checksum |
|---|---|---|
| sshc-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| sshc-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| sshc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| sshc-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| sshc-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |