Environment Variables
This page lists every HORUS_* variable HORUS reads, and every one it
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 it.
There is deliberately no count in this sentence: the last one said 47 while the
tables listed 53. Completeness is enforced instead by
docs_contract::every_environment_variable_is_in_the_reference_page, which
fails the build when the source reads or sets a name this page does not list.
Logging
| Variable | Default | Effect |
|---|---|---|
HORUS_LOG_FILE | unset | 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 |
HORUS_REMOTE_LOG_SIZE | 2000 | Entries buffered for replication to other machines |
The error buffer is separate on purpose: a node logging at high rate can evict
its own errors from the main buffer, and the error ring is what horus blackbox
reads after a crash.
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, replication is on and auto-starts at scheduler.run(); 0 or false turns it off. Any other value, including true, leaves it on |
HORUS_NO_NETWORK | unset | Set to disable replication for one run — wins over the above |
HORUS_NET_PORT | 9100 | UDP port the replicator binds |
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 | loopback + RFC1918 + link-local + CGNAT | Which source addresses inbound datagrams may come from. any/* accepts the public internet |
HORUS_NET_SECRET | unset | Shared token peers must match. Used verbatim; there is no env: indirection in horus.toml |
HORUS_NET_IMPORT | auto | Which remote topics to subscribe to |
HORUS_NET_DENY_EXPORT | empty | Topics that must never leave this machine |
HORUS_NET_OPTIMIZERS | empty | Comma-separated: fusion, delta, spatial, predict. An unknown name warns and installs nothing |
HORUS_NET_HEARTBEAT_MS | 50 (200 under WSL) | How often peers announce themselves |
HORUS_NET_MISSED_THRESHOLD | 3 (5 under WSL) | 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, or higher under WSL2 | Missed announcements before a peer is considered gone |
HORUS_NET_LOG_LEVEL | warn | Which logs replicate to peers: none/off, error, warn/warning, info, debug/trace/all |
Safety
| Variable | Default | Effect |
|---|---|---|
HORUS_ESTOP_REMOTE | warn | What a remote emergency stop does locally. off, ignore, 0 or false disable it; anything else warns |
HORUS_ESTOP_KEY | unset | HMAC-SHA256 key authenticating the networked e-stop. 64 hex chars are used as raw key material; anything else is stretched with PBKDF2-HMAC-SHA256. While unset, remote e-stops are rejected — see Authenticating the networked e-stop |
Note the default: an unrecognised value warns rather than disabling, so a typo cannot silently switch off remote e-stop.
Runtime
| Variable | Default | Effect |
|---|---|---|
HORUS_SIM_MODE | unset | 1 runs drivers in simulation. Set by horus run --sim |
HORUS_SIM_TARGETS | unset | Comma-separated driver names to simulate, when only some should be |
HORUS_DRIVERS | unset | Comma-separated driver list. Set by horus run --drivers |
HORUS_ENABLE | unset | Comma-separated capabilities, e.g. net. Set by horus run --net |
HORUS_TELEMETRY_ENDPOINT | unset | Where the scheduler exports telemetry |
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. Read once per process and cached, so changing it mid-run has no effect |
HORUS_NODE_NAME | binary name | Names the scheduler in horus node list and in presence files |
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 |
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 |
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 bool, integer, float, or string, in that order |
This is the highest-precedence layer: it beats .horus/config/params.yaml, which
beats the built-in defaults. horus launch uses it to pass per-node parameters,
so a variable set by hand overrides a launch file for that run.
CLI and toolchain
| Variable | Default | Effect |
|---|---|---|
HORUS_ASSUME_YES | unset | 1 answers horus new and horus setup-rt prompts with yes. horus new -y/--yes sets it for you; horus setup-rt has no such flag, so the variable is the only way to accept its kernel-install and limits-file prompts without a terminal |
HORUS_AUTO_INSTALL | unset | 1 installs missing C++ dependencies without asking |
HORUS_CPP_LINK | static | How the HORUS C++ runtime is linked into a node binary: static or shared. Static puts the whole runtime in every binary — about 2.0 MB stripped per node in release, 78 MB in debug — so a robot running a dozen nodes pays it a dozen times. shared links libhorus_cpp.so and leaves each node at tens of kilobytes, at the cost of shipping the library beside them. Overrides [cpp] link in horus.toml for one build |
HORUS_VERBOSE | unset | 1 shows the full compiler and CMake output |
HORUS_SOURCE_DIR | unset | Path to a HORUS checkout, used to resolve message types and sources |
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 | unset | Trust an HTTP proxy's forwarding headers |
HORUS_ALLOW_LOCAL_PLUGINS | false | Allow horus monitor to run the project-local .horus/bin/horus-monitor. Off by default — that binary is not trust-checked, and cloning a repo must not be enough to run its code |
HORUS_NO_SHELL_INTEGRATION | unset | Set during install to skip the cargo/pip/cmake shell proxies |
HORUS_BUILD_FROM_SOURCE | unset | 1 makes install.sh skip the pre-built binary and compile from a checkout instead. The installer names it in two of its own error messages — "build from source with HORUS_BUILD_FROM_SOURCE=1" when neither sha256sum nor shasum is available to verify the download, and again when the download fails — so it is the documented escape hatch from a failed install |
HORUS_INSTALL_BRANCH | main | Branch install.sh clones when it builds from source |
HORUS_NET | unset | 1 or true reports networking as enabled. horus doctor prints "use --net or HORUS_NET=1 to enable". Setting it by hand only changes what doctor reports: horus run --net sets it and adds net to the capability list, and it is the capability list that reaches cargo --features. On its own this variable does not build a networked binary |
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 contains horus/Cargo.toml |
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_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 with an error if it is missing, so it is the contract behind every horus run of a Python node |
HORUS_DRIVER_NAME | every exec driver | The driver's name from horus.toml. Injected alongside HORUS_PARAM_<KEY> for each of the driver's parameters, so an exec driver written in any language can tell which driver instance it is |
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 |
Set but not yet read
These are put into the 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: 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 |
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.
HORUS_CI_DURATION_MS, HORUS_CI_READY_FILE, HORUS_CI_TOPIC_NAME,
HORUS_CI_BEST_EFFORT, HORUS_CI_TOPIC_IN_CTOR,
HORUS_AUTH_ALLOW_MISSING_STATE, HORUS_ESTOP_STDERR_CHILD,
HORUS_TEST_BUILD_EXAMPLES, HORUS_DOCS_DIR.
(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 table above with the rest of
the installer's knobs.)
HORUS_CI_BEST_EFFORT and HORUS_CI_TOPIC_IN_CTOR shape the test node used by
the introspection contract tests. The first omits .rate(), so the node stays
on the main thread instead of moving to the RT executor; the second builds its
Topic in the constructor rather than in init(). Both distinctions matter
because the two arrangements report their metrics through different mechanisms
and have failed independently of each other.
HORUS_DOCS_DIR points the documentation contract tests at a horus-docs
checkout, and HORUS_TEST_BUILD_EXAMPLES=1 opts into the slow example-build
test. Both matter only if you are working on HORUS itself.