AI

Manage Servers with Claude Code via SSH

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.

Original content from computingforgeeks.com - post 164926

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:

Nginx web page showing Provisioned by Claude Code via SSH on Rocky Linux 10.1 served over HTTPS

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?

ScenarioClaude CodeAnsible
Ad-hoc health check on 3 serversBest choice (30 seconds, no setup)Overkill (inventory, playbook, run)
Debug a broken service at 2 AMBest choice (conversational, adaptive)Can’t adapt to unknown problems
One-time server provisioningGood (fast for unique setups)Better if you’ll repeat it
Deploy same config to 50 serversWrong tool (no parallelism at scale)Best choice (designed for fleets)
Repeatable, auditable deploymentsWrong tool (no playbook to review)Best choice (version-controlled YAML)
Convert a shell script to IaCBest 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 50 or grep to 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.

The Claude Code cheat sheet covers every command and shortcut if you want a quick reference while working through these demos.

Related Articles

AI How To Install LibreOffice 25.x on CentOS 9 | RHEL 9 AI Can AI Predict and Prevent Criminal Activity? AI OpenAI Codex CLI Cheat Sheet – Commands, Shortcuts, Tips AI Set Up Claude Code for DevOps Engineers

Leave a Comment

Press ESC to close