on-activate hook calls out to a secret store and exports the result as an
environment variable. That pattern works, but every environment that uses it
hand-writes the same retrieval script.
Plugins let you package that script once, as a regular installable
package, and configure it per environment through a dedicated [plugins]
section of the manifest. Anyone who installs the package gets the retrieval
logic; they only need to supply the configuration.
Secrets retrieval is the use case that motivated plugins, and this page
anchors on it. But [plugins] itself is general-purpose: Flox stores
whatever data you put there without interpreting it, so a plugin can use
it for anything. See Beyond secrets for other examples.
Plugins are experimental and under active development. Their behavior
may change. They require
schema-version = "1.14.0" in the manifest.How plugins work
A plugin has two halves:- Configuration lives in the manifest, under
[plugins.<plugin-name>]. Flox treats it as opaque data — any keys, any values — and stores it without validating its shape. - Behavior lives in a package. It ships a script in its output’s
etc/profile.d/directory — the standard way packages hook into shell setup — and Flox sources every installed package’sprofile.dscripts before running your manifest’shook.on-activate. See Activating environments for where this fits in the activation timeline.
profile.d script reads its own configuration with the
flox_plugin_data shell function, which Flox provides during activation.
Nothing else ties a package to a plugin — it’s a naming convention, not a
manifest field that marks a package as one.
Installing and configuring a plugin
Installing a plugin is the same as installing any package, plus one step: adding its configuration table. Suppose avault-secrets package provides a plugin that wraps HashiCorp
Vault. Install it, then add a [plugins.vault-secrets] table following
the convention its author documented — here, a flat map of environment
variable name to secret path:
flox activate, and GH_TOKEN and DB_PASSWORD are exported, fetched
fresh from Vault — the same result as a hand-written on-activate hook,
except the retrieval logic now ships with the package instead of living in
your manifest.
Add a [plugins.<name>] table without installing a matching plugin, and
nothing happens — Flox doesn’t cross-reference the two. What happens if
you install a plugin but skip its configuration is up to the plugin: a
script that lets flox_plugin_data’s failure propagate aborts activation;
one that checks for it explicitly can warn and continue instead. See
Writing a plugin for both patterns.
Writing a plugin
Any package can be a plugin. What makes it one is aprofile.d script that
reads its own manifest data:
etc/profile.d/0900_vault-secrets.sh
flox_plugin_data <plugin-name> prints the [plugins.<plugin-name>] table
from the locked manifest as compact JSON, or fails if the table is
missing. Parse the JSON however you like — ${_jq:-jq} reaches for the
jq that Flox’s own activation helpers already resolved into $_jq
before falling back to a jq on PATH, so your script doesn’t need to
depend on one itself.
The script above fails hard: _data="$(flox_plugin_data vault-secrets)"
is a plain assignment, and profile.d scripts run under set -e, so a
missing table aborts activation. That’s a choice, not something Flox
enforces — wrap the call and check its exit status yourself to degrade
gracefully instead, for example printing a warning and leaving a variable
unset when a secret is optional. Fail hard for a plugin the environment
can’t run without; fail soft for one it can.
A few conventions to follow when naming and scoping a plugin:
- Name it after your package. The plugin name doesn’t have to match the
package’s install ID or
pkg-path, but matchingpkg-pathmakes the connection obvious to anyone reading the manifest. - Read only your own table. Nothing stops a script from reading the
whole manifest, but Flox won’t enforce that boundary for you — stick to
[plugins.<your-plugin-name>]. - Order your script deliberately.
profile.dscripts run in filename order. Flox’s own setup scripts currently top out around0800; a0900prefix runs after them, and after any other plugin your logic depends on.
flox build too, so [build] commands can
read your plugin’s exported variables — not just interactive and
flox activate -- <cmd> sessions.
Plugin data in composed environments
When one environment includes another, and both configure the same plugin, the including environment’s table wins outright — Flox doesn’t merge the two tables key by key:DB_PASSWORD, not GH_TOKEN
plus an overridden DB_PASSWORD. Flox warns when this happens — a
partial, key-by-key merge could hand a plugin a table its author never
intended. If you compose environments that share a plugin, restate every
key you want to keep in the including environment’s table.
Beyond secrets
Secrets retrieval fits[plugins] well because “environment variable name
→ secret path” is exactly the kind of per-environment configuration
shared logic needs. That shape isn’t unique to secrets — a plugin could
equally:
- Standardize the config for a linter or formatter across every
environment that installs it, instead of copying the same
[vars]or[hook]entries into each manifest. - Toggle a package’s optional behavior — verbose logging, a feature flag, a telemetry opt-out — per environment.
- Inject build-time metadata, like a license key or an internal registry URL, that a package needs to configure itself correctly.
[plugins] is
free-form storage plus a convention for reading it; what a given plugin
does with its table is entirely up to its author.
Further reading
manifest.tomlreference —[plugins]section- Secrets management — the hand-written pattern a secrets plugin packages up
- Activating environments — where
profile.dscripts run relative tohookandprofile - Composing environments — how
includemerges manifests