Skip to main content
Secrets management shows a pattern: an 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. Expect much of what this page describes to change in future releases. Plugins require a schema-version of "1.14.0" or higher 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’s profile.d scripts before running your manifest’s hook.on-activate. See Activating environments for where this fits in the activation timeline.
A plugin’s 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 a vault-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:
Run 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.
Flox doesn’t check that an installed package actually provides the plugin named in [plugins.<name>], and a plugin’s script can read your entire manifest, not just its own table. Trust plugin packages the way you’d trust any package that runs code during activation.
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 a profile.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 matching pkg-path makes 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.d scripts run in filename order. Flox’s own setup scripts currently top out around 0800; a 0900 prefix runs after them, and after any other plugin your logic depends on.
The same script runs during 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 + source line appears in filename order — Flox’s own setup scripts first, then plugin scripts. If no profile.d lines 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 in run mode, which skips package profile.d scripts entirely.
  • What data did it receive? Drop the grep and the trace shows every command inside your script as it executes, including what flox_plugin_data printed:
    This is the only window into that call — flox_plugin_data exists only while profile.d scripts are being sourced, so you can’t run it by hand in the activated shell afterward.
  • Which command failed? profile.d scripts run under set -e, so when a plugin aborts activation, the last traced command before the failure is the one that caused it.
The verbose trace prints every command with its arguments fully expanded — for a secrets plugin, that includes the fetched secret values. Treat the output like the secrets themselves: don’t paste it into an issue or capture it in CI logs.

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:
The composed environment ends up with only 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.
Flox doesn’t distinguish these from a secrets plugin. [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