SSHwitch builds a validated OpenSSH configuration from provider-managed SSH records on Linux, macOS, WSL, and Windows. Bitwarden is the first built-in provider.
It uses the provider's SSH agent by default. An explicitly confirmed disk identity backend is available for environments that require exported private keys.
For every SSH record returned by the configured provider:
- The item name becomes the SSH alias after deterministic sanitization.
- The custom
HostNamefield becomes the destination host. - The optional custom
Userfield becomes the SSH user. - The public key selects the corresponding identity from the configured agent.
For example, an item named Production Server with HostName=prod.example.com
and User=ubuntu produces:
Host production-server
HostName prod.example.com
User ubuntu
IdentityFile "~/.ssh/sshwitch/current/keys/production-server.pub"
IdentitiesOnly yesThe main ~/.ssh/config receives one exact include:
Include ~/.ssh/sshwitch/current/configManual SSH entries remain outside the generated file.
- A failed provider command, parse, validation, or filesystem operation leaves the active generation unchanged.
- Provider records must match the versioned, secret-free canonical schema.
- Vault-derived metadata containing control characters or unsafe whitespace is rejected.
- Duplicate normalized aliases are rejected.
- Public keys and the generated OpenSSH config are validated before publishing.
- Concurrent syncs are blocked by a lock.
- Agent mode never persists private keys.
- Agent mode verifies that every provider public key is available from the selected SSH agent before publication.
- Switching from disk mode to agent mode replaces the complete generation, removing managed private-key files.
- Windows files are written as UTF-8 without BOM on Windows PowerShell 5.1 and PowerShell 7.
- Uninstall removes only the dedicated tool-owned directory and exact profile or SSH configuration entries.
See SECURITY.md for the trust model and vulnerability reporting.
Provider adapters authenticate, list SSH records, and optionally export one private key when the explicit disk policy permits it. The core owns all normalization, validation, agent matching, generation, Git configuration, staging, and publication.
Adapters return a version 1 envelope defined by
providers/record.schema.json. The envelope can
contain public keys and validated metadata but cannot contain private-key
fields. Private-key export is a separate provider capability and is never
called when private_key_policy=never.
The current preferences are:
| Preference | Current values | Purpose |
|---|---|---|
provider |
bitwarden |
Record source adapter |
identity_backend |
agent, disk |
How OpenSSH uses the identity |
private_key_policy |
never, export |
Whether the provider may export private keys |
auto_sync |
off, daily |
Whether the first shell opened after 24 hours should request a refresh |
Only agent/never and disk/export are accepted. Existing
agent_mode=bitwarden and agent_mode=disk preferences are mapped at runtime;
running setup writes the version 2 preference format.
Create an item of type SSH Key with:
| Value | Required | Purpose |
|---|---|---|
| Item name | Yes | SSH alias, after sanitization |
HostName custom field |
Yes for host entries | Server hostname or IP |
User custom field |
No | SSH username |
Email or GitEmail custom field |
No | Principal for allowed_signers on git-sign |
SSHwitchRole=git-sign |
No | Marks any named item as the Git signing identity |
The legacy reserved item name git-sign remains supported. A
SSHwitchRole=git-sign field can instead mark an item with any display name as
the signing identity. The previous SyncSSHRole field remains accepted during
migration. Signing identities are not emitted as SSH hosts.
Alias normalization lowercases the item name, replaces characters outside
a-z, 0-9, ., _, and - with -, and trims leading/trailing dashes.
Sync fails if two names normalize to the same alias.
All platforms require:
- Bitwarden Desktop with SSH Agent enabled;
- Bitwarden CLI (
bw), logged in withbw login; - OpenSSH client tools (
ssh,ssh-add, andssh-keygen).
Linux, macOS, and WSL additionally require:
- POSIX
sh; jq;curlandtarfor the release installer;sha256sumon Linux/WSL or the macOS-providedshasum.
WSL agent mode additionally requires:
socat;- a trusted
npiperelay.exebuild available onPATH.
Git is required only when Git commit signing is enabled.
Installers download the tagged release archive, verify its SHA-256 checksum, stage it, and restore the previous installation if setup fails.
curl -fsSL https://github.com/pablousx/sshwitch/releases/latest/download/install.sh | shThis installs the latest published release. To pin a specific release, replace
vX.Y.Z with the version you want:
curl -fsSL https://github.com/pablousx/sshwitch/releases/download/vX.Y.Z/install.sh | shirm https://github.com/pablousx/sshwitch/releases/latest/download/install.ps1 | iexThis installs the latest published release. To pin a specific release, replace
vX.Y.Z with the version you want:
irm https://github.com/pablousx/sshwitch/releases/download/vX.Y.Z/install.ps1 | iexIf the execution policy is Restricted, review the scripts and use an
appropriate CurrentUser policy before installation.
Linux, macOS, or WSL:
git clone https://github.com/pablousx/sshwitch "$HOME/ssh"
cd "$HOME/ssh"
./linux/setup.shWindows:
git clone https://github.com/pablousx/sshwitch "$HOME\ssh"
Set-Location "$HOME\ssh"
.\windows\setup.ps1Setup preserves existing preferences when s is selected. Disk mode requires
typing an explicit confirmation phrase.
Linux, macOS, and WSL:
sshwitch
sshwitch --dry-run
sshwitch --timings
sshwitch --versionWindows:
SSHwitch
SSHwitch -DryRun
SSHwitch -Timings
SSHwitch -VersionA dry run authenticates with the configured provider, generates and validates a staging generation, and reports success without replacing active SSH or Git settings.
--timings/-Timings reports secret-free stage durations for authentication,
vault synchronization, vault listing, generation, and publication. Provider
responses, session values, keys, and item metadata are never included.
Setup can enable an optional auto_sync=daily preference. The shell profile
performs only a timestamp check. If the last successful sync is at least 24
hours old and BW_SESSION is already available, SSHwitch starts a quiet,
noninteractive sync in the background. Shell startup does not wait for vault
network access, generation, validation, or publication.
If no session key is available, the profile prints one concise reminder to run
sshwitch/SSHwitch interactively. SSHwitch does not persist the Bitwarden
master password or session key. A failed automatic sync leaves the active
generation and last-success timestamp unchanged, so a later shell can retry.
The optimized generation path lists agent identities once and reuses the in-memory Bitwarden list response for explicit disk-mode exports. Private keys remain inside adapter memory until written directly to the restricted staged generation; canonical records and timing output remain secret-free.
During setup, enable Git SSH signing and add a Bitwarden SSH key item named
git-sign, or set SSHwitchRole=git-sign on another SSH key item. The optional
Email or GitEmail field becomes the principal in the generated
allowed_signers file; the global Git email is used as a fallback.
When enabled, SSHwitch owns these global Git settings:
gpg.formatuser.signingkeycommit.gpgsigngpg.ssh.allowedSignersFile, when a principal is available
The previous value and whether it existed are recorded before modification.
Disabling the feature or uninstalling restores a setting only if it still
contains the value written by SSHwitch. User-modified values are preserved.
If signing is enabled but no git-sign public key is available, SSHwitch warns,
still syncs the SSH host configuration, and restores any Git settings that it
previously owned so Git is not left pointing at a missing managed key. Later
user changes remain preserved by the ownership check.
Setup creates shell helpers that manage one exact socat child process through
a PID file and lock. The Unix socket is created with mode 600.
Linux ssh
-> ~/.bitwarden-ssh-agent.sock
-> socat
-> npiperelay.exe
-> Windows OpenSSH agent pipe
-> Bitwarden Desktop
If the Windows agent or the bridge restarts:
reset-ssh-agentThe reset helper terminates only the recorded bridge PID; it does not broadly
kill other npiperelay.exe processes.
Bitwarden uses different sockets for its two macOS
distributions. SSHwitch prefers an
active socket; when neither socket exists yet, the App Store application
container selects the App Store path and the .dmg path is the fallback:
App Store: ~/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock
.dmg: ~/.bitwarden-ssh-agent.sock
Keep Bitwarden Desktop open with SSH Agent enabled before running sshwitch.
Linux, macOS, and WSL:
| Path | Purpose |
|---|---|
~/.ssh/sshwitch/current/ |
Active generated config, keys, and manifest |
~/.config/sshwitch/config |
Preferences, or $XDG_CONFIG_HOME |
~/.local/state/sshwitch/ |
Lock, last-success timestamp, backups, bridge PID, and Git restoration state |
~/.ssh/sshwitch-env.sh |
Shell functions and agent environment |
Windows:
| Path | Purpose |
|---|---|
~\.ssh\sshwitch\current\ |
Active generated config, keys, and manifest |
%APPDATA%\sshwitch\config.json |
Preferences |
%LOCALAPPDATA%\sshwitch-state\ |
Lock, last-success timestamp, backups, and Git restoration state |
%LOCALAPPDATA%\sshwitch\ |
Installed scripts |
Running SSHwitch setup imports existing preferences and Git-restoration state
from the former sync-ssh directories. The first successful sync replaces the
old Include ~/.ssh/sync-ssh/current/config with the SSHwitch Include only
after a complete new generation passes validation. The former sync-ssh
command remains as a deprecated shell alias or PowerShell wrapper.
Legacy managed blocks are migrated only when exactly one correctly ordered start/end marker pair exists. Malformed markers cause sync to stop without editing the file.
Linux, macOS, and WSL:
curl -fsSL https://github.com/pablousx/sshwitch/releases/latest/download/uninstall.sh | shWindows:
irm https://github.com/pablousx/sshwitch/releases/latest/download/uninstall.ps1 | iexThe uninstallers ask before removing generated SSH files, the include/legacy
block, or restoring Git settings. They do not delete the legacy generic
~/.ssh/keys directory because it may contain unrelated user files.
Run:
bw loginThen retry sync.
Confirm that Bitwarden Desktop is open, the vault is unlocked, and SSH Agent is enabled. Sync deliberately fails before publication when a provider public key is missing from the agent. Inspect:
ssh-add -LOn WSL, run reset-ssh-agent before retrying.
Rename one of the Bitwarden items so the normalized names differ. Sync will not choose one key or overwrite another.
Back up ~/.ssh/config, then correct or remove the old
START/END SYNC-SSH MANAGED SECTION pair. Sync deliberately refuses to guess
which manual lines belong to a malformed block.
See CONTRIBUTING.md for checks and behavioral requirements.
CI covers the shared POSIX implementation on Linux and macOS, including dash
and Bash syntax checks on Linux, plus Windows PowerShell 5.1, PowerShell 7,
PSScriptAnalyzer, and Pester.
Every non-release push to main evaluates Conventional Commit messages since
the latest release and opens or updates a SemVer release pull request. Merging
that release pull request does not start another proposal run. The proposed
version uses these rules:
| Commit | Version change |
|---|---|
fix: ... |
Patch, for example 1.1.0 to 1.1.1 |
feat: ... |
Minor, for example 1.1.0 to 1.2.0 |
feat!: ..., fix!: ..., or a BREAKING CHANGE: footer |
Major, for example 1.1.0 to 2.0.0 |
The release pull request updates VERSION and CHANGELOG.md. Merging it into
main runs the publishing workflow, which creates the matching vX.Y.Z tag
and publishes .tar.gz and .zip packages plus SHA-256 files consumed by the
installers. Installer and uninstaller scripts are attached as release assets,
and the release notes include version-pinned POSIX and Windows installation
commands. Publishing is idempotent and never moves an existing tag.