Basics

Writing a ~/.ssh/config that actually saves you time

7 min read

If you type ssh -p 2222 deploy@198.51.100.14 -i ~/.ssh/id_ed25519_prod more than twice a week, you are doing work the SSH client offered to do for you. ~/.ssh/config is a plain text file that gives each server a short name and remembers everything else about it.

It is not an Upshell thing, or a Mac thing. It is standard OpenSSH, it ships with macOS, and every tool that speaks SSH — git, rsync, scp, VS Code Remote, most GUI clients — reads it. Which is why it is the highest-leverage file on a developer’s Mac.

The file, and where it lives

There are two of them on macOS:

  • ~/.ssh/config — yours, and the one you edit.
  • /etc/ssh/ssh_config — system-wide defaults, and you can leave it alone.

If ~/.ssh/config does not exist yet, create it:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/config
chmod 600 ~/.ssh/config

Those permissions matter. SSH refuses to use a config file that other users on the machine can write to, and it is one of the more confusing failures because the error talks about the file, not about the permissions you were thinking about.

A first block

The unit is Host <alias>, followed by an indented list of options:

Host prod
    HostName 198.51.100.14
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_prod

Now ssh prod does the whole thing. So does scp file.tar.gz prod:/tmp/, and sftp prod, and git clone prod:/srv/git/app.git.

Host is the nickname you type. HostName is the real address SSH dials. Keeping them different is the point — when the box gets rebuilt at a new IP, you change one line and every script and bookmark keeps working.

The rule everybody gets wrong: first match wins

This is the one thing to remember, because it is not what most people assume.

SSH reads the file top to bottom and collects options as it goes. For each option, the first value it finds is the one that is used. Later blocks cannot override an earlier one — they can only fill in what has not been set yet.

The practical consequence: wildcards go at the bottom, specific hosts at the top.

Host prod
    HostName 198.51.100.14
    User deploy

Host *.internal.example.com
    User admin
    ProxyJump bastion

Host *
    AddKeysToAgent yes
    UseKeychain yes
    ServerAliveInterval 30

Put that Host * block at the top instead and every host on the machine inherits nothing else — the first User wins and your per-host users are silently ignored. This is the source of an enormous share of “my ssh config isn’t working” questions.

The options actually worth setting

Most of man ssh_config is for edge cases. This is the short list that earns its place on a Mac.

AddKeysToAgent yes — loads the key into ssh-agent the first time you use it, so the passphrase prompt happens once per boot instead of once per connection.

UseKeychain yes — Apple’s addition. Stores the key passphrase in your login Keychain, so you are not asked again after a restart either. It exists only on macOS; on Linux the line is ignored, harmless if you sync dotfiles.

ServerAliveInterval 30 — sends a keepalive every 30 seconds so an idle session does not get culled by a NAT or firewall. If your sessions die when you go to lunch, this is the fix. See SSH keeps disconnecting.

IdentitiesOnly yes — tells SSH to offer only the key you named, instead of every key the agent holds. Worth setting once you have more than about five keys: servers usually cut you off after six failed offers, and you get “Too many authentication failures” while holding a key that would have worked.

ForwardAgent no as a global default, turned on only per host that needs it, and only for hosts you control. Forwarding your agent to a machine someone else roots is how you lose every key you own — the reasoning is in SSH agent forwarding.

Connection reuse, when you connect all day

If you open ten sessions to the same box, multiplexing makes the second through tenth instant — they ride inside the first connection instead of doing a fresh TCP handshake, key exchange and authentication:

Host *
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

ControlPersist 10m keeps the master connection alive for ten minutes after the last session closes. The speed difference on a high-latency link is not subtle.

One caveat worth knowing: with a master connection open, killing it takes every session with it, and some tools get confused by the socket. If something behaves strangely, ssh -O exit prod closes the master cleanly.

Splitting the file with Include

Once the file passes a couple of hundred lines — or once part of it comes from work and part from your own servers — split it:

Include ~/.ssh/config.d/*.conf

Host *
    AddKeysToAgent yes
    UseKeychain yes

Include must come before the blocks it should take precedence over, because of the first-match-wins rule. It is available in OpenSSH 7.3 and later, which covers every macOS anyone is still running.

Checking what SSH actually decided

Stop guessing. This prints the effective configuration for a host, after all the matching and merging:

ssh -G prod

Every option, resolved. When a connection uses the wrong user or the wrong key, ssh -G tells you what SSH concluded and you can usually see the offending block immediately.

For the connection itself, ssh -v prod shows the handshake, and -vv shows which keys are offered in which order.

Using the same config from a GUI client

The catch on a Mac is the App Sandbox: a sandboxed app cannot read ~/.ssh on its own, no matter how much it would like to. Upshell handles this with a one-time import — you point a file picker at ~/.ssh/config once, and hosts, users, ports, IdentityFile references, ProxyJump chains and LocalForward/RemoteForward rules come in as saved hosts with their tunnels and jump relationships intact.

After that the two stay independent: the terminal keeps using the file, the app uses its own store. Nothing is written back to ~/.ssh/config.

Most Mac SSH clients read the file in some form, and how well they do it is one of the things worth comparing before you commit to one: Upshell vs Termius, vs Royal TSX, vs Prompt, vs SecureCRT and vs iTerm2, each with the cases where the other one wins.

Step by step

  1. Create the file with the right permissionsmkdir -p ~/.ssh && chmod 700 ~/.ssh, then touch ~/.ssh/config && chmod 600 ~/.ssh/config.
  2. Add one Host block for your most-used server — alias, HostName, User, and Port and IdentityFile if they are not the defaults.
  3. Test itssh <alias>. If it fails, ssh -v <alias> shows where.
  4. Add a Host * block at the bottom — with AddKeysToAgent yes, UseKeychain yes and ServerAliveInterval 30. Bottom, not top.
  5. Verify what SSH resolvedssh -G <alias> prints the effective options, which is how you catch a wildcard shadowing a specific host.

Questions people ask

Why is my ~/.ssh/config being ignored?

Almost always one of three things: the file is not at exactly ~/.ssh/config, its permissions are too loose (it must not be writable by anyone but you), or a Host * block near the top is winning under the first-match-wins rule. ssh -G <alias> distinguishes them in one command.

Does ~/.ssh/config work with git, rsync and scp?

Yes. They all invoke the system ssh, so a Host alias works everywhere — git clone prod:/srv/repo.git, rsync -a ./dist prod:/var/www/, scp dump.sql prod:/tmp/.

What is the difference between Host and HostName?

Host is the nickname you type on the command line and the pattern SSH matches against. HostName is the real address it connects to. Keeping them separate means a server can change address without breaking anything that refers to it.

Can I have per-host keys?

Yes — IdentityFile inside the Host block. Add IdentitiesOnly yes alongside it, or SSH will still offer every other key in your agent first and may exhaust the server’s retry limit before reaching the right one.

Upshell — the native SSH & SFTP client for macOS

Tabbed and split terminal, saved hosts with keys and 2FA, multi-hop bastions, port forwarding and SFTP on the very same connection. Free to download, no subscription required.