NAME
Langertha::Raider::Loadguard - Internal gate that refuses heavy shell commands while the host is out of room
VERSION
version 0.504
SYNOPSIS
my $guard = Langertha::Raider::Loadguard->new;
my $why = $guard->check('prove -lr t/'); # undef: let it run
# as the validator of the bash tool: true to run, else the reason
validator => sub { $guard->check( $_[0] ) // 1 },
DESCRIPTION
Internal module. Its interface may change without notice.
A port of the mechanism of the loadguard plugin for Claude Code (the C hook of ~/dev/loadguard) into raider, so that a raider cannot take down the host with the commands it runs, and tells the model why and what to do instead. It is a gate in front of bash; it refuses, it never queues or rewrites. The decisions are those of the plugin and use its environment variables, so one setting serves both.
Which commands are heavy. The command string is split into simple commands (at ; & | ( ), backticks and newlines, respecting quotes, comments, redirections and here-documents); the first one that matches is heavy: prove; make test; dzil test|build|release; cpanm; perlbench; docker|podman build|run; cargo build|test; npm test; claude -p|--print|--bg; codex exec|e|review. Before the command word VAR=x, nice, ionice, nohup, timeout, env, stdbuf, setsid, xargs, time, exec with their options and numbers, and shell keywords such as if and then are skipped. A script run by an interpreter (perl /usr/bin/prove) counts as the script. This is no shell parser: what it cannot tell is light (bash -c 'prove', sudo prove, find -exec prove). A light command is never refused -- the model must always be able to look at what is going on.
When a heavy command is refused ("check"): when the host is under memory pressure -- the full avg10 figure of /proc/pressure/memory at or above LOADGUARD_PSI_FULL (default 10 %), or swap used at or above LOADGUARD_SWAP_USED (default 90 %) -- or when all heavy slots are taken. The slots are LOADGUARD_HEAVY_SLOTS (default max(1, nproc/2)); a slot is held by the topmost running heavy process under any running raider process (any raider of the user, found in /proc; a claude -p or codex exec holds none). Under pressure the processes are not scanned at all: reading the memory of others can stall on swap, and a slow scan would let the heavy command through. The reason names the figures and gives the advice that fits: wait; run a single test file when the slots are the problem (never under pressure, when any run adds memory); start no new agent sessions.
Fail-open. No /proc/pressure/memory, no swap, an unreadable /proc: that signal reports no pressure. LOADGUARD_THROTTLE=0 (exactly) turns the gate off.
Confinement ("confine_command"): off by default. With LOADGUARD_CONFINE=1 the raider starts itself again inside a transient systemd user scope with memory limits (LOADGUARD_MEMORY_HIGH 30, LOADGUARD_MEMORY_MAX 40, LOADGUARD_MEMORY_SWAP_MAX 10, in percent of the memory of the host; LOADGUARD_CPU_WEIGHT 50), so a runaway command is killed in the scope instead of taking the machine. Without a user manager of systemd with the memory controller it does nothing.
Every path into /proc goes through "proc_dir", so the tests give it a tree of their own and create no load.
proc_dir
Where pressure/memory, meminfo and the process directories are read. Default /proc; the tests point it at a directory they built.
env
The environment the LOADGUARD_* variables are read from. Default %ENV.
cgroup_dir
Where the cgroup v2 tree is, for "confine_command". Default /sys/fs/cgroup.
enabled
False when LOADGUARD_THROTTLE is exactly 0.
limits
my $limits = $guard->limits; # { psi_full => 10, swap_used => 90, slots => 4 }
The thresholds from LOADGUARD_PSI_FULL, LOADGUARD_SWAP_USED (1 to 100) and LOADGUARD_HEAVY_SLOTS (1 to 4096). A value that is no whole number (a trailing % is fine) or out of range counts as unset.
default_slots
max(1, nproc/2): the CPUs this process may run on (self/status Cpus_allowed_list, else the processor lines of cpuinfo, else 1).
pressure
my $p = $guard->pressure; # { full => 0.12, swap => 51 }
The memory signals: full is the avg10 of the full line of pressure/memory in percent, swap the share of swap in use in whole percent. Each is undef when the host does not say (the gate then takes it as no pressure).
heavy
my $label = $guard->heavy('cd /x && dzil test'); # 'dzil test'
my ( $label, $kind ) = $guard->heavy($command);
The label of the first heavy simple command in the command string (prove, make test, podman run, claude -p ...) and its kind (prove, suite, build or agent), or nothing for a light command.
slot_holders
my @holders = $guard->slot_holders; # ( { pid => 123, label => 'prove', cwd => '/x' }, ... )
The running heavy processes that hold a slot: the topmost heavy process of a chain, below a running raider. A bash -c STRING counts as its string. Scans /proc; an unreadable process is skipped.
check
my $reason = $guard->check($command);
Undef when the command may run: the gate is off, it is light, or there is room. Otherwise the reason it is refused, to be handed to the model: the heavy command found, the figures and the advice. The text continues the words Command that the bash tool puts in front of a reason.
validator
my $code = $guard->validator; # for Langertha::Raider::BashTools
A code reference for the validator of Langertha::Raider::BashTools: it gets the command and returns 1 to run it or the reason of "check".
confine_command
exec @cmd if @cmd = $guard->confine_command( command => [ $guard->self_command ] );
The command line that runs command inside a transient systemd user scope with memory limits, or nothing: when LOADGUARD_CONFINE is not 1, the process already is confined (LOADGUARD_CONFINED set), there is no systemd-run or no user manager ($XDG_RUNTIME_DIR/bus), or the memory controller is not delegated to the user manager. The caller sets LOADGUARD_CONFINED=1 and execs it; if the exec fails it goes on unconfined. For the raider itself the command is "self_command". Run from a script, options given to perl itself on the command line (-I) are not carried over; PERL5LIB is. The standalone binary carries its modules and starts as it is.
self_command
my @command = $guard->self_command;
The command line that starts this raider again: the packed binary itself (there is no perl of its own inside it, see Langertha::Raider::Binary), or this perl with the script and its arguments.
SEE ALSO
SUPPORT
Issues
Please report bugs and feature requests at https://github.com/Getty/langertha-raider/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.