Summary
This RFD documents a simple baseline for reliable GitHub SSH authentication on macOS. It complements the broader macOS workflow guidance in 0003 and the Windows workflow guidance in 0004.
The determination is to:
- use a dedicated GitHub SSH key,
- configure
github.comexplicitly in~/.ssh/config, - enable
AddKeysToAgent, - enable
UseKeychain, - enable
IdentitiesOnly, - add the key to Apple Keychain once with
ssh-add --apple-use-keychain.
This is the preferred baseline because it makes GitHub SSH behavior deterministic across:
- interactive terminals,
- fresh shells,
- editor-integrated workflows,
- agent-driven Git operations,
- harnesses and automation that may not load shell startup files.
Problem
GitHub SSH failures often come from configuration living in the wrong place.
Common symptoms include:
Permission denied (publickey)even though a valid key exists,- Git working in one terminal but not another,
- automation behaving differently from interactive shells,
- the wrong identity being offered first,
- fixes depending on
.zshrc,.zprofile, or similar shell startup files.
These problems become more visible when commands are launched by agents, IDE task runners, or automation harnesses. Those environments may not execute the same shell initialization path as an interactive terminal, so any setup that depends on startup-time ssh-add calls is inherently brittle.
Constraints and goals
The desired setup should:
- work for normal human terminal usage,
- work for non-interactive Git and SSH invocations,
- choose the intended GitHub key explicitly,
- avoid depending on shell-profile side effects,
- keep passphrase handling practical on macOS,
- be easy to inspect and debug later.
Applied configuration
The GitHub SSH key is assumed to live at:
~/.ssh/github_id_ed25519
~/.ssh/github_id_ed25519.pub
If a dedicated key does not already exist, create one:
ssh-keygen -t ed25519 -C "iancleary@hey.com" -f ~/.ssh/github_id_ed25519
~/.ssh/config should contain:
Host github.com
IdentityFile ~/.ssh/github_id_ed25519
AddKeysToAgent yes
UseKeychain yes
IdentitiesOnly yes
The config file should also have restricted permissions:
chmod 600 ~/.ssh/config
Why these settings
IdentityFile ~/.ssh/github_id_ed25519- binds GitHub auth to the intended private key.
AddKeysToAgent yes- allows SSH to register the key with the agent after first use.
UseKeychain yes- stores the passphrase in Apple Keychain on macOS.
IdentitiesOnly yes- prevents SSH from trying unrelated identities first.
IdentitiesOnly yes is especially important on machines with multiple SSH keys. Without it, authentication can fail simply because the client offers the wrong keys before it reaches the correct one.
Why this is better for agents and harnesses
Agent and harness reliability improves when SSH behavior is host-scoped and configuration-driven instead of shell-driven.
Examples of fragile setups include:
- an agent launching
git fetchin a non-interactive shell, - an IDE spawning Git without a login shell,
- a harness inheriting a different agent state than the user’s main terminal,
- a machine carrying multiple identities with no explicit host mapping.
By moving the durable behavior into ~/.ssh/config, GitHub key selection becomes explicit and repeatable. By using Apple Keychain integration, the passphrase is available without requiring every execution environment to bootstrap ssh-add manually.
Enrollment step
After the SSH config is in place, add the key to Apple Keychain once:
ssh-add --apple-use-keychain ~/.ssh/github_id_ed25519
Expected output:
Identity added: /Users/iancleary/.ssh/github_id_ed25519 (iancleary@hey.com)
Verification
Direct GitHub SSH verification:
ssh -T git@github.com
Expected result:
Hi iancleary! You've successfully authenticated, but GitHub does not provide shell access.
That message confirms authentication succeeded.
To inspect the resolved SSH config for GitHub:
ssh -G github.com | grep -E 'identityfile|identitiesonly|addkeystoagent|usekeychain'
Additional high-leverage configuration notes
A few SSH behaviors are worth making explicit because they matter for reliability:
Config precedence and ordering
Per ssh_config(5), SSH reads configuration in this order:
- command-line options,
- user config (
~/.ssh/config), - system config (
/etc/ssh/ssh_config).
It also uses the first value obtained for a given parameter. In practice, that means:
- put host-specific rules near the top,
- put broad defaults later,
- avoid accidentally overriding a specific GitHub stanza with a wildcard stanza.
Use ssh -G to inspect effective config
When debugging agent or harness behavior, do not guess which config is winning.
Use:
ssh -G github.com
This is one of the fastest ways to confirm the resolved identityfile, identitiesonly, and related settings.
Use BatchMode yes for non-interactive harnesses
Per ssh_config(5), BatchMode yes disables interactive prompts such as password and host-key confirmation prompts. That is useful for scripts, agents, and harnesses where waiting for a hidden prompt is worse than failing fast.
A per-host example:
Host github.com
IdentityFile ~/.ssh/github_id_ed25519
AddKeysToAgent yes
IdentitiesOnly yes
BatchMode yes
This is most appropriate for automation contexts, not necessarily for every interactive workflow.
Use SetEnv TERM=xterm-256color only on the host that needs it
If a single SSH target needs a specific terminal type, set it only for that host:
Host example-host
SetEnv TERM=xterm-256color
This is a good pattern because it changes only the connection that actually needs the override. It avoids broad terminal-environment changes that might affect unrelated hosts or local shell behavior.
If you want to set the terminal type for all SSH hosts, use:
Host *
SetEnv TERM=xterm-256color
That broader form is useful when you want the same terminal type everywhere, but the per-host form is still preferable when only one target needs the override.
As with other host-scoped SSH settings, this keeps the configuration explicit, narrow, and easy to inspect later with ssh -G <host>.
Windows
For this workflow, assume Git Bash.
That assumption simplifies the model substantially, because the main Windows reliability problem is split SSH environments. If you test in Git Bash but an agent or harness runs in PowerShell, cmd, or WSL, you may be exercising different binaries, agents, config files, and key stores.
With Git Bash as the standard environment, the same core SSH ideas apply:
- use a dedicated GitHub key,
- use a per-user SSH config,
- load keys into an agent,
- make GitHub host selection explicit,
- verify from Git Bash itself.
High-leverage Windows-specific notes:
- GitHub documents Windows-specific SSH onboarding and notes that some environments may surface an
ssh-addillegal optionmessage when macOS-specific flags are used. - If Git Bash is the chosen shell, treat it as the source of truth for both manual verification and harness behavior whenever possible.
- Reliability improves when the harness uses the same SSH client family and config path that you use interactively.
Practical baseline for Windows with Git Bash:
- keep keys under
%USERPROFILE%\.ssh\, - use
%USERPROFILE%\.ssh\configfor host-specific rules, - verify with
ssh -T git@github.comfrom Git Bash, - avoid mixing validation across Git Bash, PowerShell, and WSL unless you intentionally support all of them.
The biggest Windows footgun is still split environments. If the automation path does not use Git Bash semantics, SSH may look flaky even when the Git Bash setup itself is correct.
Linux
On Linux, the client configuration model is the same as on macOS because it is standard OpenSSH:
~/.ssh/configremains the primary per-user client config,IdentityFile,IdentitiesOnly,AddKeysToAgent, andBatchModework the same way,ssh_config(5)remains the reference for client behavior.
The main distribution-level differences are usually about how the SSH agent is started and persisted.
Ubuntu
Ubuntu’s server documentation emphasizes modular SSH configuration via:
/etc/ssh/sshd_config,/etc/ssh/sshd_config.d/*.conf.
That is primarily server-side guidance, but the broader lesson is useful: keep configuration explicit and inspectable.
For client-side GitHub usage on Ubuntu, the usual baseline is:
- keep host-specific client rules in
~/.ssh/config, - use
ssh-agentor a desktop keyring/session agent, - verify with
ssh -T git@github.com, - prefer config-driven key selection over shell-local workarounds.
Arch
The ArchWiki has especially useful client-side guidance for SSH keys:
AddKeysToAgent yescan be set in~/.ssh/configso clients such as Git store keys in the agent on first use,IdentitiesOnly yesplusIdentityFileis recommended when managing multiple keys,- Arch documents both traditional
ssh-agentusage and asystemd --userapproach usingssh-agent.serviceandSSH_AUTH_SOCK.
That makes Arch a good model for agent-aware setups where you want SSH behavior to persist across terminals without depending on ad hoc shell snippets.
Alpine
Alpine uses the same OpenSSH client semantics, but it is often deployed in more minimal environments. In practice that means:
- fewer desktop keyring conveniences by default,
- more cases where you explicitly start
ssh-agent, - more cases where shell or container lifecycle determines whether agent state persists.
Alpine’s wiki guidance around SSH focuses more on OpenRC and server management than on client convenience. So for GitHub client auth on Alpine, the high-leverage pattern is usually to rely on standard OpenSSH client config in ~/.ssh/config, then decide explicitly how agent state should be initialized in that environment.
What not to rely on
Avoid treating these as the primary solution:
- repeated
ssh-addcommands in.zshrc,.zprofile, or.bashrc, - implicit key selection when multiple identities exist,
- debugging Git before testing
ssh -T git@github.comdirectly, - environment-specific fixes that only work in one terminal flavor.
Those approaches may appear to work temporarily, but they increase variance across tools and make the machine harder to reason about.
Troubleshooting
Permission denied (publickey)
Usually means one of:
- the wrong key is configured,
- the public key is not uploaded to GitHub,
- the private key path is wrong,
IdentitiesOnly yesis missing,- file permissions are too loose.
Useful checks:
ls -l ~/.ssh
ssh -T -v git@github.com
No ~/.ssh/config file exists
That is fine. Create it and restrict permissions:
touch ~/.ssh/config
chmod 600 ~/.ssh/config
GitHub works in one environment but not another
That is usually evidence that the setup depends on shell initialization behavior instead of SSH client configuration. Move the durable logic into ~/.ssh/config and Keychain-backed key enrollment.
Security notes
- Use a dedicated key for GitHub instead of a general-purpose SSH key.
- Protect the private key with a passphrase.
- Prefer Keychain-backed passphrase storage over plaintext workarounds.
- Keep
~/.ssh/configpermissions restricted.
Determination
For macOS GitHub development environments, the default SSH baseline should be:
- explicit
Host github.comconfiguration in~/.ssh/config, - a dedicated GitHub key referenced with
IdentityFile, AddKeysToAgent yes,UseKeychain yes,IdentitiesOnly yes,- one-time enrollment with
ssh-add --apple-use-keychain.
This approach is simpler, more reproducible, and more reliable for both humans and automation than shell-startup-based workarounds.
Future expansion
This draft could later expand to cover:
- multiple GitHub accounts,
- Linux and Windows equivalents,
Include-based SSH config layouts,- interaction with dotfiles and bootstrap scripts,
- additional guidance for CI, remote dev shells, and agent harnesses.