
SSH for Developers: Keys, Servers, and AI Agents
Set up SSH step by step: server connections, GitHub deploy keys, and protecting your keys when using AI agents like Claude Code or Cursor.
SSH authenticates you without passwords using asymmetric cryptography. Instead of sending a secret over the network, you prove you have the private key without ever revealing it.
This tutorial covers three scenarios you will run into sooner or later:
- Connecting from your PC to a server
- Your server cloning private repos from GitHub
- Using SSH with AI agents (Claude Code, Cursor, Copilot Workspace...)
Once you understand these three, you understand SSH.
How it works: key pairs
You always work with a pair:
| Key | Where it lives | What it does |
|---|---|---|
| Private | Never leaves where it was generated | Signs messages to prove identity |
| Public | Gets copied to wherever you want access | Verifies the signature is valid |
Think of it this way: the private key is your ID card. The public key is a photocopy you leave at the front desk of places you want to get into.
Case 1: Connecting your PC to a server
The most common scenario. You want to run ssh user@server without typing a password every time.
Step 1: Generate the pair on your PC
ssh-keygen -t ed25519 -C "your-email@example.com"
This creates two files in ~/.ssh/:
~/.ssh/
├── id_ed25519 # Private key — never share this
└── id_ed25519.pub # Public key — this one gets copied
Step 2: Copy the public key to the server
# Automatic option (recommended)
ssh-copy-id user@server-ip
# Manual option
cat ~/.ssh/id_ed25519.pub | ssh user@server-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Step 3: Connect
ssh user@server-ip
No password. SSH signs with your private key, the server verifies with the public key it has stored.
What's on the server
~/.ssh/
└── authorized_keys # One line per authorized PC/person
The content is straightforward:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... your-email@example.com
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... other-dev@company.com
Want to grant someone access? Add their .pub as a new line. Revoke? Delete the line.
Flow diagram
YOUR PC SERVER
─────── ──────
~/.ssh/id_ed25519 ──signs──►
~/.ssh/id_ed25519.pub ~/.ssh/authorized_keys
◄─verifies─
Case 2: Your server needs access to GitHub
The server needs to clone private repos or run git pull in automated deployments.
Step 1: Generate the pair on the server
ssh-keygen -t ed25519 -C "deploy@my-server"
Step 2: Add the public key to GitHub
cat ~/.ssh/id_ed25519.pub
Copy the output and go to:
- A specific repo: Settings > Deploy Keys > Add deploy key
- All your repos: Profile > Settings > SSH Keys
Step 3: Test the connection
ssh -T git@github.com
If it works:
Hi username! You've successfully authenticated...
Step 4: Clone or pull
git clone git@github.com:username/private-repo.git
No credentials needed.
Diagram
SERVER GITHUB
────── ──────
~/.ssh/id_ed25519 ──signs──►
~/.ssh/id_ed25519.pub Deploy Key
◄─verifies─
Case 3: SSH with AI agents
This is where a lot of developers make mistakes without realizing it.
Tools like Claude Code, Cursor, Aider, or Copilot Workspace execute commands on your behalf. When you say "deploy this to production" or "clone the repo and fix the bug," the agent needs SSH access.
The problem
Your SSH private key is the most sensitive thing you have. If an agent has access to ~/.ssh/id_ed25519, it has access to everything that key opens: production servers, private repos, critical infrastructure.
It's not that the agent is malicious. It's that:
- Context errors: you ask it to "clean up temp files" and it deletes
~/.ssh/because it misunderstood - Prompt injection: malicious code in a repo it clones injects instructions
- Logs and telemetry: some agents log executed commands, including keys if you pass them inline
- Excessive scope: you give it production access for a task that only needed staging
Rules for using SSH with agents
1. Never expose your main key
Create agent-specific keys with minimal permissions:
# Key only for the agent, only for staging
ssh-keygen -t ed25519 -f ~/.ssh/id_agent_staging -C "agent-staging-only"
2. Use read-only deploy keys
On GitHub, don't check "Allow write access" unless the agent actually needs to push.
3. Limit scope in ~/.ssh/config
# The agent can only use this key for staging
Host staging-for-agent
HostName staging.mycompany.com
User deployer
IdentityFile ~/.ssh/id_agent_staging
# Production doesn't even appear in its config
4. Use passphrase + ssh-agent with timeout
# Key protected with passphrase
ssh-keygen -t ed25519 -f ~/.ssh/id_agent -C "agent"
# (it will ask for a passphrase)
# Add it to the SSH agent with 1 hour expiration
ssh-add -t 3600 ~/.ssh/id_agent
The AI agent can use the key while it's in memory, but cannot extract it or use it after the timeout.
5. Audit what commands it runs
Before granting SSH access to an agent, understand what it will do. If the task is "format this code," it doesn't need SSH. If it's "deploy to production," think twice.
6. Separate environments radically
~/.ssh/
├── id_personal # Your use, never for agents
├── id_work # Manual use at work
├── id_agent_staging # Agents only, staging only
└── id_agent_ci # CI/CD automation only
7. Consider a vault for sensitive keys
Instead of leaving keys on disk, use a secrets manager that requires explicit authentication for each use. The agent can't use what it doesn't have.
What NOT to do
- Give the agent your main
~/.ssh/id_ed25519 - Run SSH commands the agent suggests without reviewing them
- Assume "it's just development" when the key also opens production
- Put private keys in environment variables the agent can read
- Trust that the agent "knows" which keys are sensitive
Secure setup example for Claude Code
# 1. Create a specific key
ssh-keygen -t ed25519 -f ~/.ssh/id_claude_code -C "claude-code-dev"
# 2. Restrictive config
cat >> ~/.ssh/config << 'EOF'
Host dev-server
HostName 192.168.1.50
User developer
IdentityFile ~/.ssh/id_claude_code
EOF
# 3. Only this key in authorized_keys on the dev server
# (not on production)
# 4. On GitHub: read-only deploy key on non-critical repos
Now Claude Code can work on your dev server, but has no access to production and can't push to critical repos.
Anatomy of the ~/.ssh/ directory
| File | What it is | Fixed name? |
|---|---|---|
id_ed25519 | Your private key | No, it's the default |
id_ed25519.pub | Your public key | No, it's the default |
authorized_keys | Public keys allowed to connect | Yes |
known_hosts | Fingerprints of known servers | Yes |
config | Host configuration and aliases | Yes |
The known_hosts file
The first time you connect to a server, SSH asks:
The authenticity of host '192.168.1.100' can't be established.
ED25519 key fingerprint is SHA256:xXxXxXx...
Are you sure you want to continue connecting (yes/no)?
If you say yes, it saves the fingerprint. Next time it won't ask.
If the server changes (you reinstalled it, IP was reassigned), SSH warns you:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
Fix:
ssh-keygen -R 192.168.1.100
Multiple keys with ~/.ssh/config
If you manage several servers and services:
# Production
Host prod
HostName 162.55.62.72
User deployer
IdentityFile ~/.ssh/id_production
# Development
Host dev
HostName 192.168.1.50
User dev
IdentityFile ~/.ssh/id_development
# Personal GitHub
Host github.com
IdentityFile ~/.ssh/id_github_personal
# Work GitHub (different account)
Host github-work
HostName github.com
IdentityFile ~/.ssh/id_github_work
Now:
ssh prod # instead of ssh deployer@162.55.62.72
git clone git@github-work:company/repo.git
Complete diagram
YOUR PC SERVER GITHUB
─────── ────── ──────
~/.ssh/ ~/.ssh/
├── id_ed25519 ───────────────► authorized_keys
├── id_ed25519.pub ├── id_ed25519 ─────────────────► Deploy Key
├── id_agent_staging ─────────► authorized_keys (staging only)
├── known_hosts ├── id_ed25519.pub
└── config └── known_hosts
Flow 1: PC > Server
Signs with local private key, verifies with public key in authorized_keys
Flow 2: Server > GitHub
Signs with server's private key, verifies with public key in Deploy Keys
Flow 3: AI Agent > Dev server
Uses specific key with limited scope
Command reference
# Generate key pair
ssh-keygen -t ed25519 -C "comment"
# Generate with specific name
ssh-keygen -t ed25519 -f ~/.ssh/my_key -C "comment"
# Copy public key to server
ssh-copy-id user@server
# View your public key
cat ~/.ssh/id_ed25519.pub
# Test GitHub connection
ssh -T git@github.com
# Remove old server fingerprint
ssh-keygen -R server-ip
# Connect with specific key
ssh -i ~/.ssh/my_key user@server
# View key fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub
# Add key to SSH agent with timeout (1 hour)
ssh-add -t 3600 ~/.ssh/my_key
# List keys in SSH agent
ssh-add -l
# Remove all keys from SSH agent
ssh-add -D
Common errors
"Permission denied (publickey)"
- Your public key is not in
authorized_keyson the server - You're using the wrong private key
- Fix: check with
ssh -v user@serverto see which key it tries
"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED"
- The server changed (reinstalled, IP reassigned to a different machine)
- Fix:
ssh-keygen -R server-ip
"Permissions are too open"
- The private key has 644 permissions or similar
- Fix:
chmod 600 ~/.ssh/id_ed25519
"Could not resolve hostname"
- The alias in
~/.ssh/configis misspelled - You don't have an entry for that host
"Agent admitted failure to sign"
- The SSH agent doesn't have the key loaded
- Fix:
ssh-add ~/.ssh/your_key
SSH security checklist
- Each context (personal, work, CI, AI agents) has its own key
- Private keys have 600 permissions
- The
~/.sshdirectory has 700 permissions - Sensitive keys have a passphrase
- AI agents only access keys with limited scope
- GitHub deploy keys are read-only unless you need push
- You have backups of infrastructure keys in a vault
- You rotate keys when someone leaves the team or a machine gets compromised
- No private keys in repos, logs, or exposed environment variables
Conclusion
SSH isn't complicated once you understand that everything revolves around one concept: the private key signs, the public key verifies.
What does get complicated is managing multiple keys across multiple contexts, especially now that AI agents are part of the workflow. The golden rule: least privilege. Each key should only open the doors it needs to open, and automated agents should have the most restrictive keys of all.
Further reading
- Why .env files are dangerous with AI agents if you want to understand what happens when your secrets sit on disk
- AI agent security: a practical guide covers the full threat model for tools like Claude Code and Cursor
- 5 secret mistakes on your server for the most common issues with JWT secrets, database credentials, and rotation
- Managing secrets with Claude Code for setting up a vault that works with AI agents
- How to prevent secrets from ending up in git if you don't have pre-commit hooks set up yet
- Asymmetric cryptography: the idea that changed the internet for the theory behind how SSH keys actually work
- Try SecureCode free. Zero-knowledge secrets for developers using AI agents.