Every sysadmin SSHes into servers daily. Claude Code can do it too, and it remembers context across commands. You tell it “the web server on 10.0.1.5 is down, fix it” and it checks the service status, reads the error log, identifies the config problem, patches it, re-validates, and restarts. No scripting. No copying error messages back and forth.
This is the practical companion to the Claude Code for DevOps Engineers pillar guide. Everything below was tested live on a Rocky Linux 10.1 OpenStack VM (10.0.1.50) and two Proxmox hypervisors (Debian 12). The commands, the output, the errors are all real. You can reproduce every demo by pointing Claude Code at your own servers.
Verified working: March 2026 on Rocky Linux 10.1 (kernel 6.12), Debian 12 (Proxmox 8.x), Nginx 1.26.3, Certbot via EPEL
What You Need
- Claude Code installed (
curl -fsSL https://claude.ai/install.sh | bash) - SSH key-based access to at least one Linux server (password auth works but key-based is smoother)
- A Claude subscription (Pro, Max, or Team) or Anthropic API key
- Tested on: Rocky Linux 10.1 (RHEL family), Debian 12 (Proxmox). Commands adapt for Ubuntu automatically
Provision a Web Server from Scratch
The first demo shows the complete workflow: install software, configure it, set up SSL, and verify. One conversation, zero manual SSH sessions.
Open Claude Code and type:
SSH into 10.0.1.50 as rocky. Install Nginx, create a custom landing
page showing the hostname and OS version, open firewall ports 80 and 443,
set up Let's Encrypt SSL for sshdemo.computingforgeeks.com, and verify
the site loads over HTTPS.
Claude Code connects and runs the install:
ssh [email protected] "sudo dnf install -y nginx"
Nginx 1.26.3 installs from the Rocky Linux 10 base repository:
Installed:
nginx-2:1.26.3-1.el10.x86_64 nginx-core-2:1.26.3-1.el10.x86_64
nginx-filesystem-2:1.26.3-1.el10.noarch rocky-logos-httpd-100.4-7.el10.noarch
Complete!
It creates the custom landing page, starts the service, and verifies:
ssh [email protected] "sudo systemctl enable --now nginx"
ssh [email protected] "systemctl status nginx --no-pager"
The service comes up clean:
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
Active: active (running) since Fri 2026-03-27 23:08:59 UTC; 32ms ago
Process: 4772 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 4774 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
Main PID: 4775 (nginx)
Tasks: 2 (limit: 10852)
Memory: 2.3M (peak: 2.8M)
CPU: 40ms
CGroup: /system.slice/nginx.service
├─4775 "nginx: master process /usr/sbin/nginx"
└─4776 "nginx: worker process"
For SSL, Claude Code notices certbot isn’t in the default repos, installs EPEL first, then runs certbot:
ssh [email protected] "sudo dnf install -y epel-release"
ssh [email protected] "sudo dnf install -y certbot python3-certbot-nginx"
ssh [email protected] "sudo certbot certonly --standalone -d sshdemo.computingforgeeks.com --non-interactive --agree-tos -m [email protected]"
Certificate obtained successfully:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/sshdemo.computingforgeeks.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/sshdemo.computingforgeeks.com/privkey.pem
This certificate expires on 2026-06-25.
Claude Code then writes the Nginx SSL server block, reloads the config, and confirms HTTPS serves correctly. The final verification:
curl -sI https://sshdemo.computingforgeeks.com/ | head -5
Returns HTTP/2 200 with valid SSL:
HTTP/2 200
server: nginx/1.26.3
date: Fri, 27 Mar 2026 23:10:59 GMT
content-type: text/html
content-length: 273
The HTTPS site is live and serving over HTTP/2 with a valid Let’s Encrypt certificate:

From zero to a fully provisioned HTTPS web server in a single Claude Code conversation. No Ansible, no Terraform, no shell script. Just a natural language description of what you need. For a deep reference on setting up Let’s Encrypt certificates on Linux, that guide covers the manual steps this demo automated.
Diagnose and Fix a Broken Service
This demo is worth bookmarking. We break Nginx on purpose, then tell Claude Code the server is down and let it figure out the rest. No hints, no “check the config file.” Just “it’s broken, fix it.”
First, introduce a real config error by removing a semicolon from the events block:
ssh [email protected] "sudo sed -i 's/worker_connections 1024;/worker_connections 1024/' /etc/nginx/nginx.conf"
ssh [email protected] "sudo systemctl restart nginx"
Nginx fails:
Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
Now tell Claude Code to diagnose it:
The web server on 10.0.1.50 is down. SSH in as rocky, figure out
what's wrong, fix it, and verify it's back up.
Claude Code runs through its own diagnostic sequence. No instructions needed.
Step 1: Check service status
ssh [email protected] "systemctl status nginx --no-pager"
The status reveals the failure with the exact error line:
× nginx.service - The nginx HTTP and reverse proxy server
Active: failed (Result: exit-code) since Fri 2026-03-27 23:11:46 UTC; 8s ago
Process: 5618 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=1/FAILURE)
Mar 27 23:11:46 test-rocky-10 nginx[5618]: nginx: [emerg] unexpected "}" in /etc/nginx/nginx.conf:15
Mar 27 23:11:46 test-rocky-10 nginx[5618]: nginx: configuration file /etc/nginx/nginx.conf test failed
Mar 27 23:11:46 test-rocky-10 systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
Mar 27 23:11:46 test-rocky-10 systemd[1]: Failed to start nginx.service
Step 2: Validate config for the exact error
ssh [email protected] "sudo nginx -t"
Confirms the problem:
nginx: [emerg] unexpected "}" in /etc/nginx/nginx.conf:15
nginx: configuration file /etc/nginx/nginx.conf test failed
Step 3: Inspect the broken section
Claude Code reads lines around the error:
ssh [email protected] "sed -n '10,20p' /etc/nginx/nginx.conf"
The output reveals the missing semicolon on the worker_connections directive:
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
Line 14 has worker_connections 1024 without the required trailing semicolon. The closing brace on line 15 becomes “unexpected” because Nginx is still looking for the semicolon to terminate the directive.
Step 4: Fix, re-validate, restart
ssh [email protected] "sudo sed -i 's/worker_connections 1024$/worker_connections 1024;/' /etc/nginx/nginx.conf"
Re-validate passes:
ssh [email protected] "sudo nginx -t"
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Restart and confirm:
ssh [email protected] "sudo systemctl restart nginx && systemctl is-active nginx"
active
HTTPS verification confirms the site is back:
HTTP/2 200
server: nginx/1.26.3
The entire sequence, from “it’s broken” to “it’s fixed and verified,” took about 30 seconds. Claude Code followed the same diagnostic path an experienced sysadmin would: check status, validate config, read the error, inspect the file, fix it, re-validate, restart. The difference is speed. For a full reference on managing services, see the systemctl commands guide.
Multi-Server Health Check
When your monitoring is down (or you don’t have monitoring yet), Claude Code can SSH into every server you own and build a health report. This demo ran against three real servers: a Rocky Linux 10 VM, and two Proxmox hypervisors running Debian 12.
SSH into these 3 servers and give me a health report for each:
- 10.0.1.50 (user: rocky)
- 10.0.1.10 (user: root)
- 10.0.1.11 (user: root)
For each server check: OS version, kernel, uptime, disk usage (flag
anything over 50%), memory, top 5 CPU processes, any failed systemd
services, listening ports, and pending updates.
Claude Code SSHes into each server sequentially and aggregates the results. Here is the real output from three production systems:
Server 1: 10.0.1.50 (Rocky Linux 10.1)
OS: Rocky Linux 10.1 (Red Quartz)
Kernel: 6.12.0-124.8.1.el10_1.x86_64
Uptime: 5 minutes
Disk: /dev/vda4 19G 1.4G 18G 8% / ✓ OK
Memory: 347Mi used / 1.7Gi total (20%)
Swap: 0B used / 0B total
Top CPU:
rocky 12.9% systemd (user session)
root 0.8% systemd (PID 1)
root 0.5% sshd
Failed Services: none
Ports: 22 (SSH), 80 (HTTP), 443 (HTTPS), 9090 (Cockpit/systemd)
Pending Updates: 81 packages (including kernel, curl, glibc security fixes)
Server 2: 10.0.1.10 (Debian 12, Proxmox pve01)
OS: Debian GNU/Linux 12 (bookworm)
Kernel: 6.8.12-13-pve
Uptime: 1 week, 4 days, 19 hours
Disk: /dev/mapper/pve-root 70G 39G 32G 56% / ⚠ ABOVE 50%
Memory: 5.6Gi used / 62Gi total (9%)
Swap: 0B used / 8.0Gi total
Top CPU:
root 55.5% systemd
root 3.2% sshd
root 2.5% kvm (virtual machine)
root 1.3% tailscaled
Failed Services:
⚠ zfs-import-scan.service - Import ZFS pools by device scanning (FAILED)
Ports: 22, 25 (SMTP localhost), 85 (Proxmox proxy), 111 (RPC)
Pending Updates: 138 packages
Server 3: 10.0.1.11 (Debian 12, Proxmox pve02)
OS: Debian GNU/Linux 12 (bookworm)
Kernel: 6.8.12-13-pve
Uptime: 1 week, 6 days, 2 hours
Disk: /dev/mapper/pve-root 68G 35G 34G 52% / ⚠ ABOVE 50%
Memory: 13Gi used / 62Gi total (21%)
Swap: 0B used / 8.0Gi total
Top CPU:
root 25.7% kvm (VM workload)
root 23.9% kvm (VM workload)
root 23.9% kvm (VM workload)
root 0.9% kvm (VM workload)
Failed Services: none
Ports: 22, 25 (localhost), 85, 111, 3128 (Squid proxy), 8006 (Proxmox UI)
Pending Updates: 132 packages
What Claude Code flagged
Three findings that matter:
- pve01 has a failed ZFS service (
zfs-import-scan.service). This could mean a ZFS pool failed to import on boot, which risks data loss if not investigated - Both Proxmox hosts have 130+ pending updates. Security patches (kernel, glibc, curl) are in the queue. These should be applied during a maintenance window
- Both Proxmox root partitions are above 50%. Not critical yet, but worth monitoring. Old kernel packages and log files are the usual culprits
This is the value of conversational infrastructure checks. A raw df -h tells you disk is at 56%. Claude Code connects that to the pending updates and suggests cleaning old kernels to free space. You can follow up: “clean old kernels on pve01 and apply the security updates” and Claude Code handles both.
Automate Routine Maintenance
After the health check, the natural follow-up is fixing what it found. Here are real maintenance tasks Claude Code ran on the Rocky VM.
Check SSL certificate expiry
Check when the SSL certificate on 10.0.1.50 expires and whether
auto-renewal is configured.
Claude Code checks the certificate and certbot config:
ssh [email protected] "sudo certbot certificates"
The output shows the full certificate status:
Found the following certs:
Certificate Name: sshdemo.computingforgeeks.com
Serial Number: 598c82bccd6f11aa439a5823b430c3591b8
Key Type: ECDSA
Domains: sshdemo.computingforgeeks.com
Expiry Date: 2026-06-25 22:12:05+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/sshdemo.computingforgeeks.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/sshdemo.computingforgeeks.com/privkey.pem
89 days until expiry. Certbot installs a systemd timer for auto-renewal by default, so no manual intervention needed unless the timer is disabled.
Audit security updates
Show me the security updates available on 10.0.1.50. Don't install
them yet, just list what's pending.
Claude Code runs the security check:
ssh [email protected] "sudo dnf check-update --security"
The output shows real security patches waiting:
binutils.x86_64 2.41-58.el10_1.2 baseos
curl.x86_64 8.12.1-2.el10_1.2 baseos
glibc.x86_64 2.39-58.el10_1.7 baseos
gnutls.x86_64 3.8.10-3.el10_1 baseos
kernel-core.x86_64 6.12.0-124.40.1.el10_1 baseos
libcurl.x86_64 8.12.1-2.el10_1.2 baseos
openssl-libs.x86_64 3.2.4-1.el10_1 baseos
systemd.x86_64 257.4-3.el10_1.2 baseos
Kernel, glibc, curl, openssl, and systemd all have security patches pending. In a real scenario, you’d tell Claude Code “apply the security updates and schedule a reboot for tonight” and it would run dnf update --security -y followed by scheduling a reboot.
Audit open ports
Audit the open ports on 10.0.1.50. Identify what process owns each
port and flag anything unexpected.
Claude Code runs the port audit:
ssh [email protected] "sudo ss -tlnp"
Port ownership identified:
State Local Address:Port Process
LISTEN 0.0.0.0:22 sshd
LISTEN 0.0.0.0:80 nginx (master)
LISTEN 0.0.0.0:443 nginx (master)
LISTEN *:9090 systemd (PID 1)
Claude Code identifies port 9090 as systemd (PID 1), which is the Cockpit web console socket. On Rocky Linux 10, Cockpit is enabled by default. If you’re not using it, Claude Code can disable it: sudo systemctl disable --now cockpit.socket. Knowing what’s listening on your servers is the first step of any hardening exercise. The firewalld configuration guide covers locking down ports on RHEL systems.
When to Use Claude Code vs Ansible
After running these demos, the natural question is: why not just write an Ansible playbook?
| Scenario | Claude Code | Ansible |
|---|---|---|
| Ad-hoc health check on 3 servers | Best choice (30 seconds, no setup) | Overkill (inventory, playbook, run) |
| Debug a broken service at 2 AM | Best choice (conversational, adaptive) | Can’t adapt to unknown problems |
| One-time server provisioning | Good (fast for unique setups) | Better if you’ll repeat it |
| Deploy same config to 50 servers | Wrong tool (no parallelism at scale) | Best choice (designed for fleets) |
| Repeatable, auditable deployments | Wrong tool (no playbook to review) | Best choice (version-controlled YAML) |
| Convert a shell script to IaC | Best choice (generates the Ansible role) | The output, not the tool |
Claude Code is best for tasks you do once or twice: investigating, prototyping, debugging. Ansible is best for tasks you do repeatedly across many servers. The sweet spot: use Claude Code to generate and test the Ansible playbook, then run the playbook directly going forward.
Tips for SSH Workflows
After running dozens of SSH sessions through Claude Code, a few patterns emerged that save time.
Be specific about the SSH user. Claude Code defaults to your local username. If your servers use rocky, ubuntu, or root, include it in the prompt: “SSH into 10.0.1.5 as rocky.” This avoids permission errors and retries.
Give context about the server’s role. “Check the database server at 10.0.1.5” tells Claude Code to look for PostgreSQL/MySQL processes, check tablespace usage, and verify replication. “Check the web server” tells it to look for Nginx/Apache, check SSL certs, and test HTTP responses. The role hint focuses the investigation.
Ask for a report, not individual commands. “Give me a health report” produces structured output. “Run df -h” produces raw output. The report format is more useful because Claude Code adds analysis: flagging high usage, identifying unknown services, comparing against best practices.
Use follow-up prompts. After a health check, ask “fix the issues you found” or “apply the security updates.” Claude Code remembers the full context from earlier in the conversation, including which servers had problems and what the errors were. No need to repeat details.
For the complete list of SSH options and patterns, the SSH commands cheat sheet is a useful companion reference.
What Claude Code Cannot Do Over SSH
A few hard limitations to know before you rely on it:
- No interactive sessions. Claude Code runs individual commands via
ssh user@host "command". It cannot open vi, use top interactively, or respond to password prompts mid-session. Every command must complete and return output non-interactively - No persistent state between commands. Each SSH invocation starts a new session. Environment variables set in one command are gone in the next. Claude Code works around this by chaining commands with
&&or writing to files, but it’s less elegant than a real terminal session - No port forwarding or tunnels. Claude Code cannot set up SSH tunnels for database access or forward ports from remote servers. Use manual SSH for that
- Large output gets truncated. If a command produces thousands of lines (full log dumps, package listings), Claude Code may not see all of it. Use
tail -n 50orgrepto filter before piping to Claude Code
These limitations are architectural, not bugs. Claude Code trades interactive flexibility for contextual intelligence: it understands what the output means and acts on it, which a regular SSH session cannot do.
Part of the Claude Code for DevOps Series
This is the SSH spoke in a series of hands-on guides. Each article covers a different infrastructure tool with the same approach: real prompts, real servers, real output.
- Set Up Claude Code for DevOps Engineers (pillar guide with setup, safety rules, permissions)
- Claude Code + Terraform: generate modules, deploy real infrastructure, import resources
- Claude Code + Ansible: generate playbooks, convert scripts to roles, multi-OS testing
- Claude Code + Docker: multi-stage builds, Compose stacks, container debugging
- Claude Code + Kubernetes: manifests, Helm charts, pod log analysis
- Claude Code + GitHub Actions: automated PR review, infrastructure validation
The Claude Code cheat sheet covers every command and shortcut if you want a quick reference while working through these demos.