Pure-Rust SSH toolkit for Git: transport, keys, signing, agent.
- Project page: gitway.steelbore.com
- Maintainer: Mohamed Hammad <
Mohamed.Hammad@Steelbore.com> - Copyright: © 2026 Mohamed Hammad — GPL-3.0-or-later (see LICENSE)
General-purpose SSH clients (ssh, PuTTY) carry complexity that Git doesn't
need — interactive shells, tunneling, agent forwarding, hundreds of config
directives. That complexity causes three concrete pain points:
- Configuration errors — a misconfigured
~/.ssh/configsilently routes traffic through the wrong key. - Fragile host-key trust — the first-connection TOFU model forces developers to blindly accept a fingerprint.
- Windows inconsistency — multiple competing SSH implementations with incompatible agent protocols.
Gitway solves these by being opinionated: it connects only to GitHub, pins GitHub's published host-key fingerprints, searches for keys in a predictable order, and behaves identically on Linux, macOS, and Windows.
- Pinned host keys — GitHub's SHA-256 Ed25519, ECDSA, and RSA fingerprints are embedded in the binary. No TOFU. A key mismatch aborts immediately.
- Automatic key discovery — searches
~/.ssh/id_ed25519,~/.ssh/id_ecdsa,~/.ssh/id_rsain order, then falls back to the SSH agent. - Passphrase support — prompts securely via
rpassword; passphrase memory is zeroized on drop. - OpenSSH certificates — pass a certificate alongside your key with
--cert. - GitHub Enterprise Server — add GHE fingerprints to
~/.config/gitway/known_hosts. - Drop-in replacement — works with
GIT_SSH_COMMANDandcore.sshCommandexactly assshdoes. - Library crate — embed
anvil-ssh(the extracted SSH stack at github.com/Steelbore/Anvil) directly in Rust projects for programmatic Git transport. - Single static binary — no C runtime, no OpenSSL, no system SSH required.
Nushell:
cargo install --path gitway-cliIon:
cargo install --path gitway-cli
Bash/Brush:
cargo install --path gitway-cliNo Alpine package exists yet. The pre-built static musl binary from the GitHub Releases page runs natively on Alpine with no libc dependency.
Option A — pre-built binary (recommended):
# Download and install the latest release binary
wget -qO- https://github.com/steelbore/gitway/releases/latest/download/gitway-linux-x86_64.tar.gz \
| tar -xz
sudo install -m755 gitway /usr/local/bin/gitway
sudo install -m755 gitway-keygen /usr/local/bin/gitway-keygen
sudo install -m755 gitway-add /usr/local/bin/gitway-addOption B — build from source:
apk add cargo gcc perl pkgconf
cargo install gitway gitway-keygenTwo AUR packages are provided. gitway-bin installs the pre-built musl binary
and is recommended for most users — no compiler required.
With an AUR helper (yay):
yay -S gitway-binWith an AUR helper (paru):
paru -S gitway-binWithout an AUR helper (manual):
git clone https://aur.archlinux.org/gitway-bin.git
cd gitway-bin
makepkg -siTo track git HEAD instead (builds from source), use gitway-git in place of
gitway-bin. The PKGBUILDs for both packages are also shipped in
packaging/arch/ in this repository.
Pre-built .deb packages are produced by the CI release workflow and attached
to every GitHub release.
Install a pre-built package:
# Download the .deb for your architecture from the Releases page, then:
sudo apt install ./gitway_*.debBuild locally:
sudo apt install cargo gcc perl pkg-config
cargo install cargo-deb
cargo deb -p gitway
sudo apt install ./target/debian/gitway_*.debOn older Debian or Ubuntu releases the packaged Rust toolchain may be too old. Install a current toolchain via rustup and retry.
Pre-built .rpm packages are produced by the CI release workflow and attached
to every GitHub release.
Install a pre-built package:
# Download the .rpm from the Releases page, then:
sudo dnf install ./gitway-*.rpmBuild locally:
sudo dnf install cargo gcc perl pkgconf-pkg-config
cargo install cargo-generate-rpm
cargo build --release -p gitway
cargo generate-rpm -p gitway-cli
sudo dnf install ./target/generate-rpm/gitway-*.rpmNo ebuild is in the main Gentoo tree yet. The pre-built static musl binary works on both glibc and musl Gentoo profiles.
Pre-built binary:
# Download and install from the Releases page, then:
sudo install -m755 gitway /usr/local/bin/gitway
sudo install -m755 gitway-keygen /usr/local/bin/gitway-keygen
sudo install -m755 gitway-add /usr/local/bin/gitway-addBuild from source:
emerge dev-lang/rust
cargo install gitway gitway-keygenPre-built .rpm packages are produced by the CI release workflow and attached
to every GitHub release.
Install a pre-built package:
# Download the .rpm from the Releases page, then:
sudo zypper install ./gitway-*.rpmBuild locally:
sudo zypper install cargo gcc perl pkg-config
cargo install cargo-generate-rpm
cargo build --release -p gitway
cargo generate-rpm -p gitway-cli
sudo zypper install ./target/generate-rpm/gitway-*.rpmPre-built Windows binaries are attached to every GitHub release as a .zip
archive containing gitway.exe, gitway-keygen.exe, LICENSE, and
README.md. gitway-add is Unix-only and is not shipped for Windows;
on Windows, use gitway agent directly.
Install a pre-built binary (recommended):
The recommended location is C:\Program Files\Gitway\ on System PATH.
This mirrors how every Microsoft / JetBrains / Mozilla / etc. installer
places binaries on Windows, and System PATH (as opposed to User PATH)
ensures IDEs, GUI git clients, scheduled tasks, and Windows services that
do not inherit your interactive-shell environment can still find gitway
and gitway-keygen.
Both steps need an elevated PowerShell (right-click → Run as administrator).
-
Download
gitway-v<VERSION>-windows-x86_64.zipfrom the Releases page. -
Extract it to
C:\Program Files\Gitway\:$zip = "$env:USERPROFILE\Downloads\gitway-v<VERSION>-windows-x86_64.zip" Expand-Archive -Path $zip -DestinationPath 'C:\Program Files\Gitway\' -Force
-
Add
C:\Program Files\Gitwayto System PATH (idempotent — safe to re-run on upgrade):$machinePath = [Environment]::GetEnvironmentVariable('Path','Machine') if (-not (($machinePath -split ';') -contains 'C:\Program Files\Gitway')) { [Environment]::SetEnvironmentVariable( 'Path', "$machinePath;C:\Program Files\Gitway", 'Machine') }
-
Open a new terminal so it picks up the updated System PATH, then verify:
gitway --version gitway --test
Upgrading from an older version:
Re-run step 2 with -Force; Expand-Archive will overwrite the existing
binaries. Step 3 is a no-op once C:\Program Files\Gitway is already on
System PATH. Restart any IDE that was running before the upgrade so its
git subprocess inherits the refreshed environment.
Why not C:\tools\gitway\ or %LOCALAPPDATA%\Programs\Gitway\?
User-scoped paths (%LOCALAPPDATA%, %USERPROFILE%) work for solo-user
workflows but are invisible to system services, Windows Task Scheduler
running under a different account, and AppContainer / sandboxed clients.
Non-standard paths under C:\ (C:\tools\, C:\opt\) work mechanically
but break the Add or remove programs style mental model new contributors
expect on Windows.
Build from source:
aws-lc-rs requires NASM during compilation.
Install it before running cargo install:
winget install nasm
# or: choco install nasm
# then restart the terminal so nasm.exe is on PATH
cargo install gitway gitway-keygencargo install writes to %USERPROFILE%\.cargo\bin\, which is on User
PATH only — fine for terminal use but subject to the same discoverability
caveats above for IDEs and services. For shared use, copy the resulting
binaries into C:\Program Files\Gitway\ per the recipe above.
Agent on Windows:
The Gitway agent uses the Windows named-pipe transport
(\\.\pipe\gitway-agent.<PID> by default), compatible with
OpenSSH for Windows's \\.\pipe\openssh-ssh-agent.
Background daemon mode (auto-detach) is Unix-only. To keep the agent running
on Windows, start it in a separate terminal with -D and leave it open:
gitway agent start -DTo stop it, press Ctrl+C in that terminal, or use Stop-Process / Task Manager.
For an always-on agent, wrap gitway agent start -D in a Windows service using
a tool such as NSSM or a scheduled task with Run whether user is logged on or not.
Gitway exposes a flake at github:steelbore/gitway. Three install paths
are supported, in order of increasing declarativeness.
Imperative, per-user — nix profile:
nix profile install github:steelbore/gitwayInstalls gitway, gitway-keygen, and gitway-add into
~/.nix-profile/bin/. Upgrade later with
nix profile upgrade gitway.
One-shot run without installing:
nix run github:steelbore/gitway -- --testDeclarative, system-wide — flake input on NixOS:
In /etc/nixos/flake.nix:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
gitway.url = "github:steelbore/gitway";
};
outputs = { self, nixpkgs, gitway, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
({ pkgs, ... }: {
environment.systemPackages = [
gitway.packages.${pkgs.system}.default
];
})
];
};
};
}Then sudo nixos-rebuild switch.
Declarative, per-user — flake input via home-manager:
With gitway passed into the home-manager config as a flake input:
{ gitway, pkgs, ... }: {
home.packages = [ gitway.packages.${pkgs.system}.default ];
}All shells:
gitway --install
# Runs: git config --global core.sshCommand gitwayThe --install command writes one line into your ~/.gitconfig:
[core]
sshCommand = gitwayVerify it:
git config --global --get core.sshCommand
# → gitwayRemove it to fall back to OpenSSH:
git config --global --unset core.sshCommandAfter this, every git clone, git fetch, and git push over SSH uses Gitway
automatically. Make sure gitway itself is on a PATH that non-interactive
shells see — see Making gitway discoverable to Git
below.
Git invokes core.sshCommand = gitway via execvp, which walks the
current process's PATH — not the PATH you see in your terminal.
IDEs, GUI git clients, systemd user services, and most launchers start
processes without sourcing ~/.bashrc / ~/.zshrc / ~/.ionrc, so
paths added only in an interactive-shell rc file are invisible to them.
The gitway binary must live somewhere every inherited environment sees:
NixOS — all three standard Nix profile paths are injected into PATH by the NixOS PAM stack and thus visible to non-interactive shells and GUI apps:
~/.nix-profile/bin/gitway— fromnix profile install github:steelbore/gitway/etc/profiles/per-user/$USER/bin/gitway— from home-managerhome.packages(includingservices.gitway-agent.enable = true)/run/current-system/sw/bin/gitway— from NixOSenvironment.systemPackages
Debian / RPM distros — /usr/bin/gitway from the official .deb or
.rpm package is universal. Every shell, every launcher, every systemd
unit can reach it without configuration.
cargo install users (~/.cargo/bin) — this is the classic footgun.
~/.cargo/bin is on PATH only if it's exported system-wide (in
/etc/environment, ~/.profile, ~/.pam_environment, or a systemd
environment.d drop-in), not if it's only added in .bashrc. If
git push works from your terminal but fails from your IDE with a bare
exit 128, this is almost certainly why.
Two fixes:
# Option 1 — install gitway into a system-wide location:
sudo install -m755 ~/.cargo/bin/gitway /usr/local/bin/gitway
sudo install -m755 ~/.cargo/bin/gitway-keygen /usr/local/bin/gitway-keygen
sudo install -m755 ~/.cargo/bin/gitway-add /usr/local/bin/gitway-add
# Option 2 — add ~/.cargo/bin to a PATH file that non-interactive shells
# read. On most Linux distros, /etc/environment is the right spot:
echo 'PATH="/home/'$USER'/.cargo/bin:/usr/bin:/bin"' | sudo tee -a /etc/environment
# (and log out + back in)Quick diagnostic — does a stripped environment see gitway?
env -i PATH=/usr/bin:/bin which gitwayIf that prints nothing, neither will your IDE's embedded git.
This puts transport, signing, and agent into a single working configuration. Three pieces, in order: agent, git config, GitHub signing-key upload.
The agent persists unlocked key material for the session, so Git and gh
stop prompting for a passphrase on every push.
Enable the module this flake exposes. Add to your home.nix (assuming the
flake is imported as a gitway input):
{ gitway, pkgs, ... }: {
imports = [ gitway.homeManagerModules.default ];
services.gitway-agent.enable = true;
}Rebuild with home-manager switch. The module:
- Installs
gitway,gitway-keygen, andgitway-addinto your user profile. - Runs the hardened
gitway agent start -Das a user systemd service. - Exports
SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/gitway-agent.sockinto every child shell viahome.sessionVariables.
Load your key once per boot:
gitway-add ~/.ssh/id_ed25519The agent survives reconnects and shell restarts until you reboot or run
systemctl --user stop gitway-agent.
Identical option set, system-scoped:
{ gitway, ... }: {
imports = [ gitway.nixosModules.default ];
services.gitway-agent.enable = true;
}See packaging/systemd/gitway-agent.service:
mkdir -p ~/.config/systemd/user
cp packaging/systemd/gitway-agent.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now gitway-agent.serviceThen export the socket path in your shell rc (snippet per shell below).
Start the agent inside the login shell and export its environment. Fine for quick smoke tests; Option A/B/C is better for daily use.
Bash / Brush — add to ~/.bashrc:
if [ -z "$SSH_AUTH_SOCK" ] || ! gitway-add -l >/dev/null 2>&1; then
eval "$(gitway agent start -s)"
fiNushell — add to $nu.env-path:
if ($env.SSH_AUTH_SOCK? | is-empty) {
let agent = (^gitway agent start -s)
$env.SSH_AUTH_SOCK = ($agent | parse -r 'SSH_AUTH_SOCK=([^;]+)' | get capture0.0)
$env.SSH_AGENT_PID = ($agent | parse -r 'SSH_AGENT_PID=([^;]+)' | get capture0.0)
}Ion — Ion has no eval. Use Option A/B/C and set SSH_AUTH_SOCK
directly in ~/.config/ion/initrc:
export SSH_AUTH_SOCK = "${XDG_RUNTIME_DIR}/gitway-agent.sock"
Home-Manager (Option A) and the NixOS module (Option B) do this for you. For Option C, add one line to your shell rc so every client finds the running agent:
Bash / Brush (~/.bashrc / ~/.brushrc):
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/gitway-agent.sock"Nushell ($nu.env-path):
$env.SSH_AUTH_SOCK = $"($env.XDG_RUNTIME_DIR)/gitway-agent.sock"Ion (~/.config/ion/initrc):
export SSH_AUTH_SOCK = "${XDG_RUNTIME_DIR}/gitway-agent.sock"
Wire Git to sign every commit with your SSH key via gitway-keygen (no
GPG or OpenSSH required). All shells:
# Your identity — use your `noreply` address to hide your real email.
git config --global user.name "Your Name"
git config --global user.email "youremail@users.noreply.github.com"
# Use the public key as the signing identity.
git config --global user.signingkey ~/.ssh/id_ed25519.pub
# Sign every commit with SSH (not GPG).
git config --global gpg.format ssh
git config --global gpg.ssh.program gitway-keygen
git config --global commit.gpgsign truegpg.ssh.program=gitway-keygen is the wire: Git invokes it exactly the
way it invokes ssh-keygen -Y sign, and the shim is byte-compatible with
real ssh-keygen for that argv.
If you haven't already registered gitway as the SSH transport (step 2 of
Installation above), also add core.sshCommand = gitway — either via
gitway --install or by hand in ~/.gitconfig. Without that line,
git push still uses OpenSSH even though commit signing goes through
gitway-keygen.
So the Verified badge appears on commits you push. All shells:
# Grant gh the scope it needs to manage signing keys:
gh auth refresh -h github.com -s admin:ssh_signing_key
# Upload the public key:
gh ssh-key add ~/.ssh/id_ed25519.pub --type signing --title "gitway"The ! prefix in the original recipe (! gh ssh-key add ...) is only
relevant inside a Claude Code session — on a normal shell prompt, drop
the ! and run the command directly.
git commit --allow-empty -m "gitway signing smoke test"
git log --show-signature -1 # expect: "Good \"git\" signature ..."
git push
gh api repos/OWNER/REPO/commits/$(git rev-parse HEAD) | jq .commit.verification.verified
# expect: trueIf verification is false, re-check that the same key file is
referenced in user.signingkey and uploaded to GitHub under
Settings → SSH and GPG keys → type: Signing Key.
gitway [OPTIONS] <host> <command...>
| Flag | Description |
|---|---|
-i, --identity <FILE> |
Path to SSH private key |
--cert <FILE> |
OpenSSH certificate alongside the key |
-l, --user <USER> |
Remote SSH username (default: git; e.g. aur for AUR, the user's login on sourcehut) |
-p, --port <PORT> |
SSH port (default: 22) |
-v, --verbose |
Enable debug logging to stderr |
--insecure-skip-host-check |
Danger: skip host-key verification |
--connect-timeout <SECS> |
Per-attempt TCP connect deadline in seconds (FR-80). Default: none. |
--attempts <N> |
Total connection attempts including the first (FR-80). Default: 3; use 1 to disable retry. |
--max-retry-window <SECS> |
Hard ceiling on total retry wall-clock time in seconds (FR-81). Default: 30 s. |
--kex <LIST> |
Override KEX algorithm preference using +algo / -algo / ^algo / algo,algo syntax (FR-77). |
--ciphers <LIST> |
Override cipher preference. See gitway list-algorithms for available names. |
--macs <LIST> |
Override MAC preference. |
--host-key-algorithms <LIST> |
Override host-key algorithm preference. |
--test |
Verify connectivity and display the GitHub banner |
--install |
Register as core.sshCommand in global Git config |
Verify connectivity:
gitway --testUse a specific key:
gitway --identity ~/.ssh/id_ed25519_github github.com git-upload-pack 'org/repo.git'Verbose debug output:
gitway --verbose --testRetry with a 5-second per-attempt timeout (e.g. flaky corporate proxy):
gitway --connect-timeout 5 --attempts 3 --testTarget a GitHub Enterprise Server instance:
gitway --port 22 ghe.corp.example.com git-upload-pack 'org/repo.git'Connect to a host that uses a non-git SSH account (e.g. AUR):
# Either form works; the second is what `git clone ssh://aur@aur.archlinux.org/...`
# passes through automatically.
gitway --user aur aur.archlinux.org git-upload-pack 'package.git'
gitway aur@aur.archlinux.org git-upload-pack 'package.git'Use as GIT_SSH_COMMAND for a single operation:
Nushell:
$env.GIT_SSH_COMMAND = "gitway"
git clone git@github.com:org/repo.gitIon:
export GIT_SSH_COMMAND=gitway
git clone git@github.com:org/repo.git
Bash/Brush:
GIT_SSH_COMMAND=gitway git clone git@github.com:org/repo.gitAdd GHE host-key fingerprints to ~/.config/gitway/known_hosts. One entry per
line, in the same format as OpenSSH known_hosts:
ghe.corp.example.com SHA256:<base64-encoded-fingerprint>
Retrieve the fingerprint from your GHE instance:
ssh-keyscan -t ed25519 ghe.corp.example.com | ssh-keygen -lf -For each connection, Gitway searches for an identity in this fixed priority order:
--identity <FILE>— explicit path from the command line~/.ssh/id_ed25519~/.ssh/id_ecdsa~/.ssh/id_rsa- SSH agent via
$SSH_AUTH_SOCK(Linux/macOS)
If a key file is encrypted, Gitway prompts for the passphrase on the terminal.
Gitway is a stateless transport binary: Git launches a fresh gitway process
for every SSH transport operation (clone, fetch, push, remote-probing
helpers invoked by tools like gh). Each process decrypts the key from
scratch, so an encrypted key without an agent loaded produces one prompt per
invocation — a single gh repo clone can easily surface four or five.
Load the key into ssh-agent once per session and all subsequent operations
authenticate through the agent without prompting:
ssh-add ~/.ssh/id_ed25519Gitway detects $SSH_AUTH_SOCK and, when an agent is reachable, skips the
file-based passphrase prompt entirely. The same agent also satisfies
ssh-keygen -Y sign (Git's default signer for gpg.format = ssh), so signed
commits stop prompting as well.
For persistence across reboots, add ssh-add ~/.ssh/id_ed25519 to your shell
startup file, or use a desktop keyring that unlocks on login (e.g.
gnome-keyring-daemon --components=ssh, gcr-ssh-agent, or the macOS
Keychain-backed agent).
Caching decrypted keys inside Gitway itself would require a long-lived daemon,
duplicating ssh-agent and expanding the attack surface — outside the scope
of a transport client.
Gitway 0.4 ships a subset of ssh-keygen so you can generate keys and
SSH-sign git commits without openssh-clients installed.
# Generate an Ed25519 keypair:
gitway keygen generate -f ~/.ssh/id_ed25519
# Fingerprint an existing key:
gitway keygen fingerprint -f ~/.ssh/id_ed25519.pub
# Derive the public key from a private key:
gitway keygen extract-public -f ~/.ssh/id_ed25519 -o ~/.ssh/id_ed25519.pub
# Change (or remove) the passphrase:
gitway keygen change-passphrase -f ~/.ssh/id_ed25519All subcommands honor --json / --format json and the agent-env
detection rules documented under Dual-mode output (SFRS Rule 1).
# Sign stdin, print the armored SSH SIGNATURE to stdout:
echo 'hello' | gitway sign --namespace git --key ~/.ssh/id_ed25519
# Sign a file:
gitway sign --namespace git --key ~/.ssh/id_ed25519 --input msg.txt --output msg.sigGit invokes gpg.ssh.program when gpg.format=ssh, passing it the exact
ssh-keygen -Y sign / -Y verify argv. The gitway-keygen binary ships
alongside gitway specifically to sit in that slot — it is byte-compatible
with ssh-keygen's stdout so git's output parser is satisfied.
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global gpg.ssh.program gitway-keygenUpload the same public key to GitHub under Settings → SSH and GPG keys → New SSH key → Key type: Signing Key. After that, every commit is SSH-signed via Gitway's code and GitHub shows Verified next to it — with zero OpenSSH involvement.
Everything above uses the pure-Rust ssh-key crate (RustCrypto) for the
OpenSSH key format and the SSHSIG file-signature blob.
Gitway 0.5 adds a client for the SSH agent wire protocol. It talks to
any agent listening on $SSH_AUTH_SOCK — OpenSSH's ssh-agent,
Gitway's own future daemon (v0.6), or anything else that speaks the
protocol. Unix-only for now; Windows named-pipe support lands with the
daemon in v0.6.
# Load your default key (matches `ssh-add`):
gitway agent add
# Load a specific key with a 10-minute lifetime:
gitway agent add --lifetime 600 ~/.ssh/id_ed25519
# List what's currently loaded:
gitway agent list # short fingerprints
gitway agent list -L # full public-key lines
# Remove one or all identities:
gitway agent remove ~/.ssh/id_ed25519.pub
gitway agent remove --all
# Lock / unlock the agent with a passphrase:
gitway agent lock
gitway agent unlockAll subcommands honor --json / --format json and the agent-env
detection rules documented under Avoiding repeated passphrase prompts.
Tools that shell out to ssh-add by name (IDEs, git-credential-manager,
systemd user units) can invoke gitway-add unchanged. It accepts the
flags most-commonly used: -l, -L, -d <file>, -D, -x, -X,
-t <seconds>, -E <hash>, -c, plus bare positional paths for
add.
eval $(ssh-agent -s) # or `eval $(gitway agent start -s)` for the Gitway-native daemon
gitway-add ~/.ssh/id_ed25519
gitway-add -lGitway 0.6 ships an SSH agent daemon of its own. It speaks the standard
SSH agent wire protocol, so every SSH client — including real OpenSSH —
can use it as a transparent stand-in for ssh-agent. Unix-only;
Windows named-pipe transport is a follow-up within the v0.6.x series.
# Detach into the background, export the socket + PID into the shell,
# and return control to the prompt — mirrors `ssh-agent` exactly.
eval $(gitway agent start -s)
# Now any client — gitway-add, ssh-add, ssh-keygen -Y sign — uses it:
gitway-add ~/.ssh/id_ed25519
ssh-add -l # OpenSSH's ssh-add talks to the Gitway agentWithout -D, gitway agent start respawns itself as a fully detached
session leader (new session via setsid(2), ppid reparented to init,
stdio redirected to /dev/null). Use -D instead to stay in the
foreground — handy for debugging, systemd user units, or inline
strace. -s emits Bourne-shell export lines; -c emits csh/fish
setenv lines. With neither flag, Gitway picks based on $SHELL.
-t <seconds> sets a default lifetime — after that duration, the agent
silently evicts the key. Individual gitway agent add -t <sec>
requests override the daemon-wide default.
gitway agent stop # reads $SSH_AGENT_PID or the pid fileA hardened user unit ships in
packaging/systemd/gitway-agent.service.
Install, enable, and point your shell at the socket:
mkdir -p ~/.config/systemd/user
cp packaging/systemd/gitway-agent.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now gitway-agent.service
# add to .bashrc / .zshrc / config.fish
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/gitway-agent.sock"The unit runs gitway agent start -D under a @system-service syscall
filter with read-only $HOME, private /tmp, and no new privileges —
see the file header for the full hardening list and how to change
ExecStart= if your gitway binary lives outside /usr/local/bin.
Load a key with -c and the daemon asks for approval every time a
client tries to sign with it:
export SSH_ASKPASS=/usr/bin/ssh-askpass # or ksshaskpass, etc.
gitway-add -c ~/.ssh/id_ed25519 # the -c matches ssh-add -cThe daemon invokes $SSH_ASKPASS with SSH_ASKPASS_PROMPT=confirm
when a sign request arrives; exit 0 from that program approves the
sign, anything else denies it. The same security rules as the
client-side passphrase flow apply — SSH_ASKPASS must be an absolute
path and must not be world-writable. If SSH_ASKPASS is unset or
misconfigured, confirm-required sign requests fail safe (deny) rather
than proceed unprompted. Running under systemd? $SSH_ASKPASS needs
to be in the unit's Environment= or the user session env that
started the unit — systemctl --user import-environment SSH_ASKPASS DISPLAY WAYLAND_DISPLAY XAUTHORITY after logging in does the right
thing for GUI askpass binaries.
- Fully supported: Ed25519, ECDSA (P-256, P-384, P-521), and RSA
(
rsa-sha2-256andrsa-sha2-512) sign operations. Cross-validated against real OpenSSH —ssh-add,ssh-keygen -Y sign, andsshtransport all accept Gitway-agent signatures unchanged. The legacy SHA-1ssh-rsawire algorithm is rejected; OpenSSH 8.2+ and every modern Git host request SHA-2 by default, so this only matters if you explicitly re-enable SHA-1 in your client config. - Windows: the agent client and daemon both speak over named pipes
(
\\.\pipe\gitway-agent.<PID>by default, compatible with OpenSSH for Windows's\\.\pipe\openssh-ssh-agent).gitway agent start -Druns a foreground daemon; Ctrl+C triggers graceful shutdown. Background mode (no-D) andgitway agent stopare Unix-only — usestart /B,Stop-Process, Task Manager, or a Windows service wrapper instead.
Gitway's SSH stack (transport, keys, signing, agent) lives in the
Anvil crate, published as
anvil-ssh.
Add to Cargo.toml:
[dependencies]
anvil-ssh = "1.0"The legacy gitway-lib 0.9.x release on crates.io is deprecated; migrate by
swapping the dep and replacing use gitway_lib::*; with use anvil_ssh::*;.
The canonical type names since Anvil 0.2.0 are AnvilSession, AnvilConfig,
AnvilError; the legacy GitwaySession / GitwayConfig / GitwayError
aliases remain available as #[deprecated] re-exports through the entire
anvil-ssh 1.x line and will be removed in 2.0.0. See
docs/migration-from-v0.9.md for the full
migration guide.
use anvil_ssh::{AnvilConfig, AnvilSession};
#[tokio::main]
async fn main() -> Result<(), anvil_ssh::AnvilError> {
let config = AnvilConfig::github();
let mut session = AnvilSession::connect(&config).await?;
session.authenticate_best(&config).await?;
let exit_code = session.exec("git-upload-pack 'org/repo.git'").await?;
session.close().await?;
std::process::exit(exit_code as i32);
}use anvil_ssh::AnvilConfig;
use std::path::PathBuf;
let config = AnvilConfig::builder("ghe.corp.example.com")
.port(22)
.identity_file(PathBuf::from("/home/user/.ssh/id_ed25519"))
.build();use anvil_ssh::AnvilError;
fn handle(err: &AnvilError) {
if err.is_host_key_mismatch() {
eprintln!("Possible MITM — aborting.");
} else if err.is_no_key_found() {
eprintln!("No SSH key found. Pass --identity or start an SSH agent.");
} else if err.is_authentication_failed() {
eprintln!("Server rejected the key. Check your GitHub SSH key settings.");
}
}| Method | Default | Description |
|---|---|---|
.port(u16) |
22 |
SSH port |
.username(str) |
"git" |
Remote username |
.identity_file(path) |
none | Explicit private key path |
.cert_file(path) |
none | OpenSSH certificate path |
.skip_host_check(bool) |
false |
Bypass fingerprint pinning |
.inactivity_timeout(Duration) |
60 s |
Session idle timeout |
.custom_known_hosts(path) |
~/.config/gitway/known_hosts |
GHE fingerprint file |
.fallback(Option<(String, u16)>) |
ssh.github.com:443 |
Port-22 fallback |
Gitway embeds GitHub's published SHA-256 fingerprints for all three key types.
On every connection the server's key is hashed and compared against this list;
any mismatch aborts immediately with a HostKeyMismatch error.
Current fingerprints (verified 2026-04-05, source):
| Algorithm | SHA-256 fingerprint |
|---|---|
| Ed25519 | SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU |
| ECDSA | SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM |
| RSA | SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s |
If GitHub rotates its keys, update hostkey.rs and cut a patch release.
Passphrase strings are wrapped in Zeroizing<String> and zeroed before the
allocation is released. Private key material in memory is managed by russh's
CryptoVec, which zeroes its buffer on drop.
git clone https://github.com/steelbore/gitway
cd gitway
# Requires a C compiler (gcc) for the aws-lc-rs cryptography crate.
cargo build --releaseThe release binary is at target/release/gitway.
git clone https://github.com/steelbore/gitway
cd gitway
cargo build --releasegit clone https://github.com/steelbore/gitway
cd gitway
cargo build --release
git clone https://github.com/steelbore/gitway
cd gitway
cargo build --releaseNixOS users should use the included shell.nix environment, which provides the correct C compiler and overrides problematic system RUSTFLAGS.
# Enter the dev shell interactively
nix-shell
# Then build inside the shell
cargo build --release
# Or run the build in one command
nix-shell --run 'cargo build --release'# Enter the dev shell interactively
nix-shell
# Then build inside the shell
cargo build --release
# Or run the build in one command
nix-shell --run 'cargo build --release'
# Enter the dev shell interactively
nix-shell
# Then build inside the shell
cargo build --release
# Or run the build in one command
nix-shell --run 'cargo build --release'The default NixOS environment sets RUSTFLAGS="-C target-cpu=x86-64-v4", which requires AVX-512 instructions not available on many CPUs. The shell.nix resets this to -C target-cpu=native and provides gcc without requiring global installation.
Unit tests and doc tests (all shells):
cargo testIntegration tests (require network access and a GitHub SSH key):
Nushell:
$env.GITSSH_INTEGRATION_TESTS = "1"
cargo test --test test_connection
cargo test --test test_cloneIon:
export GITSSH_INTEGRATION_TESTS=1
cargo test --test test_connection
cargo test --test test_clone
Bash/Brush:
GITSSH_INTEGRATION_TESTS=1 cargo test --test test_connection
GITSSH_INTEGRATION_TESTS=1 cargo test --test test_cloneGitway is built on russh, a pure-Rust SSH library originally written by Pierre-Étienne Meunier and maintained by Warp Technologies and contributors. russh is licensed under the Apache License 2.0.
The complete list of dependencies and their licences is in NOTICE.md.
Copyright (C) 2026 Mohamed Hammad
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See LICENSE for the full text.