Environment Variables
This page lists every HORUS_* variable that shipped, non-test code reads,
and every one HORUS sets for a process it starts — a node under
horus launch, a Python node under horus run, an exec driver, a CLI plugin.
The ones you would set deliberately come first, grouped by area; the internal
and test-only ones are at the bottom so you can recognise one if you see one.
There is deliberately no count in this sentence. The version of this page that
carried one opened with "HORUS reads 47 HORUS_* variables" while its own
tables listed 43 distinct names.
What this page does not cover
The integration-test tree carries its own environment surface — names of the
shape HORUS_<SUITE>_CHILD, _TOPIC, _ROLE, _COUNT, whose entire job is to
let a test coordinate with a process it spawns. They outnumber the real ones: of
the 199 distinct HORUS_* string literals in the repository outside target/,
94 appear somewhere under a src/ directory and 105 appear only in test files.
None of those 105 is reachable from an installed HORUS, none has a default worth
knowing, and listing them would bury the page. They are not here, and
horus_manager/tests/docs_parity.rs:150-154 records the same reason for keeping
them out of the check that guards it.
Two automated checks stand behind the rest, and it is worth knowing what each
one can and cannot see. Both run in the contract-live job of
.github/workflows/docs-contract.yml, on every pull request, against a pinned
horus-docs commit.
docs_contract::every_environment_variable_is_in_the_reference_page builds a
surface from four scans and fails when a name in it is absent from this page:
- Rust files under a
src/path, where the name is a literal directly insideenv::var(,env::var_os(,env::set_var(,.env(orenv.insert(— matches inside a#[cfg(test)]module are dropped; .cpp,.cc,.hppand.houtsidetests/, forgetenv(andsecure_getenv(;.pyoutsidetests/, foros.environ.get(,os.environ[andos.getenv(;install.shanduninstall.sh, for any$HORUS_*the script references and never assigns.
The first scan is the one doing the work. The C++ and Python scans match nothing
in this repository today — no shipped .cpp or .hpp calls getenv at all, and
no shipped .py file names a HORUS_* variable — and the non-vacuity guard at
docs_contract.rs:908-926 probes only three of the four, so if either of them
ever did match and then stopped, nothing would say so.
Inside the Rust scan, four shapes are invisible, and all four are in use:
- a read through a helper —
csv_env("HORUS_NET_EXPORT_STREAM")athorus_net/src/config.rs:257; - a read through a constant —
env::var(LEVEL_ENV)athorus_core/src/core/hlog.rs:123, whereLEVEL_ENVisHORUS_LOG_LEVEL.HORUS_NO_PROXY_NOTICEandHORUS_SHELL_PROXYreach the code the same way, through the constants athorus_manager/src/commands/proxy.rs:47and:55; - a name built at runtime —
format!("HORUS_REAL_{}", tool_name.to_uppercase())athorus_manager/src/commands/proxy.rs:417; - a read in generated code —
HORUS_MSGS_FFIandHORUS_MSGS_FFI_BUILDare read by Python that lives inside a Rust string literal (horus_manager/src/msgspec/emit_python.rs:140and:180), which the Rust scan does not parse and the Python scan never opens.
HORUS_NET_EXPORT_STREAM went undocumented behind exactly the first of those. If
you add a variable through a helper, a constant, a format! or a generated file,
no check will remind you; add it here by hand.
The second check, docs_manifest::documented_env_vars_are_read, runs the other
way: a HORUS_* name written on any documentation page must appear somewhere in
shipped source — every .rs, .toml, .sh, .h, .hpp, .hh, .cpp, .cc,
.py and .yml file outside target/ and outside every directory named
tests. A variable invented in prose therefore fails the build. Three things
soften it, and this page depends on all three:
- a heading containing internal or test-only exempts the section under it, which is why a genuinely test-only variable has to live in the last section here rather than in a table above;
- a line that disclaims a variable — "nothing reads", "there is no", "does not" — is skipped, which is what lets the Set but not read table below name variables and then say nothing reads them;
- a name matching a prefix the source builds at runtime is accepted, so
HORUS_PARAM_MAX_SPEEDis covered by theformat!("HORUS_PARAM_{}", …)incommands/launch.rsandHORUS_REAL_CARGOby the one incommands/proxy.rs.
It matches on the name appearing, not on a read, so it catches invention rather than staleness.
Variables that relax a check
Every one of these has a safe default and a reason someone turns it off. They are collected here so an operator hardening a deployment has one list to audit; each also appears in its subsystem's table below with the full detail.
| Variable | What it gives up |
|---|---|
HORUS_NET_ALLOW_PEERS | Source-address admission for inbound UDP. any accepts the public internet |
HORUS_NET_IMPORT | Which remote topics may be written into local shared memory. The data plane is unauthenticated, so this is reach reduction, not authorization |
HORUS_ESTOP_REMOTE | Whether an authenticated remote emergency stop is acted on at all |
HORUS_TRUST_PROXY | Ignoring X-Forwarded-For / X-Real-IP, which is what stops a remote client spoofing an address past per-IP rate limiting |
HORUS_ALLOW_LOCAL_PLUGINS | The rule that cloning a repository is not enough to run its code |
HORUS_AUTH_ALLOW_MISSING_STATE | The OAuth CSRF check during horus login, when the registry returns no state |
HORUS_AUTO_INSTALL | The prompt before sudo apt install during a C++ build |
HORUS_INSTALL_BRANCH | Installing from a release tag, in favour of an arbitrary branch |
HORUS_ESTOP_KEY is the opposite case: it is the only variable here that adds
a check, and until it is set every remote emergency stop is rejected.
Logging
| Variable | Default | Effect |
|---|---|---|
HORUS_LOG_FILE | unset | Exactly 1 or true starts the file drain. Without it nothing is written to disk and the other HORUS_LOG_* variables have no effect |
HORUS_LOG_DIR | .horus/logs | Directory for rotated log files |
HORUS_LOG_MAX_SIZE | 10485760 (10 MB) | Rotate a log file once it exceeds this many bytes |
HORUS_LOG_MAX_FILES | 5 | How many rotated files to keep |
HORUS_LOG_BUFFER_SIZE | 5000 | Entries held in the in-memory ring the monitor reads. Clamped to 100–50000 — a value outside that range is silently adjusted, not rejected |
HORUS_ERROR_BUFFER_SIZE | 500 | Entries in the dedicated error ring, which survives a flood of ordinary logs. Clamped to 50–5000 |
HORUS_REMOTE_LOG_SIZE | 2000 | Entries in the buffer holding logs received from other machines. Clamped to 100–50000 |
HORUS_LOG_LEVEL | debug | Highest level hlog! will emit. Accepts off/none/silent, error/err, warn/warning, info, debug/trace/all, case-insensitively. An unrecognised value warns on stderr and falls back to the default. The default is debug, so nothing is suppressed until you set this — a production image should set info or warn to actually save the per-line cost |
The error buffer is separate on purpose: a node logging at high rate can evict
its own errors from the main buffer. horus log --level warn and
--level error read the error ring instead of the main one, which is the
longer retention (horus_manager/src/commands/log.rs:201-206); anything below
warn reads the main ring. Logs replicated from peers are merged into the same
output from a third buffer.
HORUS_LOG_LEVEL is read once per process and latched, on the first hlog!.
The filter lives in the macro so a suppressed line never evaluates its
arguments. There is no HORUS_LOG; nothing reads that name.
Networking
Only meaningful with the net capability enabled — see
Configuration.
| Variable | Default | Effect |
|---|---|---|
HORUS_NET_ENABLED | enabled | Opt-out only. Once the binary is built with the net capability, the umbrella crate registers an auto-wire hook and scheduler.run() starts the replicator; 0 or false (case-insensitive) turns it off. Any other value, including true, leaves it on |
HORUS_NO_NETWORK | unset | 1 or true disables replication for one run — wins over the above, because the replicator refuses to start on a config with enabled = false however it was wired |
HORUS_NET_PORT | 9100 | UDP port the replicator binds. A value that does not parse as a u16 falls back to the default |
HORUS_NET_MULTICAST | 224.0.69.72 | Discovery multicast group |
HORUS_NET_PEER | empty | Comma-separated peers to unicast directly, skipping multicast discovery |
HORUS_NET_ALLOW_PEERS | 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 100.64.0.0/10 | Relaxes a check. Which source addresses inbound datagrams may come from. any, *, all or 0.0.0.0/0 accepts every routable host and prints a warning saying so. Otherwise a comma-separated list of addresses and CIDR blocks; an unparseable entry warns and is skipped, and if every entry is unparseable the default list is used rather than any |
HORUS_NET_SECRET | unset | Shared token peers must match. Peer filtering, not authentication — the source says so at the field. Used verbatim; [network] secret in horus.toml is passed through unchanged, with no env: indirection |
HORUS_NET_IMPORT | auto | Relaxes a check. Which remote topics may be imported. auto (also unset or empty) admits any topic this process subscribes to; deny/none/off/false admits none; anything else is a comma-separated allowlist of topic patterns. A spec that parses to no patterns becomes deny, not auto |
HORUS_NET_DENY_EXPORT | empty | Comma-separated topic patterns that must never leave this machine |
HORUS_NET_EXPORT_STREAM | empty | Comma-separated topic patterns (globs allowed) exported as a full stream rather than one sample per export tick. See below |
HORUS_NET_OPTIMIZERS | empty | Comma-separated: fusion, spatial, predict. An unknown name warns and installs nothing. delta is recognised but refused — it encodes with no decoder on the receive side, so naming it warns and installs nothing rather than corrupting replicated data |
HORUS_NET_HEARTBEAT_MS | 50 (200 under WSL2) | How often the safety heartbeat is sent to matched peers, for link-loss detection. It does not affect discovery announcements |
HORUS_NET_MISSED_THRESHOLD | 3 (5 under WSL2) | Missed heartbeats before the link counts as lost |
HORUS_NET_ON_LINK_LOST | warn | Action on link loss: warn, safe_state (alias safestate), stop. Anything else silently means warn |
HORUS_NET_DISCOVERY_MISSES | 3 (6 under WSL2) | Missed announcements before a peer is considered gone. Announcements go out on a fixed 1-second interval that no setting changes, so the default is a 3-second peer timeout (6 s under WSL2) |
HORUS_NET_LOG_LEVEL | warn | Which logs replicate to peers: none/off, error, warn/warning, info, debug/trace/all. An unrecognised value means warn |
WSL2 is detected by looking for microsoft in /proc/version, because its
virtualised network adapter adds tens to hundreds of milliseconds of UDP jitter
and the native timings produce false link-lost reports.
HORUS_NET_EXPORT_STREAM, and the 20 Hz you did not ask for
Outbound replication is driven from the replicator's timer, which fires every
50 ms (horus_net/src/replicator.rs:44). The default export takes the newest
sample on each tick and leaves the rest, so every replicated topic crosses the
network at roughly 20 Hz however fast its publisher runs. That is correct for
state — a pose, a battery level — and wrong for a measurement stream, where the
receiver integrates every sample. A 500 Hz odometry stream crossed the LAN at
20 Hz and looked, at the far end, exactly like a 20 Hz odometry stream, and
nothing counted or said so (horus_net/src/replicator.rs:904-926).
List the streaming topics and every message crosses:
HORUS_NET_EXPORT_STREAM='odom,joint_states,lidar.*' horus run
A streaming topic contributes at most 64 messages to one export tick
(MAX_EXPORT_BATCH, replicator.rs:57); the reader resumes from its own cursor,
so the remainder goes out on the next tick rather than being dropped. At the
50 ms timer that is 1280 messages per second per topic sustained.
Whatever a topic is set to, what it drops is now counted and reported; the
warning that names the drops also names this variable
(horus_net/src/shm_reader.rs:140).
This is the one networking variable with no horus.toml equivalent. The
[network] table has fields enabled, import, deny_export, secret,
optimize and safety and no export_stream
(horus_manager/src/manifest.rs:276-303), and the per-topic export_sampling
override that would express the same thing
(horus_net/src/config.rs:217) lives in a map that nothing outside the two test
suites ever populates (horus_net/src/config.rs:44, :261). The environment
variable is the only route there is.
How [network] in horus.toml reaches the runtime
horus run translates the [network] table into these variables for the child
process, in horus_manager/src/commands/run/mod.rs:79-142:
HORUS_NO_NETWORK, HORUS_NET_SECRET, HORUS_NET_DENY_EXPORT,
HORUS_NET_OPTIMIZERS, HORUS_NET_IMPORT, HORUS_NET_HEARTBEAT_MS,
HORUS_NET_MISSED_THRESHOLD and HORUS_NET_ON_LINK_LOST. A variable already
set in your environment wins — an explicit HORUS_NET_*=… on the command line
is never overridden by a project file.
Two consequences worth knowing before you rely on the manifest. The translation
runs on one path only, execute_multiple_files, which is reached when horus run is given more than one file or a glob that matches more than one
(mod.rs:236-238 and :834); a single file, a directory and a horus.toml run
get none of it. And horus launch performs no such translation at all —
horus_manager/src/commands/launch.rs sets no HORUS_NET_* name. Where the
manifest route does not reach, the environment variable does.
Safety
| Variable | Default | Effect |
|---|---|---|
HORUS_ESTOP_REMOTE | warn | Relaxes a check. What a remote emergency stop does locally. off, ignore, 0 or false disable it; anything else warns. Trimmed and lowercased before matching |
HORUS_ESTOP_KEY | unset | HMAC-SHA256 key authenticating the networked e-stop. 64 hex characters are used as raw 32-byte key material; anything else is stretched with PBKDF2-HMAC-SHA256 at 600,000 iterations. An empty or all-whitespace value counts as unset. While unset, every remote e-stop packet is rejected — see Authenticating the networked e-stop |
Note the default on HORUS_ESTOP_REMOTE: an unrecognised value warns rather
than disabling, so a typo cannot silently switch off remote e-stop. The key is
read once per process and cached, so the KDF cost is paid at first use and never
on the packet path. The salt is fixed and domain-separated; there is nowhere to
put a per-deployment one when both ends must derive the same bytes from a bare
environment variable.
Runtime
| Variable | Default | Effect |
|---|---|---|
HORUS_SIM_MODE | unset | Runs drivers in simulation. Any value except empty, 0 or false enables it — presence alone used to be enough, so HORUS_SIM_MODE=0 turned simulation on and a deploy script forcing real hardware got inert stub nodes. Set by horus run --sim |
HORUS_SIM_TARGETS | unset | Comma-separated driver names to simulate, when only some should be. Read only when HORUS_SIM_MODE is on |
HORUS_DRIVERS | unset | Comma-separated driver list. Replaces horus.toml's when non-empty; an empty list falls back to the manifest. Set by horus run --drivers |
HORUS_ENABLE | unset | Comma-separated capabilities. net and network are both accepted for networking. Replaces horus.toml's capability list when non-empty. Set by horus run --enable, and by --net, which appends net |
HORUS_TELEMETRY_ENDPOINT | unset | Where the scheduler exports telemetry — a UDP or file URI, e.g. udp://localhost:9999. Ignored when the scheduler config already names an endpoint, and an empty value is ignored |
HORUS_NAMESPACE | default | Isolates shared memory. Two processes with different namespaces cannot see each other's topics — the equivalent of a ROS 2 domain ID. Sanitised, and a value that sanitises to nothing but underscores is discarded. Read once per process and cached, so changing it mid-run has no effect. A test binary that has not set it gets a private namespace derived from its cargo target directory instead |
HORUS_NODE_NAME | Scheduler | Names the scheduler in horus node list and in the presence file it writes under scheduler/. Unset or empty leaves the literal name Scheduler — the binary name is never consulted; Scheduler::new().name("...") overrides both |
HORUS_RT_CORES | unset | Comma-separated core IDs to pin real-time threads to, e.g. 2,3. Ignored when the scheduler already has .cores(&[..]), so it is an override for deployment rather than a default. Entries that do not parse are dropped |
HORUS_RECORD_SESSION | unset | A non-empty value enables recording for the run. Set by horus run --record <session>; setting it by hand has the same effect |
HORUS_RT_WAIT | sleep | How the RT tick loop waits out a period. spin opts into a busy-wait: median wake jitter improves to about 100 ns, but under a real RT policy Linux RT bandwidth control dequeues the thread for ~50 ms once its share is spent, and the loop warns about that unconditionally on every boot. An unrecognised value warns and keeps the sleeping wait |
HORUS_RT_SPIN_GUARD_US | 20 | Microseconds of spin at the end of a sleep-based wait, to absorb timer slack. 0 disables the spin entirely. Capped at 1/16 of the tick period regardless of what you set, so the loop's unconditional CPU draw stays at or below 6.25 % at every rate |
HORUS_RT_ALLOW_CORE_SHARING | unset | Any value except empty, 0 or false — matched case-sensitively, unlike the flags above — downgrades a refusal to share one core between RT chains into a warning. For the deliberate case: two light chains, one core, and a measurement behind the decision |
HORUS_SHM_PREFAULT | enabled | Pre-fault a shared-memory mapping at attach so the first store in a control loop does not take a page fault. 0/false/off/no disables; 1/true/on/yes enables; an unrecognised value keeps the default rather than meaning off. Read once per process and cached |
HORUS_SHM_PREFAULT_MAX_BYTES | 67108864 (64 MiB) | Upper bound on how much of a mapping is pre-faulted. A value that does not parse as a number falls back to the default |
HORUS_SHM_MLOCK | disabled | 1/true/on/yes locks shared-memory pages resident so they cannot be swapped. Off by default because it needs the memlock limit raised, and locking trades swappability for a likelier OOM kill. It is mlock on the region, not mlockall, so ordinary heap allocations still fail the way they always did |
Parameters
HORUS_PARAM_<NAME> sets the runtime parameter <name>, lowercased:
HORUS_PARAM_MAX_SPEED=2.5 HORUS_PARAM_DEBUG=true horus run
reaches your node as self.params.get::<f64>("max_speed") and
get::<bool>("debug").
| Variable | Default | Effect |
|---|---|---|
HORUS_PARAM_<NAME> | unset | Sets parameter <name> (lowercased). Values parse as integer, then float, then bool, then string — integer first, so 42 is the integer 42 and not 42.0. The bool match is exactly true or false; True is the string "True" |
This is the highest-precedence layer: it beats .horus/config/params.yaml,
which beats the built-in defaults. Each layer overrides individual keys of the
one below it rather than replacing the whole map — before that was true, a
single HORUS_PARAM_CAMERA_FPS=60 from a launch file suppressed the entire
built-in set, including emergency_stop_distance and max_speed.
horus launch and the exec driver both build these names from your parameter
keys, uppercasing them; horus launch also replaces - with _, so a launch
key max-speed arrives as HORUS_PARAM_MAX_SPEED. The exec driver does not,
so a driver parameter with a hyphen in its key produces a name a shell cannot
export. A variable set by hand overrides a launch file for that run.
CLI and toolchain
| Variable | Default | Effect |
|---|---|---|
HORUS_ASSUME_YES | unset | 1, true or yes covers three prompts. In horus new it suppresses them and takes the documented defaults; in horus setup-rt and horus env --init it answers yes. horus new -y/--yes sets it for you; the other two commands have no such flag — setup-rt takes only --check and --undo, env only --init and --uninstall — so the variable is the only way to accept setup-rt's kernel-install and limits-file prompts without a terminal |
HORUS_AUTO_INSTALL | unset | Relaxes a check. 1, true or yes installs missing C++ system dependencies with sudo apt without asking. Without it, a pcl = "1.13" line in [dependencies] prompts before touching the machine |
HORUS_CPP_LINK | static | How the HORUS C++ runtime is linked into a node binary: static, or shared (dynamic is accepted as a synonym). Static puts the whole runtime in every binary — 78 MB in Debug and 2.0 MB stripped in Release, per binary — so a robot running a dozen nodes pays it a dozen times. shared links libhorus_cpp.so: measured on the generated template, Release, x86-64, 2,074,176 bytes static against 47,392 bytes plus one 8.9 MB shared library, so it pays from the second node. Overrides [cpp] link in horus.toml for one build; an unrecognised value is an error rather than a silent fall back to static |
HORUS_VERBOSE | unset | Any value, including empty, shows the full compiler and CMake output during a C++ build. Debug-level logging has the same effect |
HORUS_SOURCE_DIR | unset | Path to a HORUS checkout, used by horus msg to resolve message types from the linked message crates of that tree and nothing else |
HORUS_SOURCE | unset | Path to a HORUS checkout, used to resolve the framework itself when horus run builds a single Rust file. Distinct from HORUS_SOURCE_DIR, which resolves message definitions — a name that is a prefix of another is exactly the kind of pair a substring check treats as one variable, and this one went undocumented behind HORUS_SOURCE_DIR for that reason. Ignored unless the path exists and contains horus/Cargo.toml |
HORUS_REGISTRY_URL | https://api.horusrobotics.dev | Package registry, for self-hosting or offline mirrors |
HORUS_PLUGIN_REGISTRY_URL | https://plugins.horusrobotics.dev/api/v1 | Plugin registry |
HORUS_TRUST_PROXY | false | Relaxes a check. 1, true or yes makes horus-monitor trust an HTTP proxy's X-Forwarded-For / X-Real-IP headers. Off by default so a remote client cannot spoof an address and bypass per-IP rate limiting. Set it only when the monitor really is behind a proxy that rewrites those headers |
HORUS_ALLOW_LOCAL_PLUGINS | false | Relaxes a check. 1 or true allows horus monitor to run the project-local .horus/bin/horus-monitor, and warns when it does. Off by default — that binary is not trust-checked and gets none of the plugin sandbox, and cloning a repo must not be enough to run its code |
HORUS_AUTH_ALLOW_MISSING_STATE | false | Relaxes a check. 1 or true lets horus login complete when the registry returns no OAuth state parameter, printing a warning that the login was not CSRF-verified. Absent state is fatal by default because the attacker picks the branch: "warn and continue when absent" is defeated by omitting the parameter. A mismatched state is never accepted, with or without this variable. This is the documented opt-out for the window before the registry echoes state back |
HORUS_NO_SHELL_INTEGRATION | unset | Any non-empty value makes the installer skip horus env --init, so no shell rc file is touched and the cargo/pip/pip3/cmake/conan/vcpkg proxies are not installed. install.ps1 reads it the same way and leaves the PowerShell profile alone |
HORUS_SHELL_PROXY | unset | Set for you by the shell functions horus env installs, carrying the name you actually typed (pip3 rather than pip) so the notice names the right command. Read, not written, by hand |
HORUS_NO_PROXY_NOTICE | unset | Any non-empty value silences the first-use notice those shell proxies print |
HORUS_REAL_<TOOL> | unset | Absolute path to the real cargo, pip, cmake, conan or vcpkg, so the proxy does not have to find it on PATH. HORUS_REAL_CARGO, HORUS_REAL_PIP, HORUS_REAL_CMAKE, HORUS_REAL_CONAN, HORUS_REAL_VCPKG — one per tool, uppercased, and there is no HORUS_REAL_PIP3 because the pip3 function delegates to horus pip. A path that does not exist is ignored and the PATH search runs as usual. Nothing in HORUS sets these; the source comment at proxy.rs:416 saying horus env.sh does is stale, and the shell functions set only HORUS_SHELL_PROXY |
HORUS_NET | unset | 1 or true reports networking as enabled. horus doctor prints "use --net or HORUS_NET=1 to enable". horus doctor is the only reader, so setting it by hand only changes what doctor says. See below for what actually builds a networked binary |
HORUS_MSGS_FFI | unset | Path to a prebuilt generated-message library. .horus/generated/python/msgs.py hands it to ctypes.CDLL verbatim instead of searching the generated crate's target/ for a debug/release build |
HORUS_MSGS_FFI_BUILD | unset | 0, no, off or never (trimmed and lowercased) makes msgs.py refuse the one-off cargo build of the generated message crate it would otherwise run on the first publisher or subscriber, raising MessageLibraryError instead. Reading and constructing message classes needs neither the library nor the build |
HORUS_STRICT_VERSION | unset | 1, true or yes turns the CLI-vs-installed version mismatch from a warning into a hard failure. For CI, which wants a mismatched toolchain to stop the job. The default is a warning on purpose — the unconditional failure it replaced printed a remedy that could not clear it, and left a whole release cohort unable to run horus new |
HORUS_NET on its own does not build a networked binary, and neither does the
capability list on its own. horus run --net sets HORUS_NET=1 and appends
net to the capability list, which is written to HORUS_ENABLE
(main.rs:2534-2542). The net capability deliberately contributes no Cargo
feature to your own crate — enable_to_features returns an empty list for it,
because your crate has no such feature and passing it produces "the package does
not contain this feature: net". What turns it on is cargo_gen, which reads
HORUS_ENABLE (or the top-level enable = [...] key in horus.toml) itself and
adds net to the horus dependency in the generated manifest
(horus_manager/src/cargo_gen.rs:1025-1037).
Installer
Read by install.sh and by install.ps1, which takes the same six names.
| Variable | Default | Effect |
|---|---|---|
HORUS_VERSION | unset | Install exactly that release — binary and source, both from that one tag. Accepted with or without the leading v, so 0.4.0 and v0.4.0 are the same request. This is how you install a specific version, reinstall the current one, or roll back. See the callout below: HORUS also sets this name for plugins |
HORUS_BUILD_FROM_SOURCE | unset | Exactly 1 makes the installer skip the prebuilt binary and compile the source at the resolved tag — the same tree the binary would have come from, not whatever is on main. The installer names it in its own error messages: when neither sha256sum nor shasum is available to verify a download, when the download fails, and when the installed binary will not run because of a glibc floor. It is the documented escape hatch from a failed install |
HORUS_INSTALL_BRANCH | unset | Relaxes a check. Developer escape hatch: build from source at that branch. Implies HORUS_BUILD_FROM_SOURCE=1 — a branch has no matching release binary, and pairing one with a branch tree is exactly the version skew this variable used to manufacture. It no longer changes only the clone; it forces the whole install to come from that branch. The installer refuses a value that is not a usable git ref. To pin a release, use HORUS_VERSION |
HORUS_LOCAL_SOURCE | unset | Path to a HORUS source tree already on disk. The installer builds that tree instead of cloning — no network fetch for the source, which is what an air-gapped robot needs. It is symlinked into the cache rather than copied, so horus clean -a removes the link and never the tree behind it. Refused if it is not a directory, or if it lacks either horus/Cargo.toml or horus_core/Cargo.toml (the message names only the first). Implies a source build: a local tree has no release tag, so there is no matching binary to pair with it |
HORUS_PREFIX | unset | Install root override. The binary goes to $HORUS_PREFIX/bin, and $HORUS_PREFIX replaces ~/.horus for the state files and the source cache. Also how a root install says where it means to go: running as root with no SUDO_USER and no HORUS_PREFIX is refused rather than silently installing everything under /root. Pass the same value to uninstall.sh, which cannot otherwise find the install root or the manifest |
HORUS_NO_SHELL_INTEGRATION | unset | As above: skip horus env --init and touch no shell rc file |
HORUS_PREFIX is read by the CLI as well as by the installer, and in two places.
horus run searches $HORUS_PREFIX/cache for the installer-cached
horus@<version> tree before ~/.cache/horus and ~/.horus/cache
(horus_manager/src/commands/run/run_rust.rs:1023-1042), so a prefix install can
build a single Rust file without you exporting HORUS_SOURCE by hand. And the
version gate resolves its state root from the same variable
(horus_manager/src/version.rs:63-76, :106-112), so installed_version and
install_manifest.toml are read from under the prefix.
install.sh has not caught up with either. A prefix install still ends by
telling you to export HORUS_SOURCE=… and by saying "the version gate is
inactive for a prefix install: version.rs looks in ~/.horus"
(install.sh:1108-1126). Neither statement is true of the Rust code cited
above. Follow the code, not the epilogue.
These are read from the installer's environment, so with a pipe they belong
on the right-hand side — HORUS_VERSION=v0.4.0 curl ... | bash puts the
variable in curl's environment, where nothing reads it:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
PowerShell has no VAR=value command prefix form at all, so there they are set
as their own statement first:
$env:HORUS_VERSION = 'v0.4.0'
irm https://github.com/softmata/horus/raw/main/install.ps1 | iex
The installer reads HORUS_VERSION as a version pin. The CLI sets the
same name in every plugin's environment, to the version of the horus that
launched it (see the table below). They do not interact in normal use, but a
plugin or script that shells out to the installer inherits the value HORUS set
and silently pins the install to the version already running. Unset it first if
you mean "latest":
env -u HORUS_VERSION sh -c 'curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | bash'
Set by HORUS for the processes it starts
You do not set these; HORUS puts them into the environment of a process it spawns, so they are readable from your node, driver or plugin. They are listed because they are a contract: your code may read them, and a release that renamed one would break that code.
| Variable | Set by | Value |
|---|---|---|
HORUS_NODE_NAME | horus launch (per node) | The node's name from the launch file. Also read by the scheduler to name itself in horus node list |
HORUS_NAMESPACE | horus launch, from the launch file's top-level namespace | The shared-memory namespace every node in that launch shares. Also set by a test binary for its children, and only then — the write is confined to a test binary that resolved a test_-prefixed namespace and found the variable unset — so a spawned helper or interpreter lands in the same private namespace as the test that spawned it |
HORUS_NODE_FILE | horus run <file>.py | Absolute path to the Python file being run. The generated wrapper reads it with os.environ.get('HORUS_NODE_FILE') and exits 2 with an error if it is missing, so it is the contract behind every horus run of a Python node |
HORUS_PARAM_<KEY> | horus launch (per node), and every exec driver | One variable per parameter, uppercased. horus launch also maps - to _. The exec driver skips its three reserved keys, max_retries, restart_delay_ms and shutdown_timeout_ms, which it consumes itself |
HORUS_PLUGIN | every CLI plugin | Always 1. A plugin can use it to tell "invoked by horus" from "invoked directly by a user" |
HORUS_VERSION | every CLI plugin | The horus version that launched the plugin, for compatibility checks. The same name is a version pin when the installer reads it — see the callout above |
horus run additionally injects the eight networking names listed under
the manifest translation. The
CLI sets HORUS_DRIVERS and HORUS_ENABLE in its own process environment
from --drivers/--enable on horus run, horus build and horus test;
HORUS_RECORD_SESSION from horus run --record; HORUS_NET from --net;
HORUS_SIM_MODE and HORUS_SIM_TARGETS from --sim; and HORUS_ASSUME_YES
from horus new --yes. A child inherits all of them.
Set but not read
These are put into a child's environment and nothing in HORUS reads them back. They are documented rather than hidden because they are visible to your code and because their emptiness is the answer to "why did setting this in my launch file do nothing?".
| Variable | Set by | Status |
|---|---|---|
HORUS_NODE_RATE_HZ | horus launch, when a node declares rate_hz | Nothing reads it. A node's rate comes from its own .rate() call; the launch file value is passed along but does not override it |
HORUS_NODE_PRIORITY | horus launch, when a node declares priority | Nothing reads it. The source says so at the assignment (launch.rs:1155-1158): kept only so a configured value leaves a trace |
HORUS_NODE_NAMESPACE | horus launch, when a node declares namespace | Nothing reads it. Shared-memory isolation is keyed off HORUS_NAMESPACE, which horus launch sets from the launch file's top-level namespace — so a per-node namespace: does not isolate that node |
HORUS_INJECT_SESSION | horus record inject --script | The session name, passed to the horus run child that executes the script. Nothing reads it |
HORUS_INJECT_NODES | horus record inject --script | Comma-separated injected node names, or * for --all. Nothing reads it |
HORUS_DRIVER_NAME | nothing on the live path | Intended to give an exec driver its own name from horus.toml. It is set only by ExecDriver::from_params (horus_core/src/drivers/exec_driver.rs:139), whose sole caller is a unit test at :355; the path horus.toml actually takes is ExecDriver::from_config (horus_core/src/drivers/mod.rs:229), which injects HORUS_PARAM_* and no name. An exec driver that reads HORUS_DRIVER_NAME today gets nothing. Earlier versions of this page listed it as set by every exec driver, which was wrong, and the comment at docs_contract.rs:774 still says the same thing |
Internal and test-only
Set by HORUS itself or by its own test suite. Listed so you can recognise them, not so you can set them. Nothing here is reachable from an installed HORUS.
| Variable | Where | What it does |
|---|---|---|
HORUS_CI_TOPIC_NAME | horus_manager/src/bin/ci_test_node.rs | Topic the CI test node publishes on. Default ci_test_topic |
HORUS_CI_READY_FILE | same | File the CI test node touches once it is up, so a harness can wait on it |
HORUS_CI_DURATION_MS | same | How long the CI test node runs, in milliseconds. Default 5000 |
HORUS_CI_BEST_EFFORT | same | Present, with any value: build the node without .rate(), so it stays on the main thread instead of moving to the RT executor |
HORUS_CI_TOPIC_IN_CTOR | same | Present, with any value: build the node's Topic in the constructor rather than in init() |
HORUS_ESTOP_STDERR_CHILD | horus_core/src/scheduling/safety_monitor.rs | Marks the child half of the test that points stderr at /dev/full and asserts the e-stop latch still engages |
HORUS_CPP_FFI_EMIT_CHILD | horus_cpp/src/c_api.rs | Marks the child half of the test that checks a C++ node's log actually reaches the terminal, which libtest cannot capture in-process |
HORUS_LIVE5_GHOST_TOPIC | horus_core/src/communication/topic/header.rs | Topic name handed to the child that registers as a subscriber and is then killed with SIGKILL, to strand a participant entry |
HORUS_LIVE5_GHOST_CAPACITY | same | Ring capacity that child must open with. Unset means the auto_capacity default, which is what a live reproduction against a real node wants |
HORUS_TEST_REQUIRE_TOOLS | horus_manager/src/commands/fmt.rs | Turns "the formatter is not installed, skip" into a failure, so a runner without ruff cannot quietly make the formatting contracts vacuous |
HORUS_TEST_BUILD_EXAMPLES | horus_manager/tests/examples_contract.rs | Opts into the slow every-example-builds test |
HORUS_DOCS_DIR | the docs_* contract suites | Points them at a horus-docs checkout. Must contain content/docs, or it is ignored and ../horus-docs beside the repository is used |
HORUS_DOCS_FILTER | horus_manager/tests/docs_examples*.rs | Runs only the documentation examples whose name matches, while you are fixing one |
HORUS_SOAK_DURATION_SEC | horus_cpp/tests/cpp_soak_test.cpp | Length of the C++ soak test in seconds. Default 7200 (two hours); the test refuses anything at or below 60 |
The first four are read by the CI test node in src/bin/, which is shipped code
by path but exists only to be driven by the introspection contract tests.
HORUS_CI_BEST_EFFORT and HORUS_CI_TOPIC_IN_CTOR shape the two arrangements
that report their metrics through different mechanisms and have failed
independently of each other, which is why both exist.
The integration-test tree carries around a hundred further names of the same kind — a test, the process it spawns, and the topic or role they agree on. They are excluded on purpose, both from this page and from the check that guards it.
(HORUS_INSTALL_BRANCH used to be listed here. It is set by the person running
install.sh, not by HORUS, so it belongs in the installer table above.)
There is no HORUS_LOG and no HORUS_API_KEY: the first is spelled
HORUS_LOG_LEVEL, and the registry key is read from auth.json and nowhere
else.