Rambo is an event-driven, kernel-backed system monitor for Linux. It watches RAM (via cgroup v2 limits), temperature (hwmon), pressure (PSI), network (netlink), CPU and disk, then acts on configurable policies: desktop notifications, graceful SIGTERM to a score-picked process, or suspending heavy jobs.
Detection (watchers) is separated from policy (a config-driven engine), so adding a sensor or changing behavior never requires touching daemon logic.
Status: experimental (0.x). Rambo attempts staged, policy-driven intervention before the kernel OOM killer under the configured conditions. It is not a guarantee that a process, or the system, survives memory exhaustion, and it does not replace the kernel OOM killer. Behavior, config keys and defaults may change between 0.x releases. Preview what it would do with
rambo daemon --dry-runbefore relying on it.Automatic killing is experimental and fail-closed. Automatic thermal killing is DISABLED by default and must not be enabled on a primary desktop: enabling
[temperature] allow_kill = truewith a wrong[temperature] kill_allowlistcan terminate the graphical session. Memory and thermal termination only target explicitly allowlisted workloads (see[kill] allowlist,[kill] allow_exe,[kill] allow_cgroup); anything else is never killed automatically unless[kill] allow_unknown = trueis set.
Watcher ── Event{Severity, Source} ──▶ Policy Engine ──▶ Action
(config.toml) notify / kill / suspend
All watchers are kernel-backed or kernel-accounted:
| Watcher | Kernel source | Triggers |
|---|---|---|
| Memory | cgroup v2 memory.high/memory.max + memory.events |
soft → warn, hard → kill, max/oom_kill → emergency |
| Temperature | hwmon temp*_input (CPU/GPU) |
≥ warning → warn, ≥ critical → throttle eligible only, ≥ emergency → throttle + notify (kill only with allow_kill + kill_allowlist) |
| Pressure | /proc/pressure/* (PSI) |
sustained some/full → warn / escalate |
| Network | netlink (NETLINK_ROUTE) |
link down, throughput above alert |
| CPU | /proc/stat |
busy% sustained above alert |
| Disk | statfs + /proc/diskstats |
space%, pathological I/O |
| Battery | power_supply (laptops only) |
low charge → suspend heavy jobs |
RAMBo is a systemd user service. The daemon runs as your user and never
needs root; only the narrow oom-protect helper runs through pkexec, and
only when the daemon is the canonical /usr/bin/rambo. The polkit rule, the
systemd unit and the daemon's path guard all agree on exactly /usr/bin/rambo,
so install to /usr (the default).
| Target | Status | Notes |
|---|---|---|
| Generic Linux tarball (amd64/arm64) | Built + validated in CI | Static binary, no distro assumptions |
Arch Linux (PKGBUILD) |
Built + validated in CI | Not published to the AUR |
Debian / Ubuntu (.deb) |
Built + validated in CI | Contents checked with dpkg-deb --contents |
Fedora / RHEL-compatible (.rpm) |
Built + validated in CI | Contents checked with rpm -qpl |
| openSUSE | Manual (generic tarball) | No openSUSE package target yet; the installer detects it and falls back to generic behavior |
Alpine .apk, Homebrew |
Future / not published | Not claimed as available |
Distro packages are experimental and may lag the source tree. The presence of a generic binary does not imply support for every distribution: only the targets marked above are built and validated by CI. Where a distro-specific package is missing, use the generic tarball or build from source.
Download the artifacts attached to the v0.1.0 release and verify them with
the published sha256sums.txt:
sha256sum -c sha256sums.txtGeneric tarball (rambo_0.1.0_linux_amd64.tar.gz, …_arm64.tar.gz):
tar xzf rambo_0.1.0_linux_amd64.tar.gz
cd rambo_0.1.0_linux_amd64
sudo install -Dm755 rambo /usr/bin/rambo
sudo install -Dm644 polkit/99-rambo.rules /usr/share/polkit-1/rules.d/99-rambo.rules
sudo install -Dm644 systemd/rambo.service /usr/lib/systemd/user/rambo.serviceDebian / Ubuntu:
sudo apt install ./rambo_0.1.0_amd64.debFedora / RHEL-compatible:
sudo dnf install ./rambo-0.1.0-1.*.rpmArch Linux (build the package from the repository root; it uses the deterministic tagged source and a real checksum):
git clone https://github.com/Jashk120/Rambo.git
cd Rambo
makepkg -sigit clone https://github.com/Jashk120/Rambo.git
cd Rambo
make build # ./rambo reports 0.1.0
make install-check # report distro, arch, paths, systemd/polkit, consistency
sudo make install # files only
make activate # explicit, separate service activationmake install is distro-aware: it reads /etc/os-release (with a generic
Linux fallback and a warning when the metadata is missing or unrecognized),
detects the architecture using Go's naming (amd64, arm64, 386, arm,
…), and installs the same canonical paths everywhere. It never runs
systemctl, pkexec, or any privileged command, and it honors DESTDIR for
staged package builds. Preview it with:
make install-checkIt prints the detected distro and architecture, the selected install paths,
whether systemd and polkit are available, and whether the executable, systemd
ExecStart and polkit rule paths agree. All install paths are overridable Make
variables: PREFIX, BINDIR, SYSTEMD_USER_DIR, POLKIT_RULE_DIR,
DOCDIR, MANDIR, LICENSEDIR, the completion dirs, and the reported
CONFIG_DIR/STATE_DIR. The polkit rule is installed to the single canonical
location, /usr/share/polkit-1/rules.d — never /etc. The helper and the
shipped unit only authorize /usr/bin/rambo, so a non-/usr prefix yields a
working unprivileged daemon but no passwordless oom-protect;
make install-check reports that mismatch rather than hiding it.
Run unprivileged without installing:
go run . daemon --dry-runThe default version (0.1.0) lives in internal/version/version.go. CI and
release builds override it reproducibly:
make build VERSION=0.1.0 COMMIT="$(git rev-parse --short HEAD)" \
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"| Command | Description |
|---|---|
rambo daemon |
Run the monitor (event loop) in the foreground |
rambo top |
One-shot view of system stats and top RAM consumers |
rambo stats |
Live dashboard (Ctrl+C to exit) |
rambo threshold set/status |
Set memory thresholds and alert levels |
rambo protect add/remove/list |
Protect processes from being killed |
rambo oom-protect |
Privileged helper (via pkexec): set oom_score_adj=-1000 on protected processes |
rambo history |
Show past events and kills with reasons |
rambo clean |
Delete the log file |
rambo daemon --dry-run runs the full decision path — the same watcher
classification, victim scoring, protection/exclusion, cooldown/rate
limiter, pending/deferred kills, suspend targets and resume reasons — but
performs no signals, no cgroup or oom_score_adj writes, no
notifications, and no state/journal writes. The limiter and pending state
are simulated in memory so previews stay accurate, and every line is
prefixed [rambo][dry-run] and worded "would ...". A preview never claims
an action was completed and never claims memory was freed.
Config lives at ~/.config/rambo/config.toml (migrated automatically from the old config.json on first run). Defaults:
protect = ["nvim", "gnome-shell"] # never killed (session/system-critical gate applies too)
expendable = ["steam"] # killed first (score penalty)
[memory]
soft_pct = 90 # memory.high — kernel throttles here (soft)
hard_pct = 96 # graceful SIGTERM line
max_pct = 99 # kernel safety-net limit
reserve_mb = 256 # absolute emergency reserve on MemAvailable (0 = off)
available_warn_pct = 15 # MemAvailable <= 15% of total → warn
available_crit_pct = 5 # MemAvailable <= 5% of total → kill
swap_warn_pct = 50 # swap in use >= 50% → warn (0 = off)
swap_crit_pct = 95 # swap in use >= 95% → kill (0 = off)
velocity_mbps = 0 # MemAvailable decline rate triggering warn (0 = off)
[memory.repeat]
interval = "20s" # repeat cadence while hard/critical is sustained
backoff = 2.0 # exponential backoff per repeat (>= 1)
max = "5m" # cap for the backed-off interval
[memory_pressure]
some_pct = 60 # PSI some → warn when sustained
full_pct = 20 # PSI full → escalate (kill) when sustained
window = "10s"
action = "escalate"
[memory_pressure.repeat]
interval = "20s" # repeat cadence while full/critical is sustained
backoff = 2.0 # exponential backoff per repeat (>= 1)
max = "5m" # cap for the backed-off interval
[temperature]
warning = 85 # log/notify only, never signals
critical = 90 # throttle (SIGSTOP) eligible processes only, never kills
emergency = 95 # throttle + notify; kills only when allow_kill is set
action = "throttle" # retained for compatibility; alone it never arms killing
throttle = true # SIGSTOP eligible heavy processes at throttle_at
throttle_at = 87 # resume 3°C below
allow_kill = false # DISABLED by default; the only thermal kill switch
kill_allowlist = [] # only these names may be thermally killed (when allow_kill)
sensors = [] # empty = auto-detect CPU/GPU sensors
[network]
alert_mbps = 900
action = "notify"
[cpu]
alert_pct = 90
action = "notify"
[disk]
space_alert_pct = 90
io_alert = true
action = "notify"
[kill]
policy = "score" # "score" or "rss"
cooldown = "30s" # min interval between kills
max_per_min = 3 # rate limit
oom_prefer = true # steer the kernel OOM killer toward rambo's picks
oom_protect = true # shield protected processes from the kernel OOM killer
grace = "5s" # wait after SIGTERM for exit before SIGKILL
escalate = true # SIGKILL survivors of SIGTERM (identity-checked)
allowlist = [] # names automatic termination may target
allow_exe = [] # executable paths automatic termination may target
allow_cgroup = [] # cgroup substrings automatic termination may target
allow_unknown = false # escape hatch; false = unclassified processes never killed
[kill.weights]
rss = 0.6 # kill score weights
cpu = 0.3
runtime = 0.1
[battery]
low_pct = 20
action = "suspend" # SIGSTOP heavy jobs at low charge (laptops only)Thermal killing is fail-closed: setting [temperature] action = "kill"
alone does not arm it. A thermal kill happens only when
[temperature] allow_kill = true is set, and even then only processes on
[temperature] kill_allowlist (or an allowlisted cgroup) are eligible.
Likewise, [kill] allowlist, [kill] allow_exe and [kill] allow_cgroup
restrict automatic termination from all sources; [kill] allow_unknown
(default false) must be set to re-enable killing of processes that are
neither session/system-critical nor allowlisted.
rambo threshold set --soft-pct 90 --hard-pct 96 --max-pct 99
rambo threshold set --soft 14 --hard 14.5 --max 15 # GB equivalents
rambo threshold set --temp-kill 90 --net-alert 900 --cpu-alert 90 --disk-alert 90
rambo threshold statusThresholds are relative to total RAM, so the same config works on 16/32/64 GB machines. The soft value must be below the hard value, which must be below the max.
Memory (hard) and PSI (full) are level-triggered: entry emits once, a repeat
re-emits while the critical level is sustained (base repeat.interval,
times repeat.backoff per repeat, capped at repeat.max), and recovery
emits once when pressure returns to normal. Soft/some warnings emit on entry
and recovery only. Memory exits with 1% of total RAM hysteresis; PSI exits
2.0 points below its threshold. Missing [memory.repeat] or
[memory_pressure.repeat] keys fall back to 20s / 2.0 / 5m.
On start, rambo finds its session cgroup and applies the limits as kernel enforcement:
memory.high= soft% — the kernel throttles allocations in the slice at the soft line (it does not guarantee the line is never crossed), and rambo notifies.memory.max= max% — kernel safety net. If rambo's graceful kill is missed, the kernel protects the session.- At the hard line, rambo
SIGTERMsthe best kill candidate (see below).
Limits are applied only to app.slice and session.slice (the desktop slices the user owns). They are restored to max when the daemon stops. On non-systemd systems, or if the cgroups aren't writable, rambo degrades to meminfo-based monitoring.
The cgroup files are in bytes. Rambo writes byte values and refuses to write any limit below 512 MiB / 1 GiB as a safety floor.
The cgroup soft/hard/max levels only see the session cgroup, so rambo layers
system-global signals from /proc/meminfo on top: MemAvailable percentage,
an absolute emergency reserve (reserve_mb), swap usage percentage
(swap_warn_pct/swap_crit_pct, via SwapTotal/SwapFree so zram works),
and the MemAvailable decline rate (velocity_mbps). The combined level is
the max of the cgroup level and the global level — the global signals can
only raise it, never lower it. Systems without swap skip the swap signals,
and a missing MemAvailable key skips the availability/reserve/velocity
signals; neither case can false-trigger.
These are conservative starting points; tune per machine. The reserve is small against a 16 GB desktop but large on a 2 GB VM (lower it there). Velocity is off by default because the decline rate is noisy — enable it explicitly once a baseline for the machine is known.
Killing is fail-closed: automatic termination only targets explicitly
allowlisted workloads, for all sources (memory and thermal). A candidate
must match [kill] allowlist, [kill] allow_exe or [kill] allow_cgroup
(or [temperature] kill_allowlist for thermal events when
[temperature] allow_kill = true). Session/system-critical and
non-allowlisted (unknown) processes are excluded from automatic
termination; [kill] allow_unknown (default false) is the escape hatch
that re-enables killing of unknown processes from non-thermal sources.
Thermal events never kill unknown processes regardless of that flag.
The default score policy ranks eligible candidates by:
score = 0.6·rss_norm + 0.3·cpu_norm + 0.1·runtime_norm
+ 0.2 if expendable
RSS and CPU are normalized against total RAM / 100%; younger processes outrank long-running ones.
A classification gate protects processes from any rambo signal
(SIGTERM/SIGKILL/SIGSTOP) and from oom_score_adj changes: rambo
itself; the session leader; startplasma-wayland/startplasma-x11;
plasmashell; kwin_wayland/kwin_x11; systemd --user;
dbus-daemon/dbus-broker; the active terminal and shell (any process
with a controlling TTY); anything in the user session.slice; plus
system-critical processes (PID 1/2, root-owned, system.slice).
Classification uses executable path, argv, PID + start time,
parent/session, UID and cgroup, not process names alone. Rambo's own
signal safety never depends on oom_score_adj; oom_score_adj only
steers the kernel OOM killer. The protect list and protect_tree
add operator-chosen processes on top of this gate and are never killed.
Cooldowns prevent thrash: kill.cooldown (default 30s) and kill.max_per_min (default 3) — a kill attempt inside the cooldown window notifies "kill throttled" instead of acting.
Set policy = "rss" for the old "biggest process" behavior.
A sent SIGTERM is not a kill. After signalling, the daemon records a
kill_requested entry and verifies asynchronously (the event loop never
blocks): it polls for process exit for kill.grace (default 5s), then —
if the process is still the same process (PID + start time + name) and
kill.escalate is true — sends SIGKILL and re-checks. Only a verified
exit notifies "Killed … (freed X MB)" with a kill history record;
survivors are marked ineffective (skipped by future picks) with a
kill_ineffective record. SIGKILL is never sent to a reused PID whose
identity no longer matches the victim.
The kernel picks OOM victims by oom_score_adj. Rambo cannot replace the kernel OOM killer, but with kill.oom_prefer = true it steers it:
- Preferred victims: each process rambo chooses to kill is marked
oom_score_adj = 1000(the kernel's #1 choice) as soon as rambo signals it, and every process in theexpendablelist is kept marked — so if the kernel OOM-kills anyway, it takes rambo's pick first. Raisingoom_score_adjneeds no privileges. - Protecting processes from the kernel: with
kill.oom_protect = true(default), the daemon asks a root helper (rambo oom-protect, run throughpkexec) to loweroom_score_adjto-1000on session/system-critical infrastructure (session leader,startplasma-wayland/startplasma-x11,plasmashell,kwin_wayland/kwin_x11,systemd --user,dbus-daemon/dbus-broker, processes insession.slice, PID 1/2, root-owned andsystem.sliceprocesses), theprotectlist, and rambo itself — such processes are excluded from the kernel's OOM victim selection. Lowering below 0 requiresCAP_SYS_RESOURCE, which is why this runs as root. See Hardening to install the polkit rule; without it the daemon simply logs a warning and keeps running unprivileged. Rambo's own signal safety never depends on this marking;oom_score_adjonly steers the kernel OOM killer.
Caveat: once a process is marked 1000 it stays marked for the process lifetime (only root, a restart, or the process exiting clears it) — which is why only expendable processes and active kill victims are ever marked. Likewise, a -1000 protection persists until the process exits or root clears it.
The daemon itself stays unprivileged — only a narrow helper runs as root. With kill.oom_protect = true (default), the daemon invokes pkexec rambo oom-protect on start and every 5 minutes (invocations are coalesced and spaced at least 30s apart). That helper sets oom_score_adj = -1000 (excluded from the kernel's OOM victim selection) on session/system-critical infrastructure, the protect list, and rambo itself. Session/system-critical and non-allowlisted processes are never automatically killed regardless: killing additionally requires an [kill] allowlist/allow_exe/allow_cgroup match (or [temperature] kill_allowlist with [temperature] allow_kill = true for thermal). It only writes oom_score_adj; it never kills, so running it as root carries no kill blast radius.
To let the passwordless helper through, install once (as root):
sudo make installThis installs the binary to /usr/bin/rambo, the polkit rule to /usr/share/polkit-1/rules.d/99-rambo.rules, and the systemd user service to /usr/lib/systemd/user/rambo.service. On Arch, makepkg -si (or the AUR) does the same. The rule is machine-wide — every user on the system gets passwordless rambo oom-protect with no per-user step.
systemctl --user enable --now rambo.serviceThe rule matches only the exact installed invocation — program is
/usr/bin/rambo and the full command line is /usr/bin/rambo oom-protect
— and grants nothing else. In particular it never matches by basename, so
a copy at any other path (e.g. /tmp/evil/rambo) gets no passwordless
grant. The daemon additionally only invokes pkexec when it is itself the
installed /usr/bin/rambo binary; a dev checkout, a temporary Go test
binary (/tmp/go-build…/cmd.test), ~/.local/bin/rambo, or any other
path skips the helper with a log line instead of prompting.
Run the canonical binary. The polkit rule, the daemon's path guard and the systemd unit all agree on exactly
/usr/bin/rambo. A user-local copy is dangerous because~/.local/binusually precedes/usr/bininPATH, and a unit in~/.config/systemd/user/rambo.serviceoverrides the packaged/usr/lib/systemd/user/rambo.service— either silently runs a stale or dev build that the rule does not grant (protection off, or an auth prompt).make installwarns when either shadow exists; remove it soramboresolves to/usr/bin/rambo.
Verify protected processes are now -1000 (rambo itself may show up as the daemon):
cat /proc/$(pgrep -x kwin_wayland)/oom_score_adj # → -1000
cat /proc/$(pgrep -x rambo)/oom_score_adj # → -1000Without the rule, the daemon logs oom-protect: helper failed and keeps running — kernel protection is simply off. Protection persists for each protected process's lifetime and is re-applied on the 5-minute cadence to catch newly started ones.
rambo protect add --name nvim
rambo protect remove --name nvim
rambo protect listThe old rambo whitelist command still works as an alias.
protect adds to the never-kill gate on top of the built-in
session/system-critical set; expendable only reorders eligible,
allowlisted candidates. Neither list bypasses the allowlist requirement:
a process is automatically killed only if it matches [kill] allowlist,
[kill] allow_exe or [kill] allow_cgroup (thermal additionally needs
[temperature] allow_kill = true plus [temperature] kill_allowlist),
or the operator sets [kill] allow_unknown = true.
Events and kills are recorded to ~/.local/state/rambo/state.json (last 500):
rambo history
rambo history --source temperature --n 50A user service template is included at systemd/rambo.service. It is installed automatically to /usr/lib/systemd/user/rambo.service by sudo make install (or the package); enable it with:
systemctl --user enable --now rambo.serviceFor a manual install without make install, copy it to your user unit dir:
mkdir -p ~/.config/systemd/user
cp systemd/rambo.service ~/.config/systemd/user/rambo.service
systemctl --user daemon-reload
systemctl --user enable --now rambo.serviceThe daemon prints a status line each second (visible with journalctl --user -u rambo -f) and restores cgroup limits on a clean stop.
~/.local/share/rambo/rambo.log records kills, and desktop notifications fall back to it when no notification daemon is available. Delete it with rambo clean.
- Linux-only; reads kernel data from cgroup v2, hwmon,
/proc, and netlink. - The hard action sends
SIGTERM, verifies the exit, and escalates toSIGKILLwhenkill.escalateis true. The kernel safety net maySIGKILLif the max limit is truly reached. - Battery protection is dormant on desktops (no
BAT*inpower_supply). - Suspended processes are tracked by identity (PID + start time + name) with
a set of suspend reasons (battery, thermal): a process suspended for
both stays stopped until every reason clears, so battery recovery never
resumes a process thermal throttling still needs stopped. Battery
recovery resumes only battery suspensions, thermal recovery only thermal
ones; a reused PID is never resumed, and processes rambo killed are
never resumed. Every change to the suspended set is journaled to
~/.local/state/rambo/suspended.json(atomic write) before SIGSTOP is delivered, and the daemon reconciles that journal at startup — resuming only identities that still validate — so a crash no longer leaves processes stopped forever. A journal write failure is logged while the daemon keeps running from memory (a later crash may then strand processes), and processes stopped by anyone other than rambo are never resumed by it. - A config reload (
SIGHUP) preserves runtime state — suspended processes, the kill cooldown/rate window, the deferred kill, and the ineffective set — so a reload never strands a suspended process. The daemon resumes any still-suspended processes on shutdown, and after a kernel OOM event it immediately re-evaluates pending/suspended state before acting.