Claude Code skills for poking around Linux servers, plus a reporting tool so the findings don't just bitrot in a terminal.
This is a proof of concept. It grew out of ideas sparked by Anthropic's Preparing your security program for AI-accelerated offense post, mixed with some thinking of our own about what a practical AI-assisted server audit might look like. Treat it as a starting point for your own work, not a finished production system. The checks are opinionated, the severity ratings are debatable, and your environment will have its own quirks. Fork it, gut it, rewrite the bits that don't fit — that's the point.
/server-explore — SSHs into a server and figures out what's actually running on it. Services, containers, cron jobs, filesystems, interesting software. Everything goes into a local SQLite database. It's the "what does this box actually do?" audit.
/security-explore — The paranoid sibling. Patch gap analysis, CISA KEV cross-referencing, EPSS scoring, SSH hardening, firewall checks, container hygiene, credential exposure, logging coverage. Findings get severity ratings and land in the same database. The "should we be worried?" audit.
/fleet-audit — Give it a file of hostnames or just list them however you like, and it runs both skills across every server. Saves markdown and HTML reports, opens the fleet dashboard when it's done. You walk away, come back to a dashboard.
report.py — Turns the database into HTML you can actually show people. Fleet dashboard for management meetings, detailed per-server breakdowns for when someone asks awkward questions about a specific box.
- Claude Code
- SSH access to the servers you want to scan (key-based)
- uv for the report generator
- Nothing to install on the servers themselves.
git clone git@github.com:ohnotnow/claude-server-explorer.git
cd claude-server-explorerThe skills live in .claude/skills/ and Claude Code picks them up automatically when you're in this directory.
Open Claude Code here and run a skill:
/server-explore user@hostname
/security-explore user@hostname
It SSHs in, runs its checks, and writes findings to ~/.server-inventory/inventory.db. At the end it'll ask if you want a local markdown file too.
/fleet-audit servers.txt
/fleet-audit elf, cordelia, muffin
/fleet-audit can we just check elf and muffin?
Runs both scans for every server, saves reports, opens the dashboard. In servers.txt, one hostname per line -- lines starting with # are comments. Or just type the hostnames in whatever format makes sense for your ssh/dns/setup.
Fleet dashboard (all servers):
uv run report.pyOne specific server:
uv run report.py --server hostnameBoth spit out a single HTML file you can open in a browser or email around.
Options:
--server, -s Detailed report for one server
--db Path to database (default: ~/.server-inventory/inventory.db)
-o, --output Output filename (default: auto-generated)
External API lookups (CISA KEV catalogue, EPSS scores) run on your local machine, so they work regardless of the remote server's network access.
Package index refreshes (apt update, dnf check-update) still run on the remote server. If your servers sit behind private subnets their package managers may need a proxy. Most servers already have this configured, but if not, copy the example config and add your proxy URL:
cp .server-explorer.conf.example .server-explorer.conf
# then edit .server-explorer.conf and uncomment/set the proxy lineThe skill will use this proxy for remote package index commands when direct connectivity fails.
SSH into server ──> Run checks ──> SQLite database ──> HTML reports
One SQLite database holds everything. Re-scanning a server updates its records rather than duplicating them, so the reports always show the latest state.
Report templates are Jinja2 (templates/), so you can restyle things without touching Python.
Skills are markdown files in .claude/skills/. Want to add a check or change a severity threshold? Edit the file.
Report templates are in templates/ with CSS at templates/css/styles.css. The Python is just plumbing -- all the presentation lives in the templates.
Some of the skills need sudo over SSH -- checking which process owns a port, reading /etc/shadow, inspecting firewall rules. The obvious first question is "what can it actually do on my box?"
Short answer: you create a dedicated user with a sudoers file that only permits specific read-only commands. Block package installs, service restarts, file writes, etc.
# System account, no password, SSH key only
sudo useradd -r -m -s /bin/bash claude
sudo passwd -l claude # lock the password -- key-only access
sudo mkdir -p /home/claude/.ssh
sudo chmod 700 /home/claude/.ssh
# Add the public key that Claude Code will use to connect.
# Basic version -- key only:
sudo tee /home/claude/.ssh/authorized_keys <<< "ssh-ed25519 AAAA... claude-code"
# Or the locked-down version with the command wrapper (see step 7):
# sudo tee /home/claude/.ssh/authorized_keys <<< 'command="/usr/local/bin/claude-validate.sh",no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... claude-code'
sudo chmod 600 /home/claude/.ssh/authorized_keys
sudo chown -R claude:claude /home/claude/.sshYou do need /bin/bash here, not /usr/sbin/nologin -- sshd uses the login shell to run remote commands. The locked password and key-only access are what keep things secure; there is no password to brute force.
There is a ready-made sudoers file in examples/sudoers-claude with comments on every entry.
sudo cp examples/sudoers-claude /etc/sudoers.d/claude
sudo chmod 0440 /etc/sudoers.d/claude
sudo visudo -c # always validate!Commands are grouped by purpose:
| Category | What it does | Example commands |
|---|---|---|
| Network | See which process owns each port | ss -tlnp, ss -ulnp |
| Packages | Refresh package index (not install) | apt update, apt list --upgradable |
| Firewall | Read firewall rules | iptables -L -n, nft list ruleset |
| Auth/access | Check for stale accounts, weak sudo rules | cat /etc/shadow, cat /etc/sudoers |
| Docker | Inspect containers (read-only) | docker ps, docker inspect |
| Files | Find SUID binaries and exposed secrets | find / -perm -4000 |
| Cron | Read scheduled jobs | crontab -l -u <user> |
| Security tools | Check if auditd/fail2ban are running | auditctl -l, fail2ban-client status |
Equally important -- the sudoers file deliberately blocks:
- Package management (
apt install,apt remove,dnf install) - Service control (
systemctl start/stop/restart) - File modification (
rm,mv,cp,tee,chmod,chown) - User management (
useradd,userdel,usermod) - Mounting filesystems
- Rebooting the server
- Arbitrary root commands
The one entry worth a second look is sudo find. The skills use it to hunt for SUID binaries and exposed .env files, but find -exec can run arbitrary commands as root.
If that makes you uncomfortable, just delete the line from the sudoers file. The skill notices the missing permission, skips the check, and notes it was skipped. Everything else carries on.
Try the user out before pointing it at anything you care about:
# These should work
ssh claude@yourserver sudo ss -tlnp
ssh claude@yourserver sudo cat /etc/shadow
ssh claude@yourserver sudo docker ps
# These should be denied
ssh claude@yourserver sudo apt install curl
ssh claude@yourserver sudo systemctl restart nginx
ssh claude@yourserver sudo rm -rf /The sudoers file controls what sudo can do, but the claude user can still run non-sudo commands freely (listing directories, reading files, etc.). If you want a hard limit on every command -- not just the privileged ones -- there is a wrapper script for that.
It uses the command= option in authorized_keys. When sshd sees a key with command="/some/script", it runs that script instead of whatever the remote side asked for. The original command goes into $SSH_ORIGINAL_COMMAND, so the script can inspect it and decide whether to let it through.
Install the wrapper:
sudo cp examples/claude-validate.sh /usr/local/bin/claude-validate.sh
sudo chmod 755 /usr/local/bin/claude-validate.shThen update authorized_keys to use it:
command="/usr/local/bin/claude-validate.sh",no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... claude-code
The no-* options stop the key being used for tunnelling, getting a terminal, or forwarding agents -- even if someone nicks the private key.
The wrapper itself (examples/claude-validate.sh) does three things:
- Allowlists command names -- only binaries the skills actually use (
ss,cat,docker,systemctl, etc.) are permitted. Anything else is rejected. Notably,curlandpython3are not on the list -- external API lookups (KEV, EPSS) run on the local machine, not the remote server. - Denylists dangerous subcommands --
aptmight be allowed, butapt installis not. Same forsystemctl restart,docker run,rm,chmod, etc. - Logs everything -- both allowed and denied commands go to syslog tagged
claude-ssh, so you have a full audit trail.
This is best-effort shell parsing. It handles the compound commands the skills construct (pipes, chains, semicolons), but shell syntax has a lot of dark corners and it will not catch every edge case. That is fine -- it is an outer fence. The sudoers file underneath still governs what sudo can do.
With both layers active, a command has to pass two checks:
ssh claude@server "sudo ss -tlnp"
|
authorized_keys command= triggers the wrapper
|
wrapper checks: is "sudo" allowed? is "ss" allowed? any denied patterns? -> OK
|
bash runs the command
|
sudo checks: is "ss" in the sudoers file for claude? -> OK
|
ss -tlnp runs
The example sudoers uses Debian/Ubuntu paths. On RHEL-family systems, run which ss, which iptables, etc. and adjust accordingly. The commands are the same.
Once you've scanned a few servers it will become a total pita unless you have decent ssh keys/access/whatever set up. If every server has different credentials and/or auth method, each new scan means fscking around in 1Password or emailing someone to get access.
A solution might be shared SSH keys in a 1Password vault (or setting up something like HashiCorp Vault). 1Password's cli SSH agent serves the key automagically.
Copyright (c) 2026 ohnotnow. Licensed under the GNU AGPL-3.0 — see LICENSE.