Every bundled hytte-plugin-* binary is configured the same way: no config
DSL, just environment variables read at startup. This page inventories every
one of them, swept from source (std::env::var / var_os / option_env!
call sites in crates/hytte-plugin-*/src, the hytte-plugin SDK, and
hytte-ai-providers) rather than hand-maintained — see #573.
Declaratively, per plugin, via the Nix option:
programs.trollshell.plugins.<name>.env = {
PET_NAME = "nisse";
PET_LLM_MODEL = "google/gemini-3.5-flash";
};
This renders into the launch-state file the shell reads at startup and is
applied as --setenv=K=V when it launches the plugin as a transient
trollshell-plugin-<id> user unit (trollshell/src/plugin_launcher.rs). Set
the same vars in a shell profile for an ad hoc / hand-launched run — the
plugin reads its environment directly either way.
Secrets never go in env. The launch-state file is world-readable, so API
keys must go through programs.trollshell.plugins.<name>.secrets instead
(e.g. secrets = [ "openrouter" ];), which injects the key from the login
keyring as <SLOT>_API_KEY at spawn time — never written to disk. See
nix/module-common.nix's secrets option description for the full mechanism.
Precedence, for a plugin that also reads a config file (e.g. usage's
~/.config/trollshell/usage.toml): environment wins over the file, which wins
over the plugin's built-in default. Not every plugin has all three layers —
each section below says which it actually reads.
Infrastructure vars, not knobs. Every plugin (via hytte-plugin-proto's
topology::socket_path) dials the host at
$XDG_RUNTIME_DIR/trollshell/plugin.sock, and a couple of plugins resolve
their own state/config paths off $XDG_STATE_HOME / $XDG_CONFIG_HOME /
$HOME. These are standard session variables set by the login session /
systemd, not something you set per-plugin — they're omitted from the tables
below and noted inline only where a plugin's behavior depends on them in a
non-obvious way.
Sections below follow bundledPluginNames' order in flake.nix (12 total).
hytte-plugin-audio-widget)No runtime knobs — configuration is entirely via the shell/wire protocol
(it renders purely off the host's StateKey::AudioSpectrum push).
hytte-plugin-bar-clock-demo)No runtime knobs — configuration is entirely via the shell/wire protocol.
hytte-plugin-caw)| Variable | Default | Effect |
|---|---|---|
CAW_BRIEFING_TIME |
07:00 |
Local trigger time for the once-daily morning briefing, as H or H:MM. off/none disables the briefing entirely. An unparseable value silently falls back to the default rather than erroring (briefing.rs::parse_time). |
CAW_LLM_URL |
unset (→ OpenRouter, only if a key is resolved) | Base URL for the briefing's voiced-composition backend. Set to a local llama-server URL (e.g. http://127.0.0.1:8080) to self-host; set to "" explicitly to force the plain canned-template composer with no network call. Unset with no key resolved also falls back to the plain composer (briefing.rs::resolve_provider). |
CAW_LLM_MODEL |
unset | Model id sent in the chat request body when using the OpenRouter path (cloud endpoints require a model id — see the SDK-level note below on what happens if it's left unset). Ignored by a local llama-server, which uses its own loaded model. |
CAW_LLM_API_KEY |
unset | Fallback OpenRouter API key, used only if hytte_ai_providers::load_key("openrouter") (see "Shared" below) finds nothing. Prefer plugins.caw.secrets = [ "openrouter" ]; instead — this direct env path exists but isn't the recommended one (world-readable launch-state file). |
CAW_EXPRESSION_PATH |
$XDG_STATE_HOME/caw/expression.json (→ ~/.local/state/caw/expression.json) |
Overrides the path to the expression file caw's own opencaw caw_express tool writes and this plugin polls read-only. The default matches opencaw's own default, so the two ends agree with no configuration (expression.rs::expression_path). |
TROLLSHELL_WEATHER_CITY |
unset | Briefing-only fallback: when the first [[place]] in ~/.config/trollshell/places.toml has no lat/lon, the briefing forward-geocodes this city name for its weather ingredient (ingredients.rs::coords) — the same var the weather plugin itself reads (see below). Not documented anywhere for caw prior to this sweep. |
Also reads $HOME (to locate places.toml) and $XDG_STATE_HOME/$HOME
(for the expression/briefing-stamp state dir) — standard XDG paths, not
per-plugin knobs.
hytte-plugin-clock-demo)No runtime knobs — configuration is entirely via the shell/wire protocol.
hytte-plugin-departures)No environment-variable knobs. Station config (which stop, walk-time budget,
line/direction filter) comes entirely from the first [[place]] block of
~/.config/trollshell/places.toml — the same file + schema the native
hytte-services::places service owns and documents a default for
(feed.rs::load_station_config). The file is re-read on every poll, so an
edit while the board is open is picked up on the next fetch. Only $HOME is
read (to locate the file) — not a configurable knob.
hytte-plugin-infobroker)No launch-time env knobs for the plugin daemon itself. Its own broker socket
and durable grant store are located from $XDG_RUNTIME_DIR and
$XDG_STATE_HOME/$HOME respectively (paths.rs) — standard session paths,
always set, not meant to be overridden via plugins.infobroker.env.
The crate also ships a separate CLI binary, hytte-infobroker (the client the
skill folder's agents shell out to — not what plugins.<name>.env launches,
which is the hytte-plugin-infobroker daemon binary). That CLI reads
HYTTE_INFOBROKER_TOKEN, the session token hytte-infobroker auth --agent <name> mints and prints as an export line for eval; hytte-infobroker get then requires it be set (cli.rs). This is CLI-side, per-invocation
state, not a plugin-launch knob.
hytte-plugin-pet)| Variable | Default | Effect |
|---|---|---|
PET_NAME |
nisse |
The pet's display name, used in its own persona prompt when the LLM brain is active (brain.rs::Cfg::from_env). |
PET_LLM_URL |
unset (→ OpenRouter, only if a key is resolved) | Base URL for the pet's brain. Set to a local llama-server URL to self-host; set to "" explicitly to force canned-only replies with no network call. Unset with no key resolved also falls back to canned-only (brain.rs::resolve_provider). |
PET_LLM_MODEL |
unset | Model id sent when using the OpenRouter path (required by that cloud endpoint — see the SDK-level note below). Ignored by a local llama-server. |
PET_LLM_API_KEY |
unset | Fallback OpenRouter API key, used only if hytte_ai_providers::load_key("openrouter") finds nothing. Prefer plugins.pet.secrets = [ "openrouter" ]; instead. |
TROLLSHELL_PET_KAOMOJI |
unset (falsy) | Set to 1 or true (case-insensitive) to force the kaomoji fallback face at startup, bypassing whatever the normal render path would pick (main.rs::init). |
hytte-plugin-preem-demo)No runtime knobs — configuration is entirely via the shell/wire protocol.
hytte-plugin-terminal)No runtime knobs — configuration is entirely via the shell/wire protocol.
hytte-plugin-timer)No runtime knobs — configuration is entirely via the shell/wire protocol.
hytte-plugin-usage)Precedence is env → ~/.config/trollshell/usage.toml → built-in default,
resolved once per field (config.rs::resolve). With no dashboard URL from
either source the plugin renders a calm empty-state card and makes no network
calls at all.
| Variable | Default | Effect |
|---|---|---|
TROLLSHELL_USAGE_DASHBOARD_URL |
unset (empty-state, no polling) | The Grafana …/public-dashboards/<token> URL (browser or API form). The one value that flips the card from empty-state to live. |
TROLLSHELL_USAGE_BUDGET |
unset (spend-only, no gauge) | The budget denominator for the "burned ÷ budget" gauge. Must parse as a finite f64; an unparseable value is ignored. |
TROLLSHELL_USAGE_PANEL |
unset (auto-discover the first value panel) | Pins the dashboard panel to query by its numeric id, skipping panel discovery. |
TROLLSHELL_USAGE_WINDOW |
now-5h |
The Grafana time-range from for the query window (e.g. now-24h, now/d); to is always now. |
hytte-plugin-weather)| Variable | Default | Effect |
|---|---|---|
TROLLSHELL_WEATHER_CITY |
unset | Forward-geocoded (via open-meteo) fallback location, used only when GeoClue2 is absent, denied, or times out after 10s (location.rs::resolve). GeoClue is always tried first. Usually set session-wide via programs.trollshell.weather.fallbackCity rather than per-plugin here — that Nix option is what actually sets this var for the weather plugin's launch. |
The hytte-plugin SDK itself (crates/hytte-plugin/src, the Plugin
trait + run()) reads no environment variables — env-based config is a
plugin-author idiom on top of it, not part of the runtime.
hytte-ai-providers (the shared OpenAI-compatible chat client pet and
caw's brains both use) reads two, via load_key:
| Variable | Default | Effect |
|---|---|---|
<NAME>_API_KEY (e.g. OPENROUTER_API_KEY) |
unset | Overrides the on-disk key file for provider <name> (upper-cased). This is exactly the variable plugins.<id>.secrets = [ "<name>" ] injects at spawn from the login keyring — the recommended way to supply a key. Wins over the file when set. |
XDG_CONFIG_HOME |
unset (→ $HOME/.config) |
Base directory load_key reads the <name>.key file from: $XDG_CONFIG_HOME/trollshell/<name>.key, e.g. openrouter.key. Standard XDG var, not plugin-specific. |
For pet/caw specifically, the OpenRouter key precedence is therefore:
OPENROUTER_API_KEY env → ~/.config/trollshell/openrouter.key file →
the plugin's own PET_LLM_API_KEY/CAW_LLM_API_KEY env fallback (last
resort, not recommended — see each plugin's table above).
A note on *_LLM_MODEL with no value set: when the OpenRouter path is
selected (a key is resolved and no *_LLM_URL override is set) but
PET_LLM_MODEL/CAW_LLM_MODEL is unset, Provider.model stays None and no
model field is sent in the request body. hytte-ai-providers' own docs note
OpenRouter requires a model id for a cloud call — the code does not enforce
this or supply a default, so an unset model on the cloud path is a
misconfiguration rather than a supported "use OpenRouter's default" mode.
Consult crates/hytte-ai-providers/src/lib.rs and each plugin's
brain.rs/briefing.rs if this needs to change.