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.
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.
Debugging a plugin
Activation runs plugin scripts silently. When one doesn’t do what you expect, pass-v to flox activate — verbose mode traces the
activation script command by command, including every profile.d script
as it’s sourced:
-- true activates, runs true, and exits — a quick way to capture a
trace without entering a subshell. The trace goes to stderr, hence the
redirect.
The trace answers the questions that come up while writing a plugin:
-
Did my script run, and when? Each
+ sourceline appears in filename order — Flox’s own setup scripts first, then plugin scripts. If noprofile.dlines appear at all, either the environment was already active somewhere and this activation attached instead of re-running setup — exit the other activation first — or the environment is inrunmode, which skips packageprofile.dscripts entirely. -
What data did it receive? Drop the
grepand the trace shows every command inside your script as it executes, including whatflox_plugin_dataprinted:This is the only window into that call —flox_plugin_dataexists only whileprofile.dscripts are being sourced, so you can’t run it by hand in the activated shell afterward. -
Which command failed?
profile.dscripts run underset -e, so when a plugin aborts activation, the last traced command before the failure is the one that caused it.
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