Environment variables
Hostveil has no config file, so the environment is the whole of its persistent configuration surface. This page lists every variable it reads, every variable it sets for the commands it runs, and every variable it inherits that changes what you see — with the default and the accepted values for each.
Hostveil's own settings
These are Hostveil's, and each has a flag equivalent or no equivalent at all. Set them in a shell profile, a systemd unit, or in front of a single command.
| Variable | Default | Values | What it does |
|---|---|---|---|
HOSTVEIL_DEBUG | unset — no tracing | Any non-empty string | Writes one line per command to stderr: the quoted argument list, how long it took to the millisecond, and whether it failed — including the binary lookups that decide whether a domain runs at all. Command output is deliberately never logged, because docker inspect reports the resolved environment of every container and a trace pasted into a bug report must not be a credential leak. Nothing else changes; without it Hostveil takes the same code path it always did. |
HOSTVEIL_ASSUME_HOST | unset — a container is detected and said so | Any non-empty string | Audit this filesystem as a host even when it is plainly a container. Without it, the firewall domain reports Skipped in a container (it has no host firewall and never will; the packet filter that decides whether its ports answer belongs to the host) and the kernel-hardening findings become Manual (the values are the host kernel's, and a drop-in written inside cannot change them). Set it when the container is the host you mean — auditing a golden image before it is deployed, or using a disposable container as a root filesystem the way this project's own end-to-end job does. |
HOSTVEIL_NO_SUDO | unset — auto-elevation is on | Any non-empty string | Never re-execute under sudo. The ground that needs root — sshd_config, /etc/shadow, firewall state — then reports Skipped with a reason instead of failing, and scoring renormalizes over what did run. Nothing about detection or fixing changes. It suppresses only the re-exec: a Hostveil already running as root is unaffected. |
HOSTVEIL_THEME | the remembered choice, else onedark | onedark, gruvbox, nord, catppuccin, tokyonight | Color palette for the TUI and the palette the dashboard opens in. Precedence is --theme > this > the choice remembered in the state directory > the default. The name is matched exactly and is case-sensitive with no trimming, so HOSTVEIL_THEME=Nord does nothing at all and says nothing — an unrecognised value here falls back silently, and only a bad --theme is an error. |
HOSTVEIL_GLYPHS | the remembered choice, else plain | plain, nerd | Symbol set for the status markers the TUI and hostveil scan draw. Precedence is --glyphs > this > the remembered choice > plain. Surrounding whitespace is trimmed but the name is case-sensitive, and an unrecognised value falls back silently. nerd needs a patched Nerd Font installed: a terminal cannot be asked what font it has, and a missing glyph occupies the same cell a present one would, so this is opt-in. |
HOSTVEIL_LAYOUT | the remembered choice, else console | console, split, triage, railverdict, lanes, inline | Screen arrangement for the TUI and the arrangement the dashboard opens in. Precedence is --layout > this > the choice remembered in the state directory > console. Matched exactly and case-sensitive with surrounding whitespace trimmed; an unrecognised value falls back silently, and only a bad --layout is an error. In the browser a choice made in the dashboard’s own picker is stored there and wins over this for that browser. |
HOSTVEIL_AI_PROVIDER | ollama | ollama, anthropic, openai | Which AI backend explain --ai and the AI buttons use. ollama is local, so nothing leaves the host without this being changed explicitly. anthropic talks to the Claude API; openai talks to any vendor that speaks the OpenAI chat-completions shape (OpenAI itself, OpenRouter, Groq, Together, a self-hosted server on another machine, …) — useful when the machine running Hostveil is not powerful enough for a local model. An unrecognised value is not silently ignored: explain --ai reports it by name instead of the generic “no AI provider is reachable”. See AI explanations. |
HOSTVEIL_OLLAMA_HOST | http://127.0.0.1:11434 | Any base URL | provider=ollama: where the local server listens. Used as a prefix — /api/version and /api/generate are appended. A trailing slash is trimmed; the value is otherwise never parsed or validated. An empty value is treated as unset. Advisory only: it affects explain --ai and the AI buttons, and no score, finding, or fix depends on the server being reachable. |
HOSTVEIL_OLLAMA_MODEL | llama3.2 | Any model tag Ollama accepts | provider=ollama: which model to ask. Not validated locally; if it has not been pulled, Ollama answers with an error naming it. An empty value is treated as unset. Advisory only, on the same terms as the host above. |
ANTHROPIC_API_KEY | unset | A Claude API key | provider=anthropic: the credential, read the same way every Anthropic tool reads it, so a key already exported for another tool works here too. Without it Available reports the provider unreachable rather than sending a request. Advisory only, on the same terms as the Ollama variables above. |
HOSTVEIL_ANTHROPIC_MODEL | claude-opus-5 | Any Claude model ID | provider=anthropic: which model to ask. Not validated locally; an unavailable model surfaces as an error from the API naming it. |
HOSTVEIL_OPENAI_BASE_URL | https://api.openai.com/v1 | Any base URL | provider=openai: the API origin. Used as a prefix — /chat/completions and /models are appended. A trailing slash is trimmed. Point this at OpenRouter, Groq, Together, or a self-hosted OpenAI-compatible server to use that vendor instead. |
HOSTVEIL_OPENAI_API_KEY | unset | An API key for the configured vendor | provider=openai: the credential. Omitted from the request entirely when unset, for a local server that takes none. |
HOSTVEIL_OPENAI_MODEL | unset | Any model name the vendor accepts | provider=openai: which model to ask. Unlike Ollama's model there is no name that exists on every vendor this covers, so an unset value is a configuration error explain --ai reports by name rather than a silent default. |
HOSTVEIL_ELEVATED | unset — Hostveil never sets it | Any non-empty string | Declares that elevation has already happened, so Hostveil does not call sudo. It exists for a wrapper script that elevates on Hostveil's behalf. Hostveil used to set this on the child itself and read it back, which could not work — see below. |
HOSTVEIL_NO_UPDATE_CHECK | unset | any value | Set to any value and Hostveil never contacts GitHub on its own. Without it, a scan refreshes a cached answer to “is there a newer release” at most once a day, in the background, and a scan never waits for it. The result appears as one line under a human-readable report and never in --json, SARIF, or a report written to a file. hostveil update still works when you run it. |
SSH_CONNECTION | set by sshd | — | Set by sshd, not by you. hostveil serve reads it to notice that you are logged in remotely, and prints the ssh -L command to run on your own machine — the dashboard listens on loopback, so the URL it prints is that address on the server. The host in the command is the address your client actually reached, taken from this variable's third field. |
USER, LOGNAME | — | — | Read only as a fallback for the login name in that ssh -L command. SUDO_USER is preferred, because serve elevates and these become root under it. |
The three flag-shaped ones — HOSTVEIL_DEBUG, HOSTVEIL_NO_SUDO, HOSTVEIL_ELEVATED — are tested for being non-empty, not for a value. HOSTVEIL_DEBUG=false enables tracing and HOSTVEIL_NO_SUDO=0 opts out of sudo. Everything writes them as =1 because that is the only value that reads the way it behaves; to turn one off, unset it or set it to the empty string.
What survives the re-exec under sudo
Running a root-benefiting command as a normal user re-executes Hostveil under sudo, and sudo's env_reset — the default on every mainstream distribution — keeps only what env_keep names: TERM, LANG, LC_*, and a short list besides. Never an application's own variable. So Hostveil hands the settings you did set to sudo as assignments on its command line, which is how they arrive.
Carried: HOSTVEIL_DEBUG, HOSTVEIL_THEME, HOSTVEIL_GLYPHS, HOSTVEIL_LAYOUT, HOSTVEIL_AI_PROVIDER, HOSTVEIL_OLLAMA_HOST, HOSTVEIL_OLLAMA_MODEL, ANTHROPIC_API_KEY, HOSTVEIL_ANTHROPIC_MODEL, HOSTVEIL_OPENAI_BASE_URL, HOSTVEIL_OPENAI_API_KEY, HOSTVEIL_OPENAI_MODEL, and NO_COLOR — each only when it is actually set, so a host with none of them builds exactly the command it always did. explain elevates like every other root-benefiting command, so a credential that did not survive the re-exec would fail the same way an unreachable provider does — quietly — which is exactly what carrying it here prevents. HOSTVEIL_NO_SUDO is not carried because reaching the re-exec at all means it was unset, and HOSTVEIL_ELEVATED is not carried for the reason below.
HOSTVEIL_DEBUG=1 hostveil scan
runs: sudo HOSTVEIL_DEBUG=1 /usr/local/bin/hostveil scanThis had to be built, because for a long time none of them crossed. HOSTVEIL_DEBUG=1 hostveil scan is printed in hostveil help, in the README and on the troubleshooting page as the thing to attach to a bug report, and on an ordinary non-root host it produced a normal scan, no trace, and nothing to suggest the variable had been discarded; a theme or glyph set was ignored the same silent way. Continuous integration never saw it, because the end-to-end job runs as root with HOSTVEIL_NO_SUDO=1 — both of the branches that skip the re-exec. Any variable not on the list above is still dropped by env_reset: use sudo -E, or add an env_keep entry for it.
HOSTVEIL_ELEVATED=1 used to be set on the sudo child and read back there as a loop guard, and env_reset deleted it every time, so the guard could never fire. On a host whose sudoers sets a non-root runas_default, sudo returned an unprivileged process that re-ran sudo, without bound, prompting for a password each time. The guard that works is SUDO_USER, which sudo sets in the target environment itself rather than passing through.
What Hostveil honours from your environment
Hostveil reads some of these itself and inherits the rest through the terminal libraries the TUI is built on. None of them changes a finding, a score, or a fix — but several change what you are shown, and two change where files land.
Color and the terminal
| Variable | Default | Values | What it does |
|---|---|---|---|
NO_COLOR | unset — color on a terminal | See the note below — the two implementations disagree | Suppresses ANSI color. For hostveil scan this is Hostveil's own check and the only place it looks; for the TUI it arrives through the terminal library. The dashboard is HTML and ignores it. |
TERM | inherited; unset is treated as dumb | Any terminfo entry name | Drives the TUI's color depth and key decoding: dumb or empty renders without color, *-256color gives 256, *-color gives 16. hostveil scan never consults it, so scan still emits color under TERM=dumb where the TUI does not. One of the few variables sudo keeps, so it survives auto-elevation. |
COLORTERM | unset | truecolor, 24bit, yes, true (case-insensitive) | Promotes the TUI to 24-bit color whatever terminfo says about TERM. SSH does not forward it, so a remote session is usually 256 colors, and the palettes are written as 24-bit hex and degraded per profile. Every theme is held to what a 256-color terminal actually renders — each severity stays a color rather than collapsing to grey, and no two roles land on the same palette entry — so the difference is a coarser theme and not a misleading one. Setting it is still the better picture. |
CLICOLOR | unset | 1, true, t (and their false forms) | The clicolors convention: on a terminal whose TERM is not dumb, raises the TUI's floor to 16 colors. NO_COLOR wins over it. TUI only. |
CLICOLOR_FORCE | unset | 1, true, t (and their false forms) | Makes the TUI emit color even when its output is not a terminal, overriding TERM=dumb. Still loses to NO_COLOR. hostveil scan does not honour it — its rule requires a real terminal — so there is no supported way to get colored scan output into a pipe. |
TTY_FORCE | unset | 1, true, t (and their false forms) | Makes the TUI treat its output as a terminal without checking the file descriptor. |
TMUX | set by tmux inside a session | Presence only; the value is not parsed | Tells the TUI it is inside tmux, after which it runs tmux info to decide between 256 and 24-bit color. That call is the one command Hostveil's process makes that does not go through its own command layer: it is not traced by HOSTVEIL_DEBUG, not bounded by Hostveil's command timeout, and not covered by the locale pin. |
TERM_PROGRAM | set by the terminal emulator | Emulator identifiers (iTerm.app, vscode, …) | Terminal-capability sniffing during TUI startup, to decide which quirks to apply. |
SSH_TTY | set by sshd on a remote login | Presence only; the path is not used | Tells the TUI it is running over SSH, which changes what it assumes the terminal supports. Relevant here because a self-hosted server is usually reached this way. |
WT_SESSION | set by Windows Terminal | Presence only | Read during TUI startup and inert on the platforms Hostveil ships for. Listed so the enumeration is complete. |
GOOGLE_CLOUD_SHELL | set by Google Cloud Shell | 1, true, t | Promotes the TUI to 24-bit color, like COLORTERM, without COLORTERM being set. Read four lines from WT_SESSION in the same function, and listed for the same reason. |
TERMINFO | unset — the system database | A directory holding a terminfo database | Where the TUI looks up what TERM can do. It decides the answer to the truecolor probe and which key table the TUI decodes with, so pointing it at a database that does not describe your terminal produces wrong colors or keys that do nothing. hostveil scan never reads a terminfo entry. |
TERMINFO_DIRS | unset | Colon-separated directories | The same lookup, searched after TERMINFO. |
GOBIN | unset | a path | Read by hostveil update and hostveil uninstall only, to recognise a binary that go install put there. Such a binary is rebuilt from source rather than replaced with a published one. Hostveil never sets it. |
GOPATH | ~/go | a path | The same question when GOBIN is unset: $GOPATH/bin, and failing that ~/go/bin. |
Hostveil's own check, which governs hostveil scan, tests only whether the variable is present: any value disables color, including NO_COLOR=0 and an empty NO_COLOR=. The TUI's check comes from the terminal library and parses the value as a boolean: NO_COLOR=0, NO_COLOR= and NO_COLOR=yes all leave color on. So NO_COLOR=0 hostveil scan is monochrome while NO_COLOR=0 hostveil tui is colored. Set it to 1 and both agree. Note that treating an empty value as "disable" also departs from the no-color.org convention, which says it should not.
The proxy variables, on the advisory explainer's request
Hostveil makes exactly one kind of network request of its own: the advisory explainer talking to whichever provider HOSTVEIL_AI_PROVIDER names. It uses Go's default HTTP transport, which honours the standard proxy variables.
| Variable | Default | Values | What it does |
|---|---|---|---|
HTTP_PROXY, HTTPS_PROXY | unset | A proxy URL; the lowercase spellings work too | Routes the AI request through a proxy. The default provider (Ollama) talks to loopback, which is exempt, so this only takes effect if you have pointed Ollama's host elsewhere or switched to anthropic or openai — in which case your prompts, which contain your findings, go through that proxy. |
NO_PROXY | unset | Comma-separated hosts, domains or CIDRs | Exempts a host from the above. Loopback is already exempt without it. |
Nothing else in Hostveil opens a socket. The scan reads files and runs local commands; the dashboard binds to loopback and serves; the CVE scan is Trivy’s own process with its own configuration.
Where Hostveil keeps things, and what it can find
| Variable | Default | Values | What it does |
|---|---|---|---|
HOME | set by login; no fallback of Hostveil's own | An absolute path | Decides the state directory when Hostveil is not root: $HOME/.local/share/hostveil, which holds the saved scans, the rollback checkpoints, and the remembered theme, glyph and layout choices. As root the directory is /var/lib/hostveil and HOME is not consulted — which is why a fix applied under sudo is invisible to an unprivileged hostveil history. |
TMPDIR | /tmp | A directory path | Two roles. It is where an edit fix stages its before-and-after copies for validation, so a copy of sshd_config transits it on every SSH fix — written 0600 into a fresh directory that is removed afterwards. And it is the last-resort state directory when HOME cannot be resolved. If the validating binary cannot read the staged copies, the validator is skipped and the fix proceeds unvalidated rather than being blocked. |
PATH | inherited; Hostveil never sets or normalizes it | A colon-separated path list | Decides what Hostveil believes is installed. Every domain's availability gate is a binary lookup, so PATH is the difference between a domain running and a domain reporting Skipped — and a binary in /usr/sbin that is not on a non-login PATH looks exactly like a binary that is not installed. It also decides whether elevation happens: no sudo on PATH and Hostveil silently runs unprivileged. HOSTVEIL_DEBUG logs every lookup for this reason. |
sudo's secure_path replaces PATH outright in the elevated child on Debian and Ubuntu, so a re-executed Hostveil can resolve a different set of binaries than the process that started it would have.
If HOME cannot be resolved — a bare systemd unit, some container entrypoints — the state directory falls back to $TMPDIR/hostveil, i.e. /tmp/hostveil on a normal host. Checkpoints written there do not survive a reboot, so "a fix is always reversible" quietly stops being true, and nothing in the output says so. Give the unit a HOME, or run as root and get /var/lib/hostveil.
DOCKER_HOST
| Variable | Default | Values | What it does |
|---|---|---|---|
DOCKER_HOST | unset — the docker CLI uses /var/run/docker.sock | unix://, tcp://, ssh://user@host — whatever the docker CLI accepts | Hostveil never reads it, but it deliberately hands the whole inherited environment to the commands it runs, so docker, docker compose and trivy can reach a daemon that is not on this machine. |
The consequence is worth stating plainly: with DOCKER_HOST pointing elsewhere, the Compose, image-CVE, Docker-daemon and Docker-firewall domains describe a remote daemon while the SSH, sysctl, file-permission and account domains describe the local host — merged into one score, with nothing in any interface distinguishing them. That is occasionally what you want and is never what you want by accident. Unset it before scanning the machine you are sitting on.
Debug hooks the TUI inherits
The TUI is built on terminal libraries that carry their own debug switches. Hostveil neither sets nor clears them, and they are live on every hostveil tui run.
| Variable | Default | Values | What it does |
|---|---|---|---|
TEA_TRACE | unset — no log | A writable file path; empty is ignored | Opens that path append-only at mode 0600 and writes the TUI framework's internals to it, on every run. |
TEA_DEBUG | unset | 1, true, t | On a TUI panic, writes a bubbletea-panic-<timestamp>.log into the current working directory. Without it a panic still prints its stack to stderr. |
UV_DEBUG | unset — no log | A file path; empty is ignored | The same thing one layer down: a debug file for the terminal renderer underneath. |
hostveil tui auto-elevates, so the process that creates these files is root and the path comes from the environment. A local unprivileged user who can set the environment of a Hostveil invocation — a wrapper script, a shared shell profile, a cron entry — gets a root-owned append to a path of their choosing. Do not export any of the three system-wide, and treat an unexplained one in a profile the way you would treat any other privileged file write.
What Hostveil sets for the commands it runs
Hostveil passes its own environment through to every command it runs, with exactly one addition.
| Variable | Default | Values | What it does |
|---|---|---|---|
LC_ALL | always C in the child, whatever you have set | Not configurable — no flag or variable overrides it | Appended to the environment of every command Hostveil runs, so tools answer in untranslated English. Every parser that matches a word rather than a number depends on it. |
This exists because of a specific failure. Hostveil inherits the operator's environment and sudo keeps LANG and LC_* by default, so on a German host Hostveil asked apt in German and then looked for English. The string [upgradable from: %s] is translated in twenty-one locales, and the counter skipped every line that did not contain that exact English text: zero pending security updates on an unpatched machine, which is the failure class the whole tool refuses. A clean report you cannot trust is worse than no report.
LC_ALL rather than LANG, and this is the part worth remembering: gettext honours LANGUAGE over LANG, so pinning LANG alone would leave a desktop's LANGUAGE=de:en in charge. LANGUAGE is ignored when the locale is C, which is why this one variable is enough.
The locale does not change the layout, on purpose
The width library Hostveil links reads these at startup and sets a global that Hostveil then does not use — it builds its own measurement with East Asian width switched off. The obvious assumption is the opposite one, so it is worth stating positively: none of these four changes how a screen is laid out.
| Variable | Default | Values | What it does |
|---|---|---|---|
LANG | set by login | Any locale name | Nothing, in Hostveil's own process. It reaches a width library at startup and is neutralized there. It is the reason the pin above exists: an inherited LANG would otherwise make apt, systemctl and docker answer in a translated locale. |
LC_CTYPE | set by login | Any locale name | Nothing, for the same reason. It sits second in that library's locale precedence and is overridden by the LC_ALL=C pin in every child regardless. |
LANGUAGE | set by some desktops | A colon-separated locale list | Nothing — Hostveil never reads or clears it. It is listed because it is load-bearing in the reasoning above: it outranks LANG in gettext, which is why the pin is LC_ALL. |
RUNEWIDTH_EASTASIAN | unset | Anything strconv.ParseBool reads as true — 1, t, true. Anything else, including an unparseable value, leaves it off. | Measures East Asian Ambiguous characters — the ellipsis, arrows and bullets Hostveil draws — as two columns instead of one, in both of the libraries that lay out the terminal UI. Set it if your terminal renders them wide. What Hostveil does not take from the locale is the same setting derived from LANG: the same binary must not lay out differently for an operator in Seoul and one in Berlin, and this variable is the explicit request rather than the inference. |
What it reads from the tooling around it
| Variable | Default | Values | What it does |
|---|---|---|---|
SUDO_USER | set by sudo, never by Hostveil | Any non-empty string means "already elevated"; the username itself is never inspected | The loop guard on auto-elevation. A non-empty value means sudo has already run, so Hostveil does not invoke it again. |
SUDO_UID, SUDO_GID | set by sudo, never by Hostveil | Non-negative numeric user and group IDs | When scan --output runs as a sudo child, the newly written report is assigned to the invoking account instead of being left root-owned. They are used only when the effective user is root and SUDO_USER is also set; an existing report keeps its current owner and mode. |
sudo sets this in the target environment itself rather than passing it through, which is precisely why env_reset cannot strip it and why it works where Hostveil's own marker could not. It is also set when the sudo target is not root, which is the case worth catching: on a host whose sudoers sets a non-root runas_default, the child comes back unprivileged and stops here instead of looping.
A false positive — SUDO_USER left in the environment by something other than the sudo that ran Hostveil — costs an unprivileged scan whose root-only domains report Skipped with a reason. That is the safe direction to be wrong in; an unbounded chain of password prompts is not.
What is not on this page
Test and build variables. The test suite reads a couple of variables the shipped binary never does, and the release build pins a couple more that do not exist at runtime. Neither is a knob on a running Hostveil, so listing them here would only invite someone to set one. They are documented where they are used, in the repository's development notes — see Contributing.
Variables Hostveil looks at, rather than reads. Some checks inspect environment variables belonging to your services: credential-shaped keys in a Compose file's environment: block, or in a container's .env. Those are data Hostveil examines on disk. It never reads your shell's environment looking for secrets, and nothing on this page is sent anywhere — every variable here is read locally and used locally.
Anything that relaxes a finding. There is no variable that changes a severity, adjusts the score, or makes a fix do less than it says. Detection is decided by the host, not by the environment; the variables that change what a scan covers do it by changing what Hostveil can reach — PATH, HOSTVEIL_NO_SUDO, and DOCKER_HOST.
HOSTVEIL_ASSUME_HOST is the one that comes closest, and it is worth being exact about. It does not turn a check off — it turns two back on, by answering a question about what kind of machine this is. Inside a container Hostveil declines to audit the host firewall and declines to offer the kernel-hardening fix, because neither can be acted on from in there; setting the variable says that this filesystem is the host you mean. It cannot make a finding smaller, and there is still no way to ask Hostveil to overlook one.