Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions concepts/plugins.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
---
title: "Plugins"
description: "Package reusable, manifest-configured behavior as an installable component"
---

[Secrets management](/concepts/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](#beyond-secrets) for other examples.

<Note>
Plugins are **experimental** and under active development. Their behavior
may change. They require `schema-version = "1.14.0"` in the manifest.
</Note>

## 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](/concepts/activation#activation-flow) 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:

```toml
[install]
vault-secrets.pkg-path = "vault-secrets"

[plugins.vault-secrets]
GH_TOKEN = "secret/github-work"
DB_PASSWORD = "secret/prod-db"
```

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.

<Warning>
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.
</Warning>

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](#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:

```bash title="etc/profile.d/0900_vault-secrets.sh"
_data="$(flox_plugin_data vault-secrets)" # {"GH_TOKEN":"secret/github-work",...}

while IFS= read -r name; do
path="$("${_jq:-jq}" -r --arg n "$name" '.[$n]' <<< "$_data")"
export "$name=$(vault kv get -field=value "secret/$path")"
done < <("${_jq:-jq}" -r 'keys[]' <<< "$_data")
```

`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.

## Plugin data in composed environments

When one environment [includes](/concepts/composition) another, and both
configure the same plugin, the including environment's table wins
outright — Flox doesn't merge the two tables key by key:

```toml
# included environment
[plugins.vault-secrets]
GH_TOKEN = "secret/github-work"
DB_PASSWORD = "secret/staging-db"

# including environment
[plugins.vault-secrets]
DB_PASSWORD = "secret/prod-db"
```

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

- [`manifest.toml` reference](/man/manifest.toml#plugins) — `[plugins]`
section
- [Secrets management](/concepts/secrets-management) — the hand-written
pattern a secrets plugin packages up
- [Activating environments](/concepts/activation) — where `profile.d`
scripts run relative to `hook` and `profile`
- [Composing environments](/concepts/composition) — how `include` merges
manifests
10 changes: 10 additions & 0 deletions concepts/secrets-management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,20 @@ active with the old secret value.
| Doppler | `doppler login` | `doppler secrets get NAME --plain` |
| Pass | GPG key unlock | `pass show service/credential` |

## Packaging this pattern as a plugin

Writing the same `on-activate` retrieval hook into every environment that
needs a given secret store gets repetitive. [Plugins](/concepts/plugins)
let you package that hook once, as an installable component that other
environments configure through a `[plugins]` table instead of copying
shell code.

## Further reading

- [Cross-platform secrets blog post](https://flox.dev/blog/get-your-preferred-secrets-manager-in-a-portable-cross-platform-cli-toolkit/)
- [Flox and AWS secrets management](https://flox.dev/blog/flox-and-aws-taking-the-chaos-out-of-secrets-management/)
- [Flox + 1Password](https://flox.dev/popular-packages/adding-1password-secrets-to-flox-environments/)
- [`manifest.toml` reference](/man/manifest.toml) — `[hook]` section
- [Activating environments](/concepts/activation) — how hooks work
- [Plugins](/concepts/plugins) — package this pattern as an installable
component
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"concepts/catalog-imports",
"concepts/publishing",
"concepts/secrets-management",
"concepts/plugins",
"concepts/flox-vs-containers"
]
},
Expand Down