:computer: make your ssh client smarter
3,220
stars
850
commits
Go
primary language
Sep 8, 2026
updated
A transparent wrapper that adds support for regex, aliases, gateways, dynamic hostnames, graphviz, json output, yaml configuration, and more to SSH.
lib-ssh wraps assh as a ProxyCommand; it means that it works seamlessly with:
lib-ssh or ssh (i.e., Tower, Atom.io, SSH Tunnel Manager)For specific examples, see 3rd Party Integration
gate -> gate.domain.tldnetcat and socat as default fallbacksassh can use the ProxyCommand with netcat feature of OpenSSH transparently and without the pain of using extended configuration.
Connect to hosta using hostb as a gateway.
flowchart
direction TB
y[you]
a[hosta]
b[hostb]
fw((firewall))
style fw fill:#f00,color:#fff
y ==x fw
fw .-> a
y --> b
b --> a
$ ssh hosta/hostb
user@hosta $
Equivalent to ssh -o ProxyCommand="ssh hostb nc %h %p" hosta
Connect to hosta using hostb as a gateway using hostc as a gateway.
flowchart
direction TB
y[you]
a[hosta]
b[hostb]
c[hostc]
fw((firewall))
style fw fill:#f00,color:#fff
y ==x fw
fw ..-> a
y --> c
c --> b
b --> a
$ ssh hosta/hostb/hostc
user@hosta $
Equivalent to ssh -o ProxyCommand="ssh -o ProxyCommand='ssh hostc nc %h %p' hostb nc %h %p" hosta
You can define an equivalent of the "ProxyCommand with netcat" feature of OpenSSH, with a simpler syntax, more advanced workflows, and a unique fallback feature.
Let's consider the following assh.yml file
hosts:
hosta:
Hostname: 1.2.3.4
hostb:
Hostname: 5.6.7.8
Gateways: hosta
hostc:
Hostname: 9.10.11.12
Gateways: hostb
hostd:
Hostname: 13.14.15.16
GatewayConnectTimeout: 2
Gateways:
- direct
- hosta
ssh hosta -> ssh 1.2.3.4ssh hostb -> ssh -o ProxyCommand="ssh hostb nc %h %p" hostassh hostc -> ssh -o ProxyCommand="ssh -o ProxyCommand='ssh hostc nc %h %p' hostb nc %h %p" hostassh hostd ->
ssh 13.14.15.16ssh -o ProxyCommand="ssh hostd nc %h %p" hosta~/.ssh/config file when neededssh -vv, assh will automatically run in debug mode)ControlPath directories so you can use slashes in your ControlPath option, can be enabled with the ControlMasterMkdir: true configuration in host or globally.BeforeConnect is called just before assh tries to connect to the remote SSH port.
Note: BeforeConnect will be called for each SSH connection; if you use multiple gateways, it will be called for each gateways until one succeed to connect.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
OnConnect is called as soon as assh is connected to the remote SSH port.
Note: OnConnect is not aware of the authentication process and will always be raised.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
// Stats: https://pkg.go.dev/moul.io/assh/v2/pkg/commands#ConnectionStats
{{.Stats.ConnectedAt}} // 2016-07-20 11:19:23.467900594 +0200 CEST
OnConnectError is called when assh fails to open a new TCP connection.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
// Error
{{.Error}} // dial tcp: lookup localhost: no such host
OnDisconnect is called as the assh socket is closed.
warning: if you don't see a notification when closing an SSH connection, then you probably have ControlMaster configured; OnDisconnect is not linked to the ssh program but to its socket which may stay alive even after exiting the ssh program.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
// Stats: https://pkg.go.dev/moul.io/assh/v2/pkg/commands#ConnectionStats
{{.Stats.ConnectedAt}} // 2016-07-20 11:19:23.467900594 +0200 CEST
{{.Stats.WrittenBytes}} // 3613
{{.Stats.WrittenBytesHuman}} // 3.6kb
{{.Stats.DisconnectAt}} // 2016-07-20 11:19:29,520515792 +0200 CEST
{{.Stats.ConnectionDuration}} // 6.052615198s
{{.Stats.ConnectionDurationHuman}} // 6s
{{.Stats.AverageSpeed}} // 596.933bps
{{.Stats.AverageSpeedHuman}} // 3.4kb/s
BeforeConfigWrite is called just before assh rewrite the ~/.ssh/config file.
Example of Golang template variables:
{{.SSHConfigPath}} // ~/.ssh/config
Exec driver uses Golang's template system to execute a shell command
Usage: exec <binary> [args...]
defaults:
Hooks:
OnConnect: exec echo '{{.Host}}' | jq .
# executes: `echo '{"HostName":"localhost","Port":"22","User":"moul","ControlPersist":"yes",...}' | jq .
# which results in printing a pretty JSON of the host
# {
# "HostName": "localhost",
# "Port": "22",
# "User": "moul",
# "ControlPersist": "yes",
# ...
# }
defaults:
Hooks:
OnConnect: exec echo 'New SSH connection to {{.Host.Prototype}}.' | mail -s "SSH connection journal" m+assh@42.am
# send an email with the connection prototype
defaults:
Hooks:
BeforeConfigWrite: exec cp {{.SSHConfigPath}} {{.SSHConfigPath}}.backup
# make a copy of ~/.ssh/config before being rewritten
defaults:
Hooks:
AfterConfigWrite: 'exec echo "# date: `date`" >> {{.SSHConfigPath}}'
# Append a comment with the compilation date to the generated ~/.ssh/config file
defaults:
Hooks:
AfterConfigWrite: 'exec cat /path/to/my/provider/generated/.ssh/config >> {{.SSHConfigPath}}'
# Append another .ssh/config file to the generated .ssh/config file
The exec commands are blocking, a new driver for background tasks is planned. For now, you can run a job in background like this:
defaults:
Hooks:
OnConnect:
- exec sleep 60 &
# execute the `sleep 60` command in background (non-blocking)
# if you quit your ssh connection, the process will continue in background.
Write driver uses Golang's template system to write out data to stdout
Usage: write <line:string...>
defaults:
Hooks:
OnConnect:
- write New SSH connection to {{.Host.Prototype}}.
# writes: "New SSH connection to moul@127.0.0.1:22." on the terminal on connection
defaults:
Hooks:
OnDisconnect:
- "write SSH connection to {{.Host.Name}} closed, {{ .Stats.WrittenBytes }} bytes written in {{ .Stats.ConnectionDuration }} ({{ .Stats.AverageSpeed }})"
# writes: SSH connection to localhost closed, 40 bytes written.
Notify driver uses Golang's template system to open Desktop notifications.
Usage: notify <line:string...>
defaults:
Hooks:
OnConnect: notify New SSH connection to {{.Host.Prototype}}.

defaults:
Hooks:
OnDisconnect:
- "notify SSH connection to {{.Host.Name}} closed, {{ .Stats.WrittenBytes }} bytes written in {{ .Stats.ConnectionDuration }} ({{ .Stats.AverageSpeed }})"

assh now manages the ~/.ssh/config file, take care to keep a backup your ~/.ssh/config file.
~/.ssh/assh.yml is a YAML file containing:
hosts dictionary containing multiple HOST definitionsdefaults section containing global flagsincludes section containing path to other configuration fileshosts:
homer:
# ssh homer -> ssh 1.2.3.4 -p 2222 -u robert
Hostname: 1.2.3.4
User: robert
Port: 2222
bart:
# ssh bart -> ssh 5.6.7.8 -u bart <- direct access
# or ssh 5.6.7.8/homer -u bart <- using homer as a gateway
Hostname: 5.6.7.8
User: bart
Gateways:
- direct # tries a direct access first
- homer # fallback on homer gateway
maggie:
# ssh maggie -> ssh 5.6.7.8 -u maggie <- direct access
# or ssh 5.6.7.8/homer -u maggie <- using homer as a gateway
User: maggie
Inherits: bart # inherits rules from "bart"
bart-access:
# ssh bart-access -> ssh home.simpson.springfield.us -u bart
Inherits:
- bart-template
- simpson-template
lisa-access:
# ssh lisa-access -> ssh home.simpson.springfield.us -u lisa
Inherits:
- lisa-template
- simpson-template
marvin:
# ssh marvin -> ssh marvin -p 23
# ssh sad-robot -> ssh sad-robot -p 23
# ssh bighead -> ssh bighead -p 23
# aliases inherit everything from marvin, except hostname
Port: 23
Aliases:
- sad-robot
- bighead
dolphin:
# ssh dolphin -> ssh dolphin -p 24
# ssh ecco -> ssh dolphin -p 24
# same as above, but with fixed hostname
Port: 24
Hostname: dolphin
Aliases: ecco
RateLimit: 10M # 10Mbytes/second rate limiting
schooltemplate:
User: student
IdentityFile: ~/.ssh/school-rsa
ForwardX11: yes
schoolgw:
# ssh school -> ssh gw.school.com -l student -o ForwardX11=no -i ~/.ssh/school-rsa
Hostname: gw.school.com
ForwardX11: no
Inherits: schooltemplate
"expanded-host[0-7]*":
# ssh somehost2042 -> ssh somehost2042.some.zone
Hostname: "%h.some.zone"
vm-*.school.com:
# ssh vm-42.school.com -> ssh vm-42.school.com/gw.school.com -l student -o ForwardX11=yes -i ~/.ssh/school-rsa
Gateways: schoolgw
Inherits: schooltemplate
# do not automatically create `ControlPath` -> may result in error
ControlMasterMkdir: true
"*.shortcut1":
ResolveCommand: /bin/sh -c "echo %h | sed s/.shortcut1/.my-long-domain-name.com/"
"*.shortcut2":
ResolveCommand: /bin/sh -c "echo $(echo %h | sed s/.shortcut2//).my-other-long-domain-name.com"
"*.scw":
# ssh toto.scw -> 1. dynamically resolves the IP address
# 2. ssh {resolved ip address} -u root -p 22 -o UserKnownHostsFile=null -o StrictHostKeyChecking=no
# requires github.com/scaleway/scaleway-cli
ResolveCommand: /bin/sh -c "scw inspect -f {{.PublicAddress.IP}} server:$(echo %h | sed s/.scw//)"
User: root
Port: 22
UserKnownHostsFile: /dev/null
StrictHostKeyChecking: no
my-env-host:
User: user-$USER
Hostname: ${HOSTNAME}${HOSTNAME_SUFFIX}
templates:
# Templates are similar to Hosts; you can inherit from them
# but you cannot ssh to a template
bart-template:
User: bart
lisa-template:
User: lisa
simpson-template:
Host: home.simpson.springfield.us
defaults:
# Defaults are applied to each hosts
ControlMaster: auto
ControlPath: ~/tmp/.ssh/cm/%h-%p-%r.sock
ControlPersist: yes
Port: 22
User: bob
Hooks:
# Automatically backup ~/.ssh/config
BeforeConfigWrite:
- 'exec set -x; cp {{.SSHConfigPath}} {{.SSHConfigPath}}.bkp'
AfterConfigWrite:
# Concat another `ssh_config` file with the one just generated by `assh`
- 'exec cat ~/.ssh/my-heroku-generated-config >> {{.SSHConfigPath}}'
# Alert me with a Desktop notification
- notify "{{.SSHConfigPath}} has been rewritten"
OnConnect:
# Log internal information to a file
- exec printf '{{.}}' | jq . >> ~/.ssh/last_connected_host.txt
# Alert me with a Desktop notification
- notify New SSH connection to {{.Host.Prototype}} at {{.Stats.ConnectedAt}}
# Write the host prototype to the terminal stderr
- write New SSH connection to {{.Host.Prototype}}
OnDisconnect:
# write on terminal and in a Desktop notification some statistics about the finished connection
- "write SSH connection to {{.Host.HostName}} closed, {{.Stats.WrittenBytes }} bytes written in {{.Stats.ConnectionDuration}} ({{.Stats.AverageSpeed}}bps)"
- "notify SSH connection to {{.Host.HostName}} closed, {{.Stats.WrittenBytes }} bytes written in {{.Stats.ConnectionDuration}} ({{.Stats.AverageSpeed}}bps)"
includes:
- ~/.ssh/assh.d/*.yml
- /etc/assh.yml
- $ENV_VAR/blah-blah-*/*.yml
ASSHBinaryPath: ~/bin/assh # optionally set the path of assh
For further inspiration, these assh.yml files on public GitHub projects can educate you on how people are using assh
assh usage
NAME:
assh - advanced ssh config
USAGE:
assh [global options] command [command options] [arguments...]
VERSION:
2.8.0 (HEAD)
AUTHOR(S):
Manfred Touron <https://github.com/moul/assh>
COMMANDS:
ping Send packets to the SSH server and display statistics
info Display system-wide information
config Manage ssh and assh configuration
sockets Manage control sockets
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--config value, -c value Location of config file (default: "~/.ssh/assh.yml") [$ASSH_CONFIG]
--debug, -D Enable debug mode [$ASSH_DEBUG]
--verbose, -V Enable verbose mode
--help, -h show help
--version, -v print the version
assh config buildRewrites and replaces the existing ~/.ssh/config file.
This action is automatically done by assh when detecting configuration changes. Running this command is useful to set up assh or repair the configuration file.
$ assh config build > ~/.ssh/config
assh config listList hosts and options.
$ assh config list
Listing entries
*.scw -> root@[hostname_not_specified]:22
StrictHostKeyChecking=no [custom options]
UserKnownHostsFile=/dev/null [custom options]
*.shortcut1 -> bob@[hostname_not_specified]:22
*.shortcut2 -> bob@[hostname_not_specified]:22
bart -> bart@5.6.7.8:22
bart-access -> bob@[hostname_not_specified]:22
dolphin -> bob@dolphin:24
expanded-host[0-7]* -> bob@%h.some.zone:22
homer -> robert@1.2.3.4:2222
lisa-access -> bob@[hostname_not_specified]:22
maggie -> maggie@[hostname_not_specified]:22
marvin -> bob@[hostname_not_specified]:23
my-env-host -> user-moul@[hostname_not_specified]:22
schoolgw -> bob@gw.school.com:22
ForwardX11=no [custom options]
schooltemplate -> student@[hostname_not_specified]:22
ForwardX11=yes [custom options]
IdentityFile=~/.ssh/school-rsa [custom options]
vm-*.school.com -> bob@[hostname_not_specified]:22
(*) General options:
ControlMaster: auto
ControlPath: ~/tmp/.ssh/cm/%h-%p-%r.sock
ControlPersist: yes
Port: 22
User: bob
assh config graphvizGenerate a graphviz graph of the hosts
$ assh config graphviz | dot -Tpng > assh-hosts.png

assh config search <keyword>Search for <keyword> in hosts and host options.
$ assh config search bart
Listing results for bart:
bart -> bart@5.6.7.8:22
bart-access -> moul@[hostname_not_specified]:22
assh infoDisplay system-wide information.
$ assh info
Debug mode (client): false
CLI Path: /path/to/assh
Go version: go1.6.2
OS/Arch: darwin/amd64
RC files:
- ~/.ssh/assh.yml
- ~/.ssh/assh.d/hosts.yml
- ~/.ssh/assh.d/moul.yml
- ~/.ssh/assh.d/test.yml
Statistics:
- 299 hosts
- 2 templates
- 4 included files
assh sockets listList active control sockets.
$ assh sockets list
4 active control sockets in "~/.ssh/cm/":
- bart/homer/lisa-22-root.sock (14 minutes)
- bart/homer-22-root.sock (14 minutes)
- bart-22-root.sock (14 minutes)
- marge-22-bart.sock (1 hour)
assh sockets flushClose active control sockets.
$ assh sockets flush
Closed 4 control sockets.
assh sockets masterCreate a master control sockets.
$ assh sockets master
assh pingSend packets to the SSH server and display stats.
$ assh ping -c 4 localhost
PING localhost (127.0.0.1) PORT 22 (ssh) PROTO tcp
Connected to 127.0.0.1: seq=0 time=321µs protocol=tcp port=22
Connected to 127.0.0.1: seq=1 time=501µs protocol=tcp port=22
Connected to 127.0.0.1: seq=2 time=550µs protocol=tcp port=22
Connected to 127.0.0.1: seq=3 time=641µs protocol=tcp port=22
--- localhost assh ping statistics ---
4 packets transmitted, 4 packets received, 0.00% packet loss
round-trip min/avg/max = 321µs/503.25µs/641µs
Get the latest version using GO (recommended way):
go install moul.io/assh/v2@latest
note: tested with Go1.7 or above
Get the latest released version using homebrew (Mac OS X):
brew install assh
Build the latest version
brew install assh --HEAD
Get a released version on: https://github.com/moul/assh/releases
Install with asdf-vm:
asdf plugin add assh
asdf install assh latest
asdf global assh latest
To improve experience when using advanced pattern matching, add the following at the end of your .bashrc / .zshrc / config.fish:
alias ssh="assh wrapper ssh --"
This step is not mandatory but highly recommended.
Note: ssh does not understand advanced patterns;
To bypass this limitation, assh maintains a list of known hosts and regenerate the ~/.ssh/config with all those expanded known hosts.
Without the wrapper, the ~/.ssh/config risks to be outdated when connecting to a new host for the first time and you will need to launch the command again.
With the wrapper, ssh will always be called with an updated ~/.ssh/config file.
~/.ssh/config: cp ~/.ssh/config ~/.ssh/config.backup~/.ssh/assh.yml fileassh config build > ~/.ssh/config to validate the syntax of your ~/.ssh/assh.yml file and automatically build your ~/.ssh/config fileExperimental: assh may run in Docker, however you will have limitations:
assh containers does not have any binaries except assh, you can't use ProxyCommand, ResolveCommand...ssh localhost will ssh to Docker hostdocker run -it --rm -v ~/.ssh:/.ssh moul/assh --help
assh in Docker is slower and has more limitations, but it may be useful for testing or if you plan to use a Docker host as a remote Gateway
assh uses the built-in netcat mode of OpenSSH (shipped with OpenSSH 5.4) by default.
If your ssh client doesn't support this feature, you can configure a custom ProxyCommand configuration, i.e.,
hosts:
myserver:
host: 1.2.3.4
gateways: mygateway
# configure a custom proxycommand
proxycommand: /bin/nc %h %p
mygateway:
host: 5.6.7.8
You can configure this rule globally:
defaults:
proxycommand: nc %h %p
Also, be sure to have netcat installed on your system, or use an alternative proxy binary, i.e., socat.
/etc/hosts and/or handle mDNS requestsassh resolves hostnames using the system built-in resolver, depending on the OS, you can enable new features and/or change modules order.
/etc/resolv.conf documentationunix_listener: "/Users/.../.ssh/cm/..." too long for Unix domain socketStarting with OpenSSH v6.7 the socket name can be shortened by configuring %C for the name expansion.
defaults:
ControlPath: ~/tmp/.ssh/cm/%C.sock
%C is a unique identifier based on a hash of the tuple of (local host, remote user, hostname, port).
Each time you call ssh, assh will check if the generated ~/.ssh/config file is outdated.
By default, it will transparently regenerate the configuration file if needed.
You can disable this behavior by generating the configuration file like this:
assh config build --no-automatic-rewrite
In your ansible.cfg under ssh_connection, make sure you have the following, changing the path to your assh:
[ssh_connection]
ansible_ssh_executable = '/usr/local/bin/assh wrapper ssh'
This project exists thanks to all the people who contribute. [Contribute].
Become a financial contributor and help us sustain our community. [Contribute]
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]
© 2009-2026 Manfred Touron - MIT License
(top 30 of 42)
Go
97.4%
Makefile
1.2%
:computer: make your ssh client smarter
3,220
stars
850
commits
Go
primary language
Sep 8, 2026
updated
A transparent wrapper that adds support for regex, aliases, gateways, dynamic hostnames, graphviz, json output, yaml configuration, and more to SSH.
lib-ssh wraps assh as a ProxyCommand; it means that it works seamlessly with:
lib-ssh or ssh (i.e., Tower, Atom.io, SSH Tunnel Manager)For specific examples, see 3rd Party Integration
gate -> gate.domain.tldnetcat and socat as default fallbacksassh can use the ProxyCommand with netcat feature of OpenSSH transparently and without the pain of using extended configuration.
Connect to hosta using hostb as a gateway.
flowchart
direction TB
y[you]
a[hosta]
b[hostb]
fw((firewall))
style fw fill:#f00,color:#fff
y ==x fw
fw .-> a
y --> b
b --> a
$ ssh hosta/hostb
user@hosta $
Equivalent to ssh -o ProxyCommand="ssh hostb nc %h %p" hosta
Connect to hosta using hostb as a gateway using hostc as a gateway.
flowchart
direction TB
y[you]
a[hosta]
b[hostb]
c[hostc]
fw((firewall))
style fw fill:#f00,color:#fff
y ==x fw
fw ..-> a
y --> c
c --> b
b --> a
$ ssh hosta/hostb/hostc
user@hosta $
Equivalent to ssh -o ProxyCommand="ssh -o ProxyCommand='ssh hostc nc %h %p' hostb nc %h %p" hosta
You can define an equivalent of the "ProxyCommand with netcat" feature of OpenSSH, with a simpler syntax, more advanced workflows, and a unique fallback feature.
Let's consider the following assh.yml file
hosts:
hosta:
Hostname: 1.2.3.4
hostb:
Hostname: 5.6.7.8
Gateways: hosta
hostc:
Hostname: 9.10.11.12
Gateways: hostb
hostd:
Hostname: 13.14.15.16
GatewayConnectTimeout: 2
Gateways:
- direct
- hosta
ssh hosta -> ssh 1.2.3.4ssh hostb -> ssh -o ProxyCommand="ssh hostb nc %h %p" hostassh hostc -> ssh -o ProxyCommand="ssh -o ProxyCommand='ssh hostc nc %h %p' hostb nc %h %p" hostassh hostd ->
ssh 13.14.15.16ssh -o ProxyCommand="ssh hostd nc %h %p" hosta~/.ssh/config file when neededssh -vv, assh will automatically run in debug mode)ControlPath directories so you can use slashes in your ControlPath option, can be enabled with the ControlMasterMkdir: true configuration in host or globally.BeforeConnect is called just before assh tries to connect to the remote SSH port.
Note: BeforeConnect will be called for each SSH connection; if you use multiple gateways, it will be called for each gateways until one succeed to connect.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
OnConnect is called as soon as assh is connected to the remote SSH port.
Note: OnConnect is not aware of the authentication process and will always be raised.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
// Stats: https://pkg.go.dev/moul.io/assh/v2/pkg/commands#ConnectionStats
{{.Stats.ConnectedAt}} // 2016-07-20 11:19:23.467900594 +0200 CEST
OnConnectError is called when assh fails to open a new TCP connection.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
// Error
{{.Error}} // dial tcp: lookup localhost: no such host
OnDisconnect is called as the assh socket is closed.
warning: if you don't see a notification when closing an SSH connection, then you probably have ControlMaster configured; OnDisconnect is not linked to the ssh program but to its socket which may stay alive even after exiting the ssh program.
Example of Golang template variables:
// Host: https://pkg.go.dev/moul.io/assh/v2/pkg/config#Host
{{.Host.Name}} // localhost
{{.Host.HostName}} // 127.0.0.1
{{.Host.Port}} // 22
{{.Host.User}} // moul
{{.Host.Prototype}} // moul@127.0.0.1:22
{{.Host}} // {"HostName":"localhost","Port":22","User":"moul","ControlPersist":"yes",...}
{{printf "%s:%s" .Host.HostName .Host.Port}} // localhost:22
// Stats: https://pkg.go.dev/moul.io/assh/v2/pkg/commands#ConnectionStats
{{.Stats.ConnectedAt}} // 2016-07-20 11:19:23.467900594 +0200 CEST
{{.Stats.WrittenBytes}} // 3613
{{.Stats.WrittenBytesHuman}} // 3.6kb
{{.Stats.DisconnectAt}} // 2016-07-20 11:19:29,520515792 +0200 CEST
{{.Stats.ConnectionDuration}} // 6.052615198s
{{.Stats.ConnectionDurationHuman}} // 6s
{{.Stats.AverageSpeed}} // 596.933bps
{{.Stats.AverageSpeedHuman}} // 3.4kb/s
BeforeConfigWrite is called just before assh rewrite the ~/.ssh/config file.
Example of Golang template variables:
{{.SSHConfigPath}} // ~/.ssh/config
Exec driver uses Golang's template system to execute a shell command
Usage: exec <binary> [args...]
defaults:
Hooks:
OnConnect: exec echo '{{.Host}}' | jq .
# executes: `echo '{"HostName":"localhost","Port":"22","User":"moul","ControlPersist":"yes",...}' | jq .
# which results in printing a pretty JSON of the host
# {
# "HostName": "localhost",
# "Port": "22",
# "User": "moul",
# "ControlPersist": "yes",
# ...
# }
defaults:
Hooks:
OnConnect: exec echo 'New SSH connection to {{.Host.Prototype}}.' | mail -s "SSH connection journal" m+assh@42.am
# send an email with the connection prototype
defaults:
Hooks:
BeforeConfigWrite: exec cp {{.SSHConfigPath}} {{.SSHConfigPath}}.backup
# make a copy of ~/.ssh/config before being rewritten
defaults:
Hooks:
AfterConfigWrite: 'exec echo "# date: `date`" >> {{.SSHConfigPath}}'
# Append a comment with the compilation date to the generated ~/.ssh/config file
defaults:
Hooks:
AfterConfigWrite: 'exec cat /path/to/my/provider/generated/.ssh/config >> {{.SSHConfigPath}}'
# Append another .ssh/config file to the generated .ssh/config file
The exec commands are blocking, a new driver for background tasks is planned. For now, you can run a job in background like this:
defaults:
Hooks:
OnConnect:
- exec sleep 60 &
# execute the `sleep 60` command in background (non-blocking)
# if you quit your ssh connection, the process will continue in background.
Write driver uses Golang's template system to write out data to stdout
Usage: write <line:string...>
defaults:
Hooks:
OnConnect:
- write New SSH connection to {{.Host.Prototype}}.
# writes: "New SSH connection to moul@127.0.0.1:22." on the terminal on connection
defaults:
Hooks:
OnDisconnect:
- "write SSH connection to {{.Host.Name}} closed, {{ .Stats.WrittenBytes }} bytes written in {{ .Stats.ConnectionDuration }} ({{ .Stats.AverageSpeed }})"
# writes: SSH connection to localhost closed, 40 bytes written.
Notify driver uses Golang's template system to open Desktop notifications.
Usage: notify <line:string...>
defaults:
Hooks:
OnConnect: notify New SSH connection to {{.Host.Prototype}}.

defaults:
Hooks:
OnDisconnect:
- "notify SSH connection to {{.Host.Name}} closed, {{ .Stats.WrittenBytes }} bytes written in {{ .Stats.ConnectionDuration }} ({{ .Stats.AverageSpeed }})"

assh now manages the ~/.ssh/config file, take care to keep a backup your ~/.ssh/config file.
~/.ssh/assh.yml is a YAML file containing:
hosts dictionary containing multiple HOST definitionsdefaults section containing global flagsincludes section containing path to other configuration fileshosts:
homer:
# ssh homer -> ssh 1.2.3.4 -p 2222 -u robert
Hostname: 1.2.3.4
User: robert
Port: 2222
bart:
# ssh bart -> ssh 5.6.7.8 -u bart <- direct access
# or ssh 5.6.7.8/homer -u bart <- using homer as a gateway
Hostname: 5.6.7.8
User: bart
Gateways:
- direct # tries a direct access first
- homer # fallback on homer gateway
maggie:
# ssh maggie -> ssh 5.6.7.8 -u maggie <- direct access
# or ssh 5.6.7.8/homer -u maggie <- using homer as a gateway
User: maggie
Inherits: bart # inherits rules from "bart"
bart-access:
# ssh bart-access -> ssh home.simpson.springfield.us -u bart
Inherits:
- bart-template
- simpson-template
lisa-access:
# ssh lisa-access -> ssh home.simpson.springfield.us -u lisa
Inherits:
- lisa-template
- simpson-template
marvin:
# ssh marvin -> ssh marvin -p 23
# ssh sad-robot -> ssh sad-robot -p 23
# ssh bighead -> ssh bighead -p 23
# aliases inherit everything from marvin, except hostname
Port: 23
Aliases:
- sad-robot
- bighead
dolphin:
# ssh dolphin -> ssh dolphin -p 24
# ssh ecco -> ssh dolphin -p 24
# same as above, but with fixed hostname
Port: 24
Hostname: dolphin
Aliases: ecco
RateLimit: 10M # 10Mbytes/second rate limiting
schooltemplate:
User: student
IdentityFile: ~/.ssh/school-rsa
ForwardX11: yes
schoolgw:
# ssh school -> ssh gw.school.com -l student -o ForwardX11=no -i ~/.ssh/school-rsa
Hostname: gw.school.com
ForwardX11: no
Inherits: schooltemplate
"expanded-host[0-7]*":
# ssh somehost2042 -> ssh somehost2042.some.zone
Hostname: "%h.some.zone"
vm-*.school.com:
# ssh vm-42.school.com -> ssh vm-42.school.com/gw.school.com -l student -o ForwardX11=yes -i ~/.ssh/school-rsa
Gateways: schoolgw
Inherits: schooltemplate
# do not automatically create `ControlPath` -> may result in error
ControlMasterMkdir: true
"*.shortcut1":
ResolveCommand: /bin/sh -c "echo %h | sed s/.shortcut1/.my-long-domain-name.com/"
"*.shortcut2":
ResolveCommand: /bin/sh -c "echo $(echo %h | sed s/.shortcut2//).my-other-long-domain-name.com"
"*.scw":
# ssh toto.scw -> 1. dynamically resolves the IP address
# 2. ssh {resolved ip address} -u root -p 22 -o UserKnownHostsFile=null -o StrictHostKeyChecking=no
# requires github.com/scaleway/scaleway-cli
ResolveCommand: /bin/sh -c "scw inspect -f {{.PublicAddress.IP}} server:$(echo %h | sed s/.scw//)"
User: root
Port: 22
UserKnownHostsFile: /dev/null
StrictHostKeyChecking: no
my-env-host:
User: user-$USER
Hostname: ${HOSTNAME}${HOSTNAME_SUFFIX}
templates:
# Templates are similar to Hosts; you can inherit from them
# but you cannot ssh to a template
bart-template:
User: bart
lisa-template:
User: lisa
simpson-template:
Host: home.simpson.springfield.us
defaults:
# Defaults are applied to each hosts
ControlMaster: auto
ControlPath: ~/tmp/.ssh/cm/%h-%p-%r.sock
ControlPersist: yes
Port: 22
User: bob
Hooks:
# Automatically backup ~/.ssh/config
BeforeConfigWrite:
- 'exec set -x; cp {{.SSHConfigPath}} {{.SSHConfigPath}}.bkp'
AfterConfigWrite:
# Concat another `ssh_config` file with the one just generated by `assh`
- 'exec cat ~/.ssh/my-heroku-generated-config >> {{.SSHConfigPath}}'
# Alert me with a Desktop notification
- notify "{{.SSHConfigPath}} has been rewritten"
OnConnect:
# Log internal information to a file
- exec printf '{{.}}' | jq . >> ~/.ssh/last_connected_host.txt
# Alert me with a Desktop notification
- notify New SSH connection to {{.Host.Prototype}} at {{.Stats.ConnectedAt}}
# Write the host prototype to the terminal stderr
- write New SSH connection to {{.Host.Prototype}}
OnDisconnect:
# write on terminal and in a Desktop notification some statistics about the finished connection
- "write SSH connection to {{.Host.HostName}} closed, {{.Stats.WrittenBytes }} bytes written in {{.Stats.ConnectionDuration}} ({{.Stats.AverageSpeed}}bps)"
- "notify SSH connection to {{.Host.HostName}} closed, {{.Stats.WrittenBytes }} bytes written in {{.Stats.ConnectionDuration}} ({{.Stats.AverageSpeed}}bps)"
includes:
- ~/.ssh/assh.d/*.yml
- /etc/assh.yml
- $ENV_VAR/blah-blah-*/*.yml
ASSHBinaryPath: ~/bin/assh # optionally set the path of assh
For further inspiration, these assh.yml files on public GitHub projects can educate you on how people are using assh
assh usage
NAME:
assh - advanced ssh config
USAGE:
assh [global options] command [command options] [arguments...]
VERSION:
2.8.0 (HEAD)
AUTHOR(S):
Manfred Touron <https://github.com/moul/assh>
COMMANDS:
ping Send packets to the SSH server and display statistics
info Display system-wide information
config Manage ssh and assh configuration
sockets Manage control sockets
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--config value, -c value Location of config file (default: "~/.ssh/assh.yml") [$ASSH_CONFIG]
--debug, -D Enable debug mode [$ASSH_DEBUG]
--verbose, -V Enable verbose mode
--help, -h show help
--version, -v print the version
assh config buildRewrites and replaces the existing ~/.ssh/config file.
This action is automatically done by assh when detecting configuration changes. Running this command is useful to set up assh or repair the configuration file.
$ assh config build > ~/.ssh/config
assh config listList hosts and options.
$ assh config list
Listing entries
*.scw -> root@[hostname_not_specified]:22
StrictHostKeyChecking=no [custom options]
UserKnownHostsFile=/dev/null [custom options]
*.shortcut1 -> bob@[hostname_not_specified]:22
*.shortcut2 -> bob@[hostname_not_specified]:22
bart -> bart@5.6.7.8:22
bart-access -> bob@[hostname_not_specified]:22
dolphin -> bob@dolphin:24
expanded-host[0-7]* -> bob@%h.some.zone:22
homer -> robert@1.2.3.4:2222
lisa-access -> bob@[hostname_not_specified]:22
maggie -> maggie@[hostname_not_specified]:22
marvin -> bob@[hostname_not_specified]:23
my-env-host -> user-moul@[hostname_not_specified]:22
schoolgw -> bob@gw.school.com:22
ForwardX11=no [custom options]
schooltemplate -> student@[hostname_not_specified]:22
ForwardX11=yes [custom options]
IdentityFile=~/.ssh/school-rsa [custom options]
vm-*.school.com -> bob@[hostname_not_specified]:22
(*) General options:
ControlMaster: auto
ControlPath: ~/tmp/.ssh/cm/%h-%p-%r.sock
ControlPersist: yes
Port: 22
User: bob
assh config graphvizGenerate a graphviz graph of the hosts
$ assh config graphviz | dot -Tpng > assh-hosts.png

assh config search <keyword>Search for <keyword> in hosts and host options.
$ assh config search bart
Listing results for bart:
bart -> bart@5.6.7.8:22
bart-access -> moul@[hostname_not_specified]:22
assh infoDisplay system-wide information.
$ assh info
Debug mode (client): false
CLI Path: /path/to/assh
Go version: go1.6.2
OS/Arch: darwin/amd64
RC files:
- ~/.ssh/assh.yml
- ~/.ssh/assh.d/hosts.yml
- ~/.ssh/assh.d/moul.yml
- ~/.ssh/assh.d/test.yml
Statistics:
- 299 hosts
- 2 templates
- 4 included files
assh sockets listList active control sockets.
$ assh sockets list
4 active control sockets in "~/.ssh/cm/":
- bart/homer/lisa-22-root.sock (14 minutes)
- bart/homer-22-root.sock (14 minutes)
- bart-22-root.sock (14 minutes)
- marge-22-bart.sock (1 hour)
assh sockets flushClose active control sockets.
$ assh sockets flush
Closed 4 control sockets.
assh sockets masterCreate a master control sockets.
$ assh sockets master
assh pingSend packets to the SSH server and display stats.
$ assh ping -c 4 localhost
PING localhost (127.0.0.1) PORT 22 (ssh) PROTO tcp
Connected to 127.0.0.1: seq=0 time=321µs protocol=tcp port=22
Connected to 127.0.0.1: seq=1 time=501µs protocol=tcp port=22
Connected to 127.0.0.1: seq=2 time=550µs protocol=tcp port=22
Connected to 127.0.0.1: seq=3 time=641µs protocol=tcp port=22
--- localhost assh ping statistics ---
4 packets transmitted, 4 packets received, 0.00% packet loss
round-trip min/avg/max = 321µs/503.25µs/641µs
Get the latest version using GO (recommended way):
go install moul.io/assh/v2@latest
note: tested with Go1.7 or above
Get the latest released version using homebrew (Mac OS X):
brew install assh
Build the latest version
brew install assh --HEAD
Get a released version on: https://github.com/moul/assh/releases
Install with asdf-vm:
asdf plugin add assh
asdf install assh latest
asdf global assh latest
To improve experience when using advanced pattern matching, add the following at the end of your .bashrc / .zshrc / config.fish:
alias ssh="assh wrapper ssh --"
This step is not mandatory but highly recommended.
Note: ssh does not understand advanced patterns;
To bypass this limitation, assh maintains a list of known hosts and regenerate the ~/.ssh/config with all those expanded known hosts.
Without the wrapper, the ~/.ssh/config risks to be outdated when connecting to a new host for the first time and you will need to launch the command again.
With the wrapper, ssh will always be called with an updated ~/.ssh/config file.
~/.ssh/config: cp ~/.ssh/config ~/.ssh/config.backup~/.ssh/assh.yml fileassh config build > ~/.ssh/config to validate the syntax of your ~/.ssh/assh.yml file and automatically build your ~/.ssh/config fileExperimental: assh may run in Docker, however you will have limitations:
assh containers does not have any binaries except assh, you can't use ProxyCommand, ResolveCommand...ssh localhost will ssh to Docker hostdocker run -it --rm -v ~/.ssh:/.ssh moul/assh --help
assh in Docker is slower and has more limitations, but it may be useful for testing or if you plan to use a Docker host as a remote Gateway
assh uses the built-in netcat mode of OpenSSH (shipped with OpenSSH 5.4) by default.
If your ssh client doesn't support this feature, you can configure a custom ProxyCommand configuration, i.e.,
hosts:
myserver:
host: 1.2.3.4
gateways: mygateway
# configure a custom proxycommand
proxycommand: /bin/nc %h %p
mygateway:
host: 5.6.7.8
You can configure this rule globally:
defaults:
proxycommand: nc %h %p
Also, be sure to have netcat installed on your system, or use an alternative proxy binary, i.e., socat.
/etc/hosts and/or handle mDNS requestsassh resolves hostnames using the system built-in resolver, depending on the OS, you can enable new features and/or change modules order.
/etc/resolv.conf documentationunix_listener: "/Users/.../.ssh/cm/..." too long for Unix domain socketStarting with OpenSSH v6.7 the socket name can be shortened by configuring %C for the name expansion.
defaults:
ControlPath: ~/tmp/.ssh/cm/%C.sock
%C is a unique identifier based on a hash of the tuple of (local host, remote user, hostname, port).
Each time you call ssh, assh will check if the generated ~/.ssh/config file is outdated.
By default, it will transparently regenerate the configuration file if needed.
You can disable this behavior by generating the configuration file like this:
assh config build --no-automatic-rewrite
In your ansible.cfg under ssh_connection, make sure you have the following, changing the path to your assh:
[ssh_connection]
ansible_ssh_executable = '/usr/local/bin/assh wrapper ssh'
This project exists thanks to all the people who contribute. [Contribute].
Become a financial contributor and help us sustain our community. [Contribute]
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]
© 2009-2026 Manfred Touron - MIT License
(top 30 of 42)
Go
97.4%
Makefile
1.2%