---
title: Claude Code as a Linux sysadmin
---

For a while now, Claude Code has effectively been the sysadmin for my servers. I describe what I want — provision a box, deploy a site, chase down a 502 — and it SSHes in, does the work, and tells me what it changed. The shift is from typing commands *into a server* to typing intent *into a prompt*. After setup, I rarely SSH in by hand.

This is how I have it wired up, running real production boxes (a couple of Linode VPSes behind Caddy) this way.

## How it works

Claude Code runs on my **local machine**, not on the server. When it needs to touch a box, it runs ordinary `ssh` from my terminal:

```bash
ssh isis "systemctl status caddy"
```

That's the whole trick. The SSH key already lives in `~/.ssh` on the machine where Claude Code runs, so I never paste a key anywhere — Claude just shells out to `ssh` and the OS handles auth. Same for provider CLIs: if the Linode CLI is configured locally, Claude can call it.

<Callout type="warning">
  **Keep secrets out of CLAUDE.md.** Don't put private keys, passphrases, or API tokens into `CLAUDE.md` or any file you might commit. CLAUDE.md is for *non-secret* facts — hostnames, paths, what's installed. The SSH key stays in `~/.ssh`; tokens live in environment variables or a git-ignored file Claude reads at run time. Claude needs to *use* a credential, not *see* it.
</Callout>

Persistent context comes from **CLAUDE.md** and Claude Code's automatic memory — not from re-explaining the server every session. I keep a short server doc that loads at the start of every session, so Claude already knows the layout.

## Setup

1. **SSH access.** Confirm you can `ssh user@server` from the machine running Claude Code (your key in `~/.ssh`, the public key in the server's `authorized\_keys`). If it works in a plain terminal, it works for Claude. A `~/.ssh/config` host alias makes everything cleaner — `ssh isis` beats memorizing an IP.
2. **A server doc in CLAUDE.md** with the non-secret facts Claude needs: host alias and SSH user; what's installed (Caddy, Docker, systemd services, runtimes); where things live (web roots, the Caddyfile, log paths, deploy scripts); and how deploys work ("merging to `main` auto-deploys via the GitHub Action"). The richer this is, the fewer wrong turns it takes.
3. **Provider API access (Linode example).** Keep a scoped Linode API token in the environment or a git-ignored file. Claude calls the Linode API/CLI to manage DNS and domains — without the token ever landing in a prompt or a committed file.

This works on any Linux VPS with SSH key auth — Linode, DigitalOcean, Hetzner, a plain EC2 box.

## What it handles

- **Provisioning** — Caddy or Nginx, UFW, fail2ban, unattended upgrades, users and permissions, reverse proxies, Let's Encrypt certs.
- **Deployments** — pull from GitHub, run the build (Rust, Node, …), restart the service; add a new site or domain to the Caddyfile; auto-deploy on merge.
- **Troubleshooting** — paste a 502 or a stack trace and it SSHes in, reads the right logs (app, system, Caddy access/error), forms a hypothesis, and fixes it.
- **Ongoing ops** — system updates, resource checks, config and data backups, service restarts, reading custom audit logs.

## The way I actually talk to it

> Set up example.com to serve my Rust app in `/var/www/myapp` behind Caddy, with HTTPS.

> I'm getting a 502 on mysite.com — here's the error: [paste]. Check the Caddy and app logs and fix it.

> Point the A record for staging.mysite.com at the new server IP.

> Run security updates on prod and tell me if anything looks off.

Short intent in; documented work out.

## Guardrails (and why I leave them on)

Handing an agent SSH to production sounds reckless until you use the permission model. By default Claude Code **asks before it runs a command**, so I see the exact `ssh … "…"` before it executes. In `settings.json` I allowlist the boring read-only stuff so it stops asking (`systemctl status`, `journalctl`, `git status`, `caddy validate`) and deny the things I never want it doing unprompted. It moves fast on diagnostics and waits for a nod on anything that changes state.

<Callout type="info">
  **Verify, don't trust.** After anything important I ask it to summarize what it changed and show me the relevant log lines. "It ran" isn't "it worked" — with services, the breakage shows up at request time, not command time.
</Callout>

## Where it's weaker

This shines on Linux servers you reach over SSH. It's a worse fit where there's no shell to drive — GUI-only appliances, desktop apps — though pasting logs and errors for diagnosis still helps there.

> *TODO (Chris): a real war story here would land — the gnarliest thing you've had Claude fix on a live box.*
