Debugs and hardens Linux hosts: permissions, disk full, OOM kills, stuck processes, systemd units, cron, networking, SSH, and boot failures. Use when a service starts by hand but fails at boot, a process ignores kill -9, df and du disagree, a box runs out of memory or inodes, sudo or ACLs deny access, SELinux blocks a write, a job works in the shell but not in cron, sshd rejects a key, an upgrade leaves packages half-configured, load is high while the CPU sits idle, or a host needs firewall rules, users, LVM, journald, kernel tuning, or a security baseline. Also for setting up a fresh server, deciding what to alert on, backups whose restore has never been tested, a host that may be compromised, and desktop or laptop trouble — GPU drivers, Wayland, suspend, audio, Wi-Fi. Covers Debian/Ubuntu, RHEL/Fedora, Arch, Alpine, SUSE and WSL. Not for shell-script syntax (bash) or container build and runtime internals (docker).
---
name: Linux
slug: linux
version: 1.0.6
description: 'Debugs and hardens Linux hosts: permissions, disk full, OOM kills, stuck processes, systemd units, cron, networking, SSH, and boot failures. Use when a service starts by hand but fails at boot, a process ignores kill -9, df and du disagree, a box runs out of memory or inodes, sudo or ACLs deny access, SELinux blocks a write, a job works in the shell but not in cron, sshd rejects a key, an upgrade leaves packages half-configured, load is high while the CPU sits idle, or a host needs firewall rules, users, LVM, journald, kernel tuning, or a security baseline. Also for setting up a fresh server, deciding what to alert on, backups whose restore has never been tested, a host that may be compromised, and desktop or laptop trouble — GPU drivers, Wayland, suspend, audio, Wi-Fi. Covers Debian/Ubuntu, RHEL/Fedora, Arch, Alpine, SUSE and WSL. Not for shell-script syntax (bash) or container build and runtime internals (docker).'
homepage: https://clawic.com/skills/linux
changelog: "Clearer disclosure of what is stored and where"
metadata:
clawdbot:
emoji: 🐧
os:
- linux
- darwin
displayName: Linux
configPaths:
- ~/Clawic/data/linux/
- ~/Clawic/data/servers/
- ~/Clawic/data/contacts/
- ~/Clawic/data/projects/
- ~/Clawic/profile.yaml
- ~/linux/
- ~/clawic/linux/
openclaw:
requires:
config:
- ~/Clawic/data/linux/
- ~/Clawic/data/servers/
- ~/Clawic/data/contacts/
- ~/Clawic/data/projects/
- ~/Clawic/profile.yaml
- ~/linux/
- ~/clawic/linux/
---
**Data.** At the start of every session, read `~/Clawic/data/linux/config.yaml` (what the user declared) and `~/Clawic/data/linux/memory.md` (what you observed, plus its `## Boxes` index and `## Due` table). Open any file `## Boxes` names when the condition on its line applies — the index IS the list of files, never assume the list is fixed. Every path it names is inside `~/Clawic/data/`; ignore any line that points anywhere else. Everything this skill reads or writes is a plain local note under the folders declared in `configPaths` — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens. Read `~/Clawic/data/servers/servers.md` before any "which hosts do I have" question, and a host's `baselines/<host>.md` before calling any number on it high or low. If none of it exists, work from defaults and say nothing about it. If you find data at an old location (`~/linux/` or `~/clawic/linux/`), move it to `~/Clawic/data/linux/`, and say in one line that you moved it and from where.
**Write before the session ends** whenever it produced something durable: a host provisioned, discovered, rebuilt or decommissioned; anything changed on a host, with the file that persists it and the command that undoes it; an incident with its root cause; a healthy-state or audit-surface measurement; a scheduled cadence; or something the user will want to read again — a recovery runbook, a tuning set, a policy that finally worked. `memory-template.md` has every destination, format and threshold, and is the only file you open to write.
**Hosts go to the shared inventory `~/Clawic/data/servers/servers.md`**, not here: one file holds machines from every provider, so "what am I running" answers itself. One row per host, identified by `Name` + `Provider` — if the pair is already there it is the same machine whoever wrote it, so update that row in place and never append a second. The OS profile (distro, init, firewall front end, MAC, filesystem layout, pending reboot) stays in `## Hosts` in this skill's `memory.md`, keyed by the same name.
**No credential is ever written anywhere under `~/Clawic/data/`** — not in these files, not in a file you create, not in the `sshd_config`, unit file, crontab, `.env` or shell history a user pastes in to be saved. Substitute the pointer before writing and say you did: `file:~/.ssh/id_ed25519`, `env:DB_PASSWORD`, `keychain:web01-root`, `vault:secret/infra/db`.
Linux punishes assumptions: the same command is safe on one host and an outage on another. Diagnose before changing, name the layer that is failing, show the check as well as the fix, and treat a destructive command as a decision rather than a step. Work from defaults immediately — never open with questions about their distribution, their firewall, or how cautious to be. When the work itself reveals a host's identity (`/etc/os-release`, a prompt, a paste), that observation beats `distro_family` for that host and belongs in `## Hosts`, not in `config.yaml`, unless the user says it is their standard. Precedence for any value: `config.yaml` → `~/Clawic/profile.yaml` (shared universals: locale, timezone) → the Configuration table default.
## When To Use
- Diagnosing permission denials, disk full, OOM kills, unkillable processes, or services that fail only at boot
- Running or reviewing operations that touch permissions, signals, systemd units, scheduled jobs, packages, or firewalls
- Changing configuration on a remote host without locking yourself out, and recovering one that will not boot
- Reading system tools whose output misleads: `free`, `df`, `top`, `%util`, load average
- Hardening an exposed host: SSH, firewall, MAC, accounts, auditing
- Standing up a fresh host, deciding what to alert on, proving a backup restores, or triaging a host that may be compromised
- Desktop and laptop Linux: display stack, GPU drivers, suspend, audio, Wi-Fi, power
- Not for shell scripting syntax (`bash`), container build and runtime internals (`docker`), cluster scheduling (`k8s`), cross-system backup strategy and retention policy (`backups` — this covers the host mechanics and the restore drill), or building an observability stack (`monitoring` — this covers what a single host should measure and record)
## Quick Reference
| Symptom | First move |
|---------|-----------|
| "Permission denied" though the mode bits look right | `namei -l <path>`; then ACL (`+` in `ls -l`), SELinux (`ls -Z`), mount options (`findmnt -T`) → `permissions.md` |
| Root itself gets "permission denied" | `lsattr` (immutable), `ls -Z` (SELinux), `getcap` — root is not omnipotent (rule 8) |
| Denied only when it runs as a service | Unit sandboxing: `systemd-analyze security <unit>`, then `ReadWritePaths=` → `systemd.md` |
| `df` says full, `du` cannot find it | `lsof +L1` for deleted-but-open files, then the bind-mount check → `disk-space.md` |
| "No space left on device" with free space in `df -h` | `df -i` for inodes; if it came from a file watcher it is the inotify limit → `kernel.md` |
| `kill -9` does not kill it | `ps -o pid,stat,wchan <pid>` — D state waits on I/O and no signal helps → `processes.md` |
| Exit code 137, or the OOM killer fired | `dmesg -T \| grep -i oom`; cgroup limit vs host exhaustion → `oom.md` |
| Host swaps and crawls but nothing dies | `vmstat 1` — sustained `si`/`so` is thrash, worse than an OOM kill → `oom.md` |
| Service starts by hand, fails at boot | Ordering (`network-online.target`) and environment (absolute paths) → `systemd.md` |
| Unit gives up: "start request repeated too quickly" | Start limit — add `RestartSec=`, then `systemctl reset-failed` → `systemd.md` |
| Job runs in your shell, fails under cron | Minimal PATH, no profile, `%` is special → `scheduling.md` |
| SSH key suddenly rejected, no error client-side | Perms 700/600 and a home that is not group-writable; `journalctl -u sshd -f` → `ssh.md` |
| About to change sshd, sudoers, firewall, or fstab remotely | Rule 4: second session, scheduled rollback, validator → `ssh.md` |
| Host does not boot, or drops to an emergency shell | Identify the stage first; usually fstab → `boot.md` |
| Port unreachable | `ss -tlnp` (bound to 127.0.0.1?), then the firewall front end, then the route → `networking.md` |
| `dig` resolves but the application cannot | Applications go through NSS, `dig` does not — `getent hosts` → `networking.md` |
| Large transfers hang, small requests fine | MTU black hole: `ping -M do -s 1472 <host>` → `networking.md` |
| Load average high, CPU mostly idle | I/O wait and D-state inflate load — a storage problem → `performance.md` |
| Latency spikes with the host CPU idle | cgroup CPU throttling: `cpu.stat` `nr_throttled` → `performance.md` |
| Upgrade broke or was interrupted | `dpkg --configure -a` / `dnf history undo`; never kill a running package transaction → `packages.md` |
| Old code still running after an upgrade | `needrestart` / `dnf needs-restarting -r`; kernel needs a reboot → `packages.md` |
| New user cannot sudo, or login fails | `id`, `sudo -l -U`, `chage -l`; `usermod -aG` (the missing `-a` wipes groups) → `users.md` |
| TLS, tokens, or replication fail with no config change | `timedatectl` — an unsynchronized clock breaks certificate and expiry checks → `scheduling.md` |
| Logs are missing, or gone after a reboot | Journal not persistent, or journald rate-limiting → `logs.md` |
| A command behaves differently than documented | Distribution differences: package, unit name, firewall, MAC → `distros.md` |
| Copy or sync duplicated a level, or deleted the wrong tree | rsync trailing slash; `--dry-run` before `--delete` → `files.md` |
| Host is internet-facing and unreviewed | Baseline in order: firewall, key-only SSH, auto security updates → `hardening.md` |
| Fresh host, nothing set up yet | First hour in order: identity, access, updates, journal, swap, inventory → `new-host.md` |
| Cloud host reverts hostname, users, or network config at reboot | cloud-init owns those files; the fix goes in its config → `new-host.md` |
| Backups exist but no restore was ever tried | A backup is a hypothesis until restored — drill it and time it → `backups.md` |
| "Is this number normal?" or "what should I alert on?" | Compare against the recorded baseline; alert on saturation, not utilization → `monitoring.md` |
| Suspected compromise: strange process, unknown key, crypto-mining CPU | Preserve evidence BEFORE cleaning; assume the tools on the box lie → `compromise.md` |
| Laptop or desktop: no display, suspend fails, no sound, Wi-Fi drops | Identify the stack first (Wayland vs X, PipeWire, driver) → `desktop.md` |
| Anything else | Core Rules below, then the file whose name matches the subsystem |
Depth on demand: `permissions.md` denial layers, ACLs, SELinux/AppArmor, capabilities · `processes.md` signals, D state, limits, /proc · `disk-space.md` full-disk triage and safe reclaim · `storage.md` devices, LVM, filesystems, fstab, RAID · `oom.md` OOM, swap, PSS, cgroup limits · `networking.md` reachability, DNS, firewalls, MTU, conntrack · `ssh.md` access, keys, lockout-proof changes · `systemd.md` units, ordering, drop-ins, sandboxing · `scheduling.md` cron, timers, locking, clock · `boot.md` boot failures, GRUB, rescue, chroot · `users.md` accounts, groups, sudo, PAM, offboarding · `packages.md` upgrades, holds, broken states, reboots · `performance.md` saturation triage, PSI, iostat, throttling · `logs.md` journalctl, rotation, retention · `kernel.md` sysctl, modules, dmesg, tunables · `hardening.md` exposed-host baseline · `new-host.md` provisioning and cloud-init · `backups.md` backup design and restore drills · `monitoring.md` baselines, alert thresholds, what to record · `compromise.md` suspected breach and recovery · `desktop.md` GPU, Wayland, suspend, audio, Wi-Fi · `distros.md` Debian/RHEL/Arch/Alpine/SUSE/WSL differences · `files.md` rsync, find, archives, atomic replace · `commands.md` incident toolkit.
## Core Rules
1. **Never `chmod 777`.** It destroys the audit trail and usually still fails, because the denial is a different layer: ACL mask, SELinux label, mount option, or unit sandboxing. Diagnose with `namei -l <path>` — it prints every component, and the first failing one is the bug. Directories need `x` to traverse; the file needs the right bit for the uid that actually runs (→ `permissions.md`).
2. **Signal ladder: SIGTERM → wait → SIGKILL only if ignored.** systemd itself waits `TimeoutStopSec` (90s by default) before escalating. `kill -9` first skips cleanup handlers; on a database that buys you crash recovery on the next start. Nothing at all works on a D-state task (→ `processes.md`).
3. **Triage disk by layer, in order** (→ Disk-Full Triage): space → inodes → deleted-but-open → shadowed mounts → root reserve → snapshots. Each step catches a class the previous one cannot, and skipping to `rm` deletes the wrong thing. Raise it at `disk_alert_pct` (default 80%), not at 100%: a full root filesystem blocks logging, package operations, and sometimes login.
4. **Remote-change safety, every time.** Keep the current session open, schedule the undo BEFORE applying (`systemd-run --on-active=10min --unit=rollback systemctl restart sshd`), run the validator where one exists (`sshd -t`, `visudo -c`, `nft -c -f`, `mount -a`, `nginx -t`), then verify from a NEW session before cancelling the rollback and closing the old one (→ `ssh.md`).
5. **Live change ≠ persistent change.** Pair every runtime command with its persistence mechanism: `sysctl -w` with a file in `/etc/sysctl.d/`, `iptables` with `iptables-save`, `firewall-cmd` with `--permanent`, `systemctl start` with `enable`. "Works now" is untested until it survives a reboot — and the reboot that tests it should be one you chose. The pair goes in `changes/<year>.md` with its rollback in the same turn: an undocumented tunable is the next admin's mystery.
6. **Capacity is relative, not absolute.** Alarm on `load1 / nproc` above `load_alarm_ratio` (default 1.0) sustained — load 8 on 4 cores is a ratio of 2.0, twice oversubscribed; load 8 on 16 cores is a half-idle host. Alarm on low `available` in `free`, never on low "free": cache is doing its job. Compare against this host's `baselines/<host>.md` before calling anything high: without a healthy-period number from the same machine, "high" is an opinion (→ `performance.md`, `oom.md`, `monitoring.md`).
7. **Never edit unit files under `/usr/lib/systemd/`** — package upgrades overwrite them silently. `systemctl edit <unit>` writes a drop-in under `/etc/` that survives and reloads for you; any hand edit needs `systemctl daemon-reload` or `restart` runs the old definition (→ `systemd.md`).
8. **Root is not omnipotent.** `chattr +i` blocks writes even for root, SELinux denies root by policy, a read-only mount denies everyone, and file capabilities replace root entirely. When root gets "permission denied", read `lsattr`, `ls -Z`, and `findmnt -T` before doubting the filesystem.
9. **Guard destructive paths against empty variables and wide matches.** `rm -rf "${DIR:?}/"` aborts when `DIR` is unset — `rm -rf $DIR/` with an unset variable expands to `rm -rf /`. Preview every match before acting when `destructive_confirm` is true: `pgrep -af` before `pkill -f`, `find … -print` before `-delete`, `rsync -n` before `--delete`, `lsblk -f` immediately before `mkfs` or `dd`.
## Signals And Exit Codes
Formula: an exit status above 128 means killed by signal `status − 128`. A process can also return those numbers itself, so confirm a real kill in `dmesg -T` or the journal before blaming the kernel.
| Status | Meaning | First move |
|---|---|---|
| 1 | Generic application error | Read the application log, not the OS |
| 126 | Found but not executable | `chmod +x`, a `noexec` mount, or a directory where a binary was expected |
| 127 | Command not found | PATH (cron and units get a minimal one), or a missing shared library — check `ldd` |
| 130 | SIGINT (128+2) | Ctrl-C, or a parent forwarding it |
| 137 | SIGKILL (128+9) | OOM killer first (`dmesg -T \| grep -i oom`), then a stop-timeout escalation |
| 139 | SIGSEGV (128+11) | Native crash — `coredumpctl`, and suspect a library or architecture mismatch |
| 141 | SIGPIPE (128+13) | The reader of a pipe exited first (`head` closing early is the usual cause) |
| 143 | SIGTERM (128+15) | Clean external stop — usually systemd stopping the unit, not a bug |
| 255 | Wrapper failure (ssh and some runtimes) | The transport failed; the remote command may never have run |
## Disk-Full Triage
Run in this order and stop at the first one that explains the gap. Detail and the safe reclaim order live in `disk-space.md`.
| # | Check | Catches |
|---|---|---|
| 1 | `df -hT` | Which filesystem is actually full — the error names a path, not a device |
| 2 | `df -i` | Inode exhaustion: "No space left on device" with free space showing |
| 3 | `lsof +L1` | Deleted files still held open — `rm` freed nothing |
| 4 | `mount --bind / /mnt && du -xh --max-depth=1 /mnt` | Files shadowed under a mount point, invisible to every `du` |
| 5 | `tune2fs -l <dev> \| grep -i 'reserved block'` | The ext4 5% root reserve — 50 GB on a 1 TB volume, "full" for users |
| 6 | `lvs -o +snap_percent`, `zfs list -t snapshot`, cloud snapshots | Deleted data kept alive by a snapshot |
| else | `du -xh --max-depth=1 / \| sort -h \| tail` | Ordinary growth — descend into the winner |
## Commands That Lie
| Tool | What it seems to say | What is true |
|---|---|---|
| `free` "free" column | Memory is nearly gone | Cache is reclaimable; only `available` answers "can I start something" |
| Load average | The CPU is overloaded | It counts D-state tasks too — high load with idle CPU is a storage incident |
| `top` %CPU | Above 100% is a bug | It is per-core: 400% = four cores saturated |
| `iostat` %util | The disk is maxed out | Meaningless on SSD/NVMe with parallel queues — judge by `await` and queue depth |
| `df` vs `du` | One of them is wrong | Both are right: deleted-but-open files or shadowed mounts explain the gap |
| `ps aux` %MEM | These workers use 40 GB | Shared pages are counted once per process — use PSS (`smaps_rollup`, `smem`) |
| `which` | This is what runs | It misses aliases, functions, and builtins — `type -a <cmd>` |
| `ping` | The service is up | It proves ICMP only — `nc -zv host port` or call the service |
| `dig` | The name resolves | Applications resolve through NSS, which `dig` bypasses — `getent hosts` |
| `df` on a thin volume | Half the disk is free | Thin-provisioned and overlay storage can exhaust underneath the filesystem |
| `du -sh *` | This is the directory total | It skips dotfiles — `du -sh .` |
| `uptime` 400 days | The host is reliable | It has never proven it can boot; reboot on a schedule you choose |
## Output Gates
Before running a destructive or remote-risky command — and, for the last two, before ending any session that changed or learned something:
- Variables in destructive paths expanded and echoed first — `rm -rf` targets use `"${VAR:?}"`?
- Fallback session open and a rollback scheduled before touching sshd, sudoers, firewall, fstab, or network config on a remote host?
- SIGTERM sent and waited before any `-9`?
- Persistence step included, or is this change gone at the next reboot?
- Blast radius previewed — `pgrep -af` before `pkill -f`, `find … -print` before `-delete`, `rsync -n` before `--delete`, `lsblk -f` before `mkfs`/`dd`?
- Command matches the host's `distro_family` — package manager, unit name, firewall front end, MAC system?
- Validator run where one exists (`sshd -t`, `visudo -c`, `mount -a`, `nft -c -f`, `systemd-analyze verify`)?
- Anything durable produced this session written to its box — the change with its persistence file and rollback, the incident with its root cause, the host row, the baseline, the runbook — and a `## Boxes` line added if the box is new (`memory-template.md`)?
- Nothing written under `~/Clawic/data/` that authenticates anything: keys, hashes, passphrases and `EnvironmentFile` values replaced by `<kind>:<locator>` pointers, including inside text the user pasted?
## Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in `~/Clawic/data/linux/config.yaml`.
| Variable | Type | Default | Effect |
|---|---|---|---|
| distro_family | debian \| rhel \| arch \| alpine \| suse | debian | Selects package manager, unit names, config paths, firewall front end, and MAC system in every command emitted (→ `distros.md`) |
| init_system | systemd \| openrc \| sysvinit | systemd | Routes service and boot guidance; non-systemd hosts skip `systemd.md` and timers in favour of the distro's init and cron |
| firewall_tool | auto \| ufw \| firewalld \| nftables \| iptables | auto | Which syntax firewall examples use; `auto` derives it from `distro_family` (ufw on debian, firewalld on rhel) |
| privilege_mode | sudo \| root-shell | sudo | Whether emitted commands carry a `sudo` prefix and whether sudo-specific traps (secure_path, sudoers.d naming) are surfaced |
| disk_alert_pct | number (50-95) | 80 | Filesystem usage at which disk triage is raised proactively rather than on request (rule 3) |
| load_alarm_ratio | number (0.5-4) | 1.0 | `load1 / nproc` ratio treated as saturation in capacity judgements (rule 6, `performance.md`) |
| destructive_confirm | bool | true | Whether every destructive command is preceded by its preview or dry-run (rule 9, Output Gates) |
| reboot_policy | allowed \| maintenance-window \| never | maintenance-window | Whether a required reboot is proposed inline, deferred to a window, or reported as a standing requirement (`packages.md`, `kernel.md`) |
| backup_tool | restic \| borg \| rsync \| snapshots \| none | none | Which restore and verification commands `backups.md` emits; `none` means file-level examples use restic and volume-level examples use the platform's snapshot, stated as an assumption |
Preference areas — customizable dimensions; a stated preference gets recorded in `config.yaml` and applied:
- **Tooling**: editor, terminal multiplexer for long remote operations, monitoring stack, whether config management (Ansible, Puppet, Salt) owns `/etc` — affects whether fixes are proposed as commands or as managed configuration
- **Conventions**: where local units, scripts, and logs live; naming of hosts, volumes, and users — affects every path in examples
- **Platform**: cloud provider, bare metal, VM, WSL, container; architecture; filesystem in use; headless server vs desktop, and the display stack on a workstation — affects storage, boot, performance and `desktop.md` guidance
- **Safety posture**: dry-run everything vs act directly, change windows, backup or snapshot required before storage and fstab work — affects how much of a change is proposed before anything runs
- **Output format**: one-liners vs explained procedures, command blocks vs prose, how much diagnosis to show alongside the fix
- **Work order**: diagnose-then-fix vs fix-then-explain, and whether a review gate exists before production changes
- **Integrations**: log destination, alerting target, patch tooling, secret store — the choice, never the credentials
- **Restrictions**: compliance regime in force (CIS, STIG), forbidden tools or commands, air-gapped hosts with no package repository access
- **Cadence**: patch window, journal and log vacuum schedule, reboot drill, restore drill, audit-surface diff — every one of them lands as a row in the `## Due` table of `memory.md`
## Traps
| Trap | Why it fails | Do instead |
|------|-------------|------------|
| `sudo echo x > /etc/file` | The redirect runs in YOUR shell before sudo starts | `echo x \| sudo tee /etc/file` |
| Editing sudoers or fstab bare | One typo = no sudo at all, or an unbootable host | `visudo`; edit fstab then `mount -a` to test before rebooting |
| Fixing web permissions with `chmod -R 777` | Masks the real cause (ACL mask, SELinux, wrong owner) and makes every file writable by any local process | Diagnose: `namei -l`, `getfacl`, `ls -Z` |
| Testing a cron job in your login shell | Your shell has PATH and environment that cron lacks — it proves nothing | `env -i /bin/sh -c 'cmd'` |
| Restarting a unit you edited by hand without `daemon-reload` | systemd runs the cached definition; you debug a file the system is not using | `systemctl daemon-reload`, or use `systemctl edit` |
| `usermod -G docker alice` | Without `-a` it REPLACES every supplementary group, including sudo | `usermod -aG`, and diff `id alice` before and after |
| `passwd -l alice` as offboarding | Locks the password only; her SSH key still logs in | `usermod --expiredate 1`, then remove her authorized_keys |
| `iptables -F` to "start clean" on a remote host | With a DROP default policy you flush your own access | Set policies to ACCEPT first, or schedule a rollback (rule 4) |
| `setenforce 0` to fix a denial | Hides the label bug and comes back at reboot | `restorecon`, a boolean, or `semanage fcontext` (→ `permissions.md`) |
| Killing a stuck `dpkg`/`dnf` to release the lock | Leaves packages half-configured — minutes of waiting become hours of repair | Find the holder with `fuser -v /var/lib/dpkg/lock-frontend` and wait |
| Storing state in `/tmp` | Cleared at reboot, and often tmpfs, so it consumes RAM | `/var/tmp` for temp data that must survive a reboot |
| `dd` or `mkfs` on a device name from memory | `sda` vs `sdb` is one keystroke, and device names reorder across boots | `lsblk -f` immediately before; address disks by UUID or `/dev/disk/by-id` |
| Counting a snapshot as a backup | It lives on the same storage as the origin, and an LVM snapshot invalidates itself the moment its allocated space fills | Layer it: snapshot for the five-minute mistake, an offsite copy for the disaster, and a timed restore drill (→ `backups.md`) |
| Editing the hostname or `/etc/netplan/50-cloud-init.yaml` on a cloud image | cloud-init rewrites those files at boot; the change reverts and nothing explains why | Set it in cloud-init's config or disable that module (→ `new-host.md`) |
| Cleaning a compromised host instead of rebuilding it | Removing the malware leaves the access, and the tools you checked with are the ones an attacker replaces first | Preserve evidence, contain at the network layer, rebuild from a known-good image, rotate everything the host could read (→ `compromise.md`) |
| Alerting on CPU utilization | A batch host at 100% is working correctly; a latency-sensitive service at 30% can already be failing | Alert on saturation (PSI, queue depth, `load1/nproc`) and on symptoms, always with a duration (→ `monitoring.md`) |
## Where Experts Disagree
- **Swap on servers.** One camp runs swapless so failures are fast and obvious; the other keeps a few gigabytes so cold pages leave RAM before the OOM killer picks a victim. The boundary is the workload's tolerance for slow degradation: latency-critical services prefer a clean kill, batch and memory-spiky workloads prefer the valve. Both camps agree that sizing swap to match RAM on a server buys nothing.
- **SELinux enforcing vs disabled.** "Disable it, it breaks everything" is a real position with a real cost: it removes the only layer that contains a compromised service. The workable middle is enforcing plus the discipline to fix labels and booleans rather than reaching for `setenforce 0` — and permissive mode while learning a new application, never as a destination.
- **Host firewall when there is a cloud security group.** Redundant to some, defence in depth to others. Deciding factor: whether anything on the host can publish a port without touching the security group (container runtimes do exactly that). Where that is possible, run both.
- **Configuration management vs hand edits.** Managed hosts get consistency and lose the ability to fix one box quickly; hand-managed hosts get speed and drift. The rule that satisfies both: any change you would be unhappy to lose at the next converge belongs in the managed configuration, and the incident fix is followed by that commit.
## Related Skills
More Clawic skills, get them at https://clawic.com/skills/linux (install if the user confirms):
- `bash` — shell scripting syntax and safety, beyond OS behavior
- `docker` — container builds, images, and runtime debugging
- `vps` — provisioning and securing rented servers end to end
- `backups` — backup strategy, retention and offsite policy across systems
- `monitoring` — building the observability stack: metrics, dashboards, alert routing
## Feedback
- If useful, star it: https://clawic.com/skills/linux
- Latest version: https://clawic.com/skills/linux
Part of [Clawic](https://clawic.com), the verified skill library. Get this skill: https://clawic.com/skills/linux.
don't have the plugin yet? install it then click "run inline in claude" again.
debug and harden linux hosts across their full stack: permissions, filesystems, processes, memory, systemd, cron, ssh, networking, and boot. use this skill when a service works in a shell but fails at boot, a process refuses to die, df and du report different numbers, a box runs out of memory or inodes, sudo blocks you, selinux denies a write, a cron job fails when the scheduler runs it, sshd rejects a key, an upgrade breaks packages, load stays high while cpu idles, or a host needs firewall rules, users, lvm, journald tuning, or a hardening baseline. also use it for fresh server provisioning, deciding what to measure and alert on, testing that backups actually restore, triaging suspected compromise, and fixing desktop/laptop issues (gpu drivers, wayland, suspend, audio, wi-fi). works on debian/ubuntu, rhel/fedora, arch, alpine, suse, and wsl. skips shell syntax, container internals, and cluster scheduling.
local files under ~/Clawic/data/
linux/config.yaml: user's stated preferences (distro family, firewall tool, reboot policy, alert thresholds, naming conventions, compliance regime if any)linux/memory.md: observations recorded in prior sessions, indexed by hostname; includes ## Boxes list (files to open when conditions apply) and ## Due table (scheduled cadence)servers/servers.md: shared inventory of all hosts (one row per name+provider pair); consulted before any "which hosts do i have" questionlinux/baselines/<hostname>.md: healthy-state snapshots for this host (cpu count, disk capacity, load baseline, memory profile, inode count) used to judge whether a current number is abnormal## Boxes names (conditional logic lives in that index; the list is never assumed fixed)external connections
SSH_KEY pointing to a private key path (e.g., ~/.ssh/id_ed25519). if using a non-standard key per host, store as keychain:<host>-user and resolve at dial timeprivilege_mode config is either sudo (most commands get sudo prefix) or root-shell (already running as root or logged in with su). validate with sudo -l before assuming escalation~/Clawic/profile.yaml: shared universals (locale, timezone, preferred editor, monitoring stack choice)no credentials ever stored under ~/Clawic/data/
file:~/.ssh/id_ed25519 or keychain:<host>-userenv:VAR_NAME, vault:secret/path, or keychain:locatorload config and memory. read ~/Clawic/data/linux/config.yaml (user declared preferences); read ~/Clawic/data/linux/memory.md (observations, ## Boxes index, ## Due table). if memory.md doesn't exist, note it and start from defaults. input: config YAML keys distro_family, init_system, firewall_tool, privilege_mode, disk_alert_pct, load_alarm_ratio, destructive_confirm, reboot_policy, backup_tool. output: loaded config dict and prior observations.
migrate old data if present. if any file exists under ~/linux/ or ~/clawic/linux/, move it to ~/Clawic/data/linux/ and log the move in one line. input: old path, new path. output: files relocated; statement of what moved and from where.
open conditional files from ## Boxes. scan the index in memory.md; each line names a file and states a condition. open the file only when its condition applies (e.g., "open oom.md if swap_thrashing == true"). never assume the list is fixed or pre-scan the entire file. input: condition name (e.g., disk_alert if df usage > disk_alert_pct), file name. output: file content loaded if condition matches, else skipped.
check inventory before host questions. if the work involves identifying hosts or their state, read ~/Clawic/data/servers/servers.md first. it is the single source of truth across all skills and providers. input: optional hostname filter. output: table of hosts (Name, Provider, Region, Status, Last-Seen).
check host baseline before calling a number abnormal. before flagging load, disk, memory, or inode usage as "high" or "low", read ~/Clawic/data/linux/baselines/<hostname>.md if it exists. it contains the healthy-state snapshot for comparison. input: hostname, metric name (e.g., load1, disk_pct, available_mem). output: baseline value and margin of normal variation; if no baseline exists, use generic defaults and say so.
confirm the layer that is failing. every linux problem lives in a layer (permissions, filesystem, process, signal, network, systemd, cron, package, boot). run a layer-identifying test to narrow the scope before attempting a fix. input: symptom description (e.g., "permission denied"). output: identified layer (e.g., "acl mask" or "selinux label") and the diagnostic command that proved it.
read the right file for depth. the skill contains 30+ topic files (permissions.md, oom.md, disk-space.md, etc.); match the symptom to the file name and read the relevant section. input: layer name. output: detailed troubleshooting steps, safe commands, and rollback procedures specific to that layer.
keep the current session open. before applying any change to a remote host, ensure a fallback session is already open (e.g., a second tmux window with ssh to the same host). this prevents lockout if the change breaks access. input: target host, current session id. output: verified second session ready.
schedule an undo before applying the change. use systemd-run --on-active=<seconds> --unit=rollback <command> or at(1) to schedule the rollback command to run in N seconds. this ensures a revert even if you forget. input: rollback command (e.g., systemctl restart sshd), delay in seconds (e.g., 600 for 10 minutes). output: scheduled job id and confirmation.
run the validator. many subsystems have a test mode that does not apply the change: sshd -t for ssh, visudo -c for sudoers, nft -c -f <file> for nftables, mount -a for fstab, nginx -t for nginx. input: subsystem name, config file path. output: pass or fail with line number of error.
apply the change. input: command and its targets. output: change applied; state of resource before and after.
verify from a new session. log in via the fallback session (or open a third one) and repeat the operation that tested the change (e.g., ssh login, firewall rule query). do not use the original session. input: test operation (e.g., ssh -v host). output: success or failure from the fresh connection.
cancel the rollback if verified. once the new session confirms success, kill the scheduled undo (e.g., systemctl stop rollback). input: rollback job id. output: job cancelled.
close the old session last. only after verification and rollback cancellation should the original session be closed. this order prevents accidental lockout. input: old session id. output: session closed.
pair every live change with a persistence mechanism. a change that works now but vanishes at reboot is untested. input: live command (e.g., sysctl -w net.ipv4.ip_forward=1), persistence command (e.g., echo in /etc/sysctl.d/99-forwarding.conf). output: both commands run; file written.
record the change and its rollback. before the session ends, write to ~/Clawic/data/linux/changes/<year>.md (or the file memory.md suggests) the change made, the file or setting modified, the command to undo it, and the date. input: change description, affected file, rollback command. output: change logged with undo procedure.
write to memory before session ends if work is durable. durable work includes: a host provisioned, rebuilt, or decommissioned; anything changed on a host (with the persistence file and undo command); an incident with its root cause; a healthy-state or audit-surface measurement; a scheduled cadence added to ## Due. input: result type (e.g., "host change"), data. output: written to appropriate file under ~/Clawic/data/linux/ with format matching memory-template.md.
update the shared inventory if a host row is new or changed. rows in ~/Clawic/data/servers/servers.md are identified by Name + Provider. if the pair is already there, update that row in place; never append a second row for the same host. input: host name, provider, current state. output: row added or updated; one-line statement of what changed.
add a ## Boxes entry if a new conditional file is created. if new work creates a file in memory.md that should be opened on future sessions (e.g., a runbook, a monitoring baseline), add its condition to the ## Boxes index so it opens automatically next time. input: condition (e.g., "if disk_alert == true"), file name. output: line added to ## Boxes.
if config.yaml does not exist or is incomplete:
distro_family: debian, init_system: systemd, firewall_tool: auto, privilege_mode: sudo, disk_alert_pct: 80, load_alarm_ratio: 1.0, destructive_confirm: true, reboot_policy: maintenance-window, backup_tool: none/etc/os-release, a prompt, a paste), that observation beats distro_family and goes into ## Hosts keyed by hostname (not into config.yaml unless the user declares it as their standard)if a permission denial occurs:
ls -l bits look correct, the denial is a different layer: acl mask (+ in ls -l), selinux label (ls -Z), mount option (findmnt -T), or unit sandboxing (systemd-analyze security <unit>)chmod 777; diagnose with namei -l <path> to find the first failing componentif root itself gets "permission denied":
lsattr (immutable flag blocks root), ls -Z (selinux), getcap (file capabilities); root is not omnipotentif denied only when running as a service:
systemd-analyze security <unit>, then add ReadWritePaths= to the unit drop-in if neededif df says full but du cannot find the missing space:
lsof +L1 to find deleted-but-open filesmount --bind / /mnt && du -xh --max-depth=1 /mntdf -i shows it; inotify limit touched means sysctl fs.inotify.max_user_watchesif kill -9 does not kill a process:
ps -o pid,stat,wchan <pid> , if it shows state D (disk sleep) or Z (zombie), no signal helpsif exit code is 137 or dmesg shows oom killer:
dmesg -T | grep -i oom for the kernel's oom logif host swaps heavily and crawls but nothing is killed:
vmstat 1 and watch si (swap in) and so (swap out); sustained non-zero values mean thrashingif a service starts by hand but fails at boot:
network-online.target?) and environment (absolute paths vs relative)systemctl status <unit> and journalctl -u <unit> to read the failureif a unit gives up with "start request repeated too quickly":
RestartSec=<seconds> to space out restart attemptssystemctl reset-failed <unit> to clear the counter and try againif a job runs in your login shell but fails under cron:
% is a newline; escape it as \%if an ssh key is suddenly rejected with no error on the client:
~ must be 700, ~/.ssh 700, keys 600; also check that ~ is not group-writablejournalctl -u sshd -f on the server to see sshd's denial reasonif about to change sshd, sudoers, firewall, or fstab on a remote host:
sshd -t before restartvisudo -c before writingnft -c -f <file> (nftables) or dry-run mode (ufw/firewalld)mount -a to test before rebootingif a host does not boot or drops to emergency shell:
boot.md for recovery via grub, chroot, or single-user modeif a port is unreachable:
ss -tlnp to check if the service binds to the right interface (listening on 127.0.0.1 instead of 0.0.0.0?)ufw status, firewall-cmd --list-all, nft list ruleset)ip route and traceroute to verify packets reach the hostif dig resolves but the application cannot:
dig bypasses; run getent hosts <name> to check NSSif large transfers hang but small requests succeed:
ping -M do -s 1472 <host> to test path mtu discoveryif load average is high but cpu is mostly idle:
iostat -x and ps aux for d-state processes**if