Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 42 additions & 117 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# HaH

HaH is a "hunt and heal" utility for inspecting and cleaning up Linux systems. The goal is to detect common system maintenance problems, explain why they matter, and offer safe cleanup or repair actions.
HaH is a diagnostic utility for inspecting Linux systems. The goal is to detect common system maintenance problems, explain why they matter, and offer safe remediation suggestions.

## Usage

```
```bash
hah <COMMAND>
```

Expand All @@ -17,12 +17,10 @@ hah <COMMAND>

### `hah scan` options

| Option | Default | Description |
| ------------------- | -------------- | --------------------------------------------------------------------------- |
| `--output <FORMAT>` | `terminal` | Output format: `terminal`, `json`, or `yaml` |
| `--check <ID>` | _(all checks)_ | Run only the single check with this ID |
| `--fix` | off | Apply safe remediations automatically |
| `--dry-run` | on | Report findings only, no changes (default behavior, conflicts with `--fix`) |
| Option | Default | Description |
| ------------------- | -------------- | -------------------------------------------- |
| `--output <FORMAT>` | `terminal` | Output format: `terminal`, `json`, or `yaml` |
| `--check <ID>` | _(all checks)_ | Run only the single check with this ID |

### Exit codes

Expand All @@ -31,35 +29,15 @@ hah <COMMAND>
| `0` | No findings, or only Info / Warning findings |
| `1` | At least one Critical finding was detected |

### Configuration

HaH loads configuration from the following locations in order, with later files taking precedence:

1. `/etc/hah/config.yaml` — system-wide defaults
2. `~/.config/hah/config.yaml` — per-user overrides

Example configuration:

```yaml
thresholds:
boot_space_mb: 100 # warn when /boot free space drops below this
initramfs_size_mb: 100 # warn on initramfs images larger than this
journal_size_mb: 500 # warn when the systemd journal exceeds this
snap_max_revisions: 2 # warn when a snap retains more revisions than this
crash_dump_max_days: 30 # warn on crash dumps older than this many days

allowlist:
packages:
- some-package-to-ignore # suppress findings for this package
---

denylist:
packages:
- name: flashplugin-installer
reason: "Adobe Flash is end-of-life and a security risk"
## Documentation

disabled_checks:
- broken-symlinks # skip this check entirely
```
- [User Guide](docs/user/README.md) — Getting started and usage
- [Configuration Guide](docs/config.md) — Customizing thresholds and filters
- [Built-in Checks](docs/checks.md) — List of what HaH detects
- [DSL Reference](docs/dsl.md) — Writing custom YAML rules
- [Developer Guide](docs/dev/README.md) — Working on the HaH codebase

---

Expand All @@ -75,98 +53,45 @@ HaH is intended to help with:
- network configuration hygiene (NTP, DHCP, DNS, interface management)
- general system health checks

## Target Problems

### Boot and Kernel Maintenance
## Capabilities

- low disk space on `/boot`
- unused kernels that can be removed safely
- oversized or outdated initramfs images
- initramfs compression choices that waste boot partition space
- stale kernel headers and modules
- mismatched running kernel versus installed kernel packages
HaH detects a wide range of system maintenance issues and provides information on why they matter, along with remediation suggestions.

### Drivers and DKMS
### Boot and Kernel

- DKMS modules that fail to build on newer kernels
- orphaned driver sources left behind after upgrades
- third-party drivers that block kernel upgrades
- NVIDIA, VirtualBox, ZFS, or similar modules with broken rebuild status
- missing build dependencies required for DKMS recovery
- **Disk Space**: Low free space on `/boot`.
- **Cleanup**: Unused kernels and stale kernel headers/modules.
- **Configuration**: Suboptimal initramfs compression or oversized images.
- **Drivers**: DKMS modules that fail to build or broken driver states.

### APT and Repository Cleanup
### Package Hygiene (APT, Snap, Dpkg)

- old or deprecated APT repositories
- duplicate repository definitions across `/etc/apt/sources.list` and `sources.list.d`
- leftover repository keys or keyrings that are no longer used
- old signing keys stored with deprecated trust methods such as `apt-key`
- legacy APT source formats that should be migrated to newer `.sources` entries or modern keyring usage
- outdated APT configuration snippets that override current defaults or reference removed repositories
- packages installed from repositories that no longer exist
- failed or partial package states in `dpkg` or `apt`
- **State**: Failed or partial package states (`dpkg --audit`).
- **Cleanup**: Residual configuration files (`rc` state) and auto-removable packages.
- **Security**: Deprecated `apt-key` usage and legacy repository formats.
- **Conflicts**: Software duplicated across multiple package managers (e.g., APT and Snap).
- **Custom Rules**: Support for user-defined package denylists via configuration.

### Package Hygiene
### Network Configuration

- packages that should no longer be installed
- obsolete packages left over from distro migrations
- package cleanup rules driven by YAML configuration
- automatically removable packages that were never cleaned up
- residual config packages in the `rc` state
- **Redundancy**: Multiple active NTP or DHCP clients causing management overlap.
- **Legacy**: Outdated network tooling (`ifupdown`, `ntp`) alongside modern managers.
- **Resolved**: Incorrect `systemd-resolved` stub resolver configuration.

### Snap and Cross-Package-Manager Conflicts
### System Drift and Tuning

- software installed via both APT and Snap
- cases where the Snap package is preferred because it is still maintained
- broken Snap installs, disabled revisions, or excessive retained revisions
- packages duplicated across APT, Snap, Flatpak, or manual installs
- **Integrity**: Broken symbolic links and stale systemd units.
- **Resources**: Excessive journal growth and old crash dumps.
- **Kernel Tuning**: Conflicting or redundant `sysctl` parameters across different files.

### Network Configuration

- legacy NTP daemon (`ntp` / ISC ntpd) installed instead of `chrony` or `systemd-timesyncd`
- multiple time-sync services active simultaneously, competing to adjust the clock
- legacy ISC DHCP client (`dhclient`) still installed when NetworkManager or `systemd-networkd` handles DHCP
- non-loopback interface definitions in `/etc/network/interfaces` (ifupdown) alongside Netplan or NetworkManager
- `/etc/resolv.conf` not linked to `systemd-resolved`'s stub resolver after an upgrade
- `resolvconf` package conflicting with `systemd-resolved`
- `ifupdown` installed alongside a modern network manager causing management overlap

### Leftovers and System Drift

- residual configuration files from removed software
- old log files, caches, and temporary artifacts
- broken symlinks left by removed packages
- stale systemd units, timers, or service drop-ins
- configuration drift after in-place upgrades
- outdated configuration files or settings carried forward across releases
- legacy defaults that no longer match current distro recommendations
- missing, conflicting, or suspicious `sysctl` parameters
- `sysctl` overrides that degrade security, stability, or network behavior

## Additional Ideas

- dry-run mode that reports findings without changing the system
- severity levels such as info, warning, and critical
- clear remediation output with exact commands before execution
- backup or snapshot hooks before destructive actions
- allowlist and denylist support for packages and repositories
- profile-based scans for desktop, server, VM, or container hosts
- distro-specific handlers for Debian, Ubuntu, Mint, and related systems
- machine-readable output such as JSON or YAML
- audit report generation for scheduled maintenance runs
- interactive mode for reviewing each fix before applying it
- non-interactive mode for automation
- plugin or rule system so checks can be added incrementally
- safety checks to avoid removing the currently running kernel
- detection of unsupported end-of-life releases
- checks for held packages that block security updates
- checks for interrupted upgrades or pending reboot requirements
- cleanup of old crash dumps and journal growth
- validation of `sysctl.d` ordering, overrides, and obsolete kernel tunables
- detection of deprecated config formats across package manager and system settings
- detection of conflicting or redundant NTP, DHCP, and DNS resolver configurations
- migration guidance from legacy network tooling to Netplan or NetworkManager
- optional integration with SMART, filesystem, and memory health checks
---

## Future Direction

HaH could evolve into a rule-based maintenance assistant that combines detection, explanation, and safe remediation for long-lived Linux systems.
HaH is evolving into a comprehensive diagnostic assistant for long-lived Linux systems. Future goals include:

- **Audit Reports**: Generation of detailed maintenance reports in HTML or Markdown.
- **System Profiles**: Check sets tailored for specific roles (Desktop, Server, Container).
- **Extended Diagnostics**: Integration with SMART data, filesystem health, and hardware metrics.
- **Release Lifecycle**: Detection of unsupported end-of-life distribution releases.
- **DSL Expansion**: More powerful data sources and filtering for the YAML rule engine.
7 changes: 0 additions & 7 deletions crates/hah-checks/src/apt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ impl Check for ResidualConfigCheck {
remediation: Some(Remediation {
description: "Purge residual configurations.".into(),
commands: vec![format!("sudo dpkg --purge {list}")],
safe: false,
}),
})
}
Expand Down Expand Up @@ -105,7 +104,6 @@ impl Check for DpkgStateCheck {
"sudo dpkg --configure -a".into(),
"sudo apt-get install -f".into(),
],
safe: false,
}),
})
}
Expand Down Expand Up @@ -152,7 +150,6 @@ impl Check for AutoremovableCheck {
remediation: Some(Remediation {
description: "Remove unused auto-installed packages.".into(),
commands: vec!["sudo apt autoremove --purge".into()],
safe: false,
}),
})
}
Expand Down Expand Up @@ -186,7 +183,6 @@ pub(crate) fn apt_key_finding(path: &Path) -> Option<Finding> {
--import /tmp/key.asc"
.into(),
],
safe: true,
}),
})
} else {
Expand Down Expand Up @@ -265,7 +261,6 @@ fn legacy_sources_finding(legacy_files: Vec<String>) -> Option<Finding> {
remediation: Some(Remediation {
description: "Convert to DEB822 format (one .sources file per repository).".into(),
commands: vec!["# See: https://wiki.debian.org/SourcesList#DEB822_format".into()],
safe: true,
}),
})
}
Expand Down Expand Up @@ -332,7 +327,6 @@ impl Check for UserDefinedPackageCheck {
remediation: Some(Remediation {
description: format!("Remove {}", entry.name),
commands: vec![format!("sudo apt remove --purge {}", entry.name)],
safe: false,
}),
});
}
Expand Down Expand Up @@ -371,7 +365,6 @@ mod tests {

fn make_ctx(runner: Arc<dyn CommandRunner>, config: Config, distro_id: &str) -> Context {
Context {
dry_run: false,
verbose: false,
config,
distro: DistroInfo {
Expand Down
7 changes: 0 additions & 7 deletions crates/hah-checks/src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ impl Check for BootSpaceCheck {
remediation: Some(Remediation {
description: "Remove unused kernels to free space.".into(),
commands: vec!["sudo apt autoremove --purge".into()],
safe: false,
}),
})
} else {
Expand Down Expand Up @@ -143,7 +142,6 @@ impl Check for UnusedKernelsCheck {
remediation: Some(Remediation {
description: "Remove unused kernels with apt.".into(),
commands: vec!["sudo apt autoremove --purge".into()],
safe: false,
}),
})
}
Expand Down Expand Up @@ -205,7 +203,6 @@ impl Check for StaleKernelHeadersCheck {
.iter()
.map(|p| format!("sudo apt remove --purge {p}"))
.collect(),
safe: false,
}),
})
}
Expand Down Expand Up @@ -255,7 +252,6 @@ impl Check for InitramfsCheck {
remediation: Some(Remediation {
description: "Regenerate initramfs images.".into(),
commands: vec!["sudo update-initramfs -u -k all".into()],
safe: false,
}),
});
}
Expand Down Expand Up @@ -301,7 +297,6 @@ impl Check for DkmsStatusCheck {
remediation: Some(Remediation {
description: "Attempt DKMS rebuild.".into(),
commands: vec!["sudo dkms autoinstall".into()],
safe: false,
}),
});
}
Expand Down Expand Up @@ -367,7 +362,6 @@ pub(crate) fn classify_compression(content: &str) -> Option<Finding> {
.into(),
"sudo update-initramfs -u -k all".into(),
],
safe: false,
}),
})
} else {
Expand Down Expand Up @@ -405,7 +399,6 @@ mod tests {

fn make_ctx(runner: Arc<dyn CommandRunner>, distro_id: &str) -> Context {
Context {
dry_run: false,
verbose: false,
config: Config::default(),
distro: DistroInfo {
Expand Down
4 changes: 0 additions & 4 deletions crates/hah-checks/src/drift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ pub(crate) fn scan_for_broken_symlinks(dirs: &[&str]) -> CheckResult {
remediation: Some(Remediation {
description: "Remove the broken symlink.".into(),
commands: vec![format!("sudo rm {}", path.display())],
safe: false,
}),
});
}
Expand Down Expand Up @@ -96,7 +95,6 @@ pub(crate) fn scan_crash_dirs(dirs: &[&str], max_days: u64) -> CheckResult {
remediation: Some(Remediation {
description: "Remove old crash dump.".into(),
commands: vec![format!("sudo rm {parent}/{name}")],
safe: false,
}),
});
}
Expand Down Expand Up @@ -141,7 +139,6 @@ impl Check for JournalSizeCheck {
remediation: Some(Remediation {
description: "Vacuum the journal to reclaim space.".into(),
commands: vec![format!("sudo journalctl --vacuum-size={threshold_mb}M")],
safe: true,
}),
})
} else {
Expand Down Expand Up @@ -181,7 +178,6 @@ mod tests {

fn make_ctx(runner: Arc<dyn CommandRunner>) -> Context {
Context {
dry_run: false,
verbose: false,
config: Config::default(),
distro: DistroInfo::default(),
Expand Down
6 changes: 0 additions & 6 deletions crates/hah-checks/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ impl Check for LegacyNtpCheck {
"sudo apt remove --purge ntp".into(),
"sudo apt install chrony && sudo systemctl enable --now chrony".into(),
],
safe: false,
}),
})
}
Expand Down Expand Up @@ -146,7 +145,6 @@ impl Check for NtpConflictCheck {
"sudo systemctl disable --now systemd-timesyncd".into(),
"# Then enable only one: sudo systemctl enable --now chrony".into(),
],
safe: false,
}),
})
}
Expand Down Expand Up @@ -199,7 +197,6 @@ impl Check for LegacyDhcpClientCheck {
remediation: Some(Remediation {
description: "Remove the legacy ISC DHCP client.".into(),
commands: vec!["sudo apt remove --purge isc-dhcp-client".into()],
safe: false,
}),
})
}
Expand Down Expand Up @@ -312,7 +309,6 @@ pub(crate) fn legacy_interfaces_finding(
"# Netplan reference: https://netplan.readthedocs.io/".into(),
"# After migration: sudo apt remove --purge ifupdown".into(),
],
safe: true,
}),
})
}
Expand Down Expand Up @@ -377,7 +373,6 @@ impl Check for ResolvedConfigCheck {
commands: vec![
"sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf".into(),
],
safe: false,
}),
})
}
Expand Down Expand Up @@ -429,7 +424,6 @@ mod tests {

fn make_ctx(runner: Arc<dyn CommandRunner>, distro_id: &str) -> Context {
Context {
dry_run: false,
verbose: false,
config: Config::default(),
distro: DistroInfo {
Expand Down
Loading
Loading