> ## Documentation Index
> Fetch the complete documentation index at: https://flox-daniel-session-wrap-openshell.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugins

> Extend Flox with environment plugins and subcommand extensions

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

There are two ways to extend Flox:

* **Environment plugins** are packages installed into an environment that
  take part in its lifecycle — a `profile.d` script at activation, or a
  [`session-wrap` hook](#lifecycle-hooks) around the whole session, which
  is how [sandboxing](/concepts/sandboxing) works — and are configured per
  environment through the manifest. Most of this page is about them.
* **[Subcommand extensions](#subcommand-extensions)** add commands to the
  `flox` CLI itself: an executable `flox-<name>` becomes `flox <name>`.
  They are installed per user, belong to no environment, and run only
  when you invoke them.

<Warning>
  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; the
  [`session-wrap` hook](#lifecycle-hooks) additionally requires `"1.16.0"`
  and a feature flag, and is currently prototype-only. Subcommand
  extensions are a **beta** feature behind the `features.beta` flag.
</Warning>

## 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, at well-known paths inside its output:
  * a script in `etc/profile.d/`, sourced during activation — the
    standard way packages hook into shell setup, and the only payload most
    plugins need. 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.
  * optionally, a `session-wrap` hook executable under `etc/flox/hooks/`,
    which lets the plugin run the entire activation session under its
    control. See [Lifecycle hooks](#lifecycle-hooks).

A plugin's `profile.d` script reads its own configuration with the
`flox_plugin_data` shell function, which Flox provides during activation.
A hook executable receives the same table through a context file instead,
since it runs outside the activation shell. Nothing else ties a package
to a plugin — it's a naming convention, not a manifest field that marks a
package as one.

## The environment lifecycle

A Flox environment moves through phases — it's created and edited, locked
and built, activated, attached to by additional shells, and eventually
deactivated. Two mechanisms let a plugin participate:

| Lifecycle phase                       | Mechanism                                                                           | Payload        | Availability |
| ------------------------------------- | ----------------------------------------------------------------------------------- | -------------- | ------------ |
| Activation: before the session starts | [`session-wrap`](#session-wrap) — run the entire session under the plugin's control | executable     | prototype    |
| Activation: environment setup         | `profile.d` script — runs before `hook.on-activate`                                 | sourced script | Flox 1.14.0  |

Further hooks for the other phases were prototyped on the
flox/flox branch `prototype/sandbox-plugins` and are deferred until
something ships that needs them.

[Subcommand extensions](#subcommand-extensions) sit outside this table:
they extend the CLI rather than an environment, so there is no phase at
which Flox dispatches them — you run them.

## 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 theme={null}
[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.

A plugin that uses the [`session-wrap` hook](#lifecycle-hooks) needs one
more piece: a declaration in the
[`[plugin-hooks]`](#declaring-hooks-plugin-hooks) section. Unlike
`[plugins.<name>]` data, hook participation *is* cross-referenced — a
declaration without a matching installed package fails the activation,
and a shipped hook without a declaration is ignored with a warning.

## 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" theme={null}
_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. For a plugin that
  declares a [`session-wrap` hook](#lifecycle-hooks) the alignment is
  mandatory: the `[plugin-hooks]` value, the package's install ID, and
  the shipped hook filename must all carry the same name.
* **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.

## Lifecycle hooks

<Warning>
  Lifecycle hooks are a **prototype**: they exist on a development branch
  of Flox, not in any release. They require a manifest `schema-version`
  of `"1.16.0"` and an explicit feature flag:
  `flox config --set features.plugin_hooks true` (or export
  `FLOX_FEATURES_PLUGIN_HOOKS=true`). With the flag off, a
  `[plugin-hooks]` declaration is ignored with a warning and the
  activation proceeds unwrapped — so an environment that declares a hook
  stays usable for teammates who haven't opted in.
</Warning>

`profile.d` scripts cover one moment in the lifecycle: environment setup
at activation start. A lifecycle hook is a file at a well-known path
inside the plugin package, discovered in the rendered environment and
dispatched by Flox at the right moment. One hook exists today:
[`session-wrap`](#session-wrap), which runs before the activation
session starts.

With the feature flag off, the warning names the flag to enable:

```text theme={null}
Ignored [plugin-hooks] because the 'plugin_hooks' feature is not enabled.
Enable it with 'flox config --set features.plugin_hooks true'.
```

### The hook tree

```text theme={null}
<plugin package>/
├── etc/profile.d/0900_<name>.sh        # existing: activation env setup
└── etc/flox/hooks/
    └── session-wrap.d/<name>           # executable
```

Per-plugin files inside per-hook directories merge across packages
exactly like `profile.d` does. One caveat is load-bearing: two packages
shipping an identical leaf filename is a hard build failure, so naming
the hook file after the plugin (`<plugin-name>`) is a requirement, not
tidiness.

### Declaring hooks: `[plugin-hooks]`

A hook doesn't run just because a package ships it. The environment's
manifest must opt in, through a typed, top-level section with a single
key:

```toml theme={null}
[plugin-hooks]
session-wrap = "plugin-openshell"   # a string, not a list: at most one wrapper
```

The value names a plugin — the install ID of a package that must ship
`etc/flox/hooks/session-wrap.d/<plugin-name>`. Unknown keys in
`[plugin-hooks]` fail at parse time, and `session-wrap` is typed as a
single string, so two wrappers are unrepresentable in one manifest.

At activation, Flox verifies the binding in both directions. Each of the
following fails the activation with the message shown:

* The declared hook file is missing — the plugin isn't installed, or its
  package doesn't ship the hook:

  ```text theme={null}
  Plugin '<name>' declares a session-wrap hook but the environment provides none.
  Expected an executable at <path>.
  Ensure the plugin package is installed and provides the hook, or remove the [plugin-hooks] declaration.
  ```

* A hook file exists, but no installed package has the declared install
  ID:

  ```text theme={null}
  Plugin '<name>' is declared in [plugin-hooks] but not installed in this environment.
  Add a package with install id '<name>' to [install] before declaring its hooks.
  ```

* The hook file is shipped by a *different* package than the declared
  plugin's — a look-alike package shadowing a plugin's name:

  ```text theme={null}
  The session-wrap hook for plugin '<name>' is provided by a different package.
  Hooks must be shipped by the declared plugin's own package.
  Remove the conflicting package or fix the [plugin-hooks] declaration.
  ```

* The hook file isn't executable:

  ```text theme={null}
  The session-wrap hook for plugin '<name>' is not executable.
  The plugin package must ship etc/flox/hooks/session-wrap.d/<name> with the executable bit set.
  ```

In the other direction, a shipped hook that isn't declared is ignored
with a warning naming the fix:

```text theme={null}
Ignored session-wrap hook '<name>' shipped by an installed package.
Declare it under [plugin-hooks] in the manifest to enable it.
```

Why the declaration exists at all: installing any package already
concedes code execution at activation — every package's `profile.d`
script runs with your privileges. The declaration is not a code-execution
boundary. What it gates is one specific power a `profile.d` script
doesn't have: **session capture** — a `session-wrap` hook execs your
terminal session under code the plugin controls. `profile.d` scripts
have no such power, so they stay undeclared.

### Consent and composition

Declaring a session wrapper means "activating this environment hands the
session to that plugin". Flox makes sure that's always something *you*
wrote, and something you agree to:

* **Only the top-level manifest's `[plugin-hooks]` section is
  effective.** When one environment [includes](/concepts/composition)
  another, an included manifest's `[plugin-hooks]` section is dropped
  during composition, with a notice naming the include:

  ```text theme={null}
  Ignored [plugin-hooks] declared by included environment '<include>'.
  Declare plugin hooks in this environment's manifest to enable them.
  ```

  Plugin *data* tables flow through includes; hook *participation* does
  not — a declaration can never arrive from a manifest you didn't
  author. To enable an included environment's plugin hook, restate the
  declaration in your own manifest.
* **[Auto-activation](/concepts/auto-activation) asks first.** A
  directory whose environment declares a session wrapper is never
  activated in place by the prompt hook. Instead, entering it prompts
  before handing over the session, and the default is No:

  ```text theme={null}
  Enter '<path>'? Activation hands this session to plugin '<name>'. [y/N]
  ```

  Only `y` or `yes` accepts; bare Enter declines. The answer is
  remembered for the current shell visit — leaving the directory clears
  it, and re-entering asks again. The prompt appears even for
  directories you've allowed with `flox activate allow`, since a prior
  allow may predate the wrap declaration; an unregistered directory
  prompts only while `auto_activate` is `prompt`, and a denied one never
  does. Accepting runs `flox activate --dir <path>` as a foreground
  session rather than the usual in-place activation: when the session
  exits you are back in your original shell, with nothing activated. On
  fish and tcsh, or without a terminal, no prompt is shown — a notice
  points at running `flox activate` yourself instead:

  ```text theme={null}
  Run 'flox activate --dir <path>' to enter this environment (activation is handled by plugin '<name>').
  ```

### The hook protocol

Flox writes a JSON context file readable only by you (mode `0600`, in
Flox's temporary directory) and invokes the hook with five environment
variables:

* `FLOX_HOOK_CTX` — path to the context file
* `FLOX_HOOK` — the hook kind, `session-wrap`
* `FLOX_PLUGIN_NAME` — the plugin whose hook is being invoked
* `FLOX_BIN` — the invoking `flox` binary, for hooks that need to run
  Flox commands themselves (an image bake with `flox containerize`, say)
* `FLOX_HOOK_JQ` — a `jq` bundled with Flox, so shell-scripted hooks can
  parse the context without depending on one

The context is versioned (`ctx_version`, currently `1`) and carries
`plugin_table` — the plugin's own `[plugins.<name>]` table as verbatim
JSON, or `null` when the manifest has no table for it. This is how a
hook executable reads its configuration: it runs outside the activation
shell, so the `flox_plugin_data` function isn't available to it.

Hooks are language-agnostic — a hook with real logic can be a compiled
binary shipped in the package; simple ones stay shell. The hook inherits
your environment, working directory, and stdio, and runs before any
activation setup — which on macOS can mean bash 3.2 for a shell hook, so
keep it compatible.

### session-wrap

The hook runs the entire activation session under the plugin's control.
Flox dispatches it during `flox activate`, after the environment is
locked, built, and rendered (hooks are discovered in the rendered
environment), immediately before the session would start. The hook
composes whatever boundary it implements — an OS sandbox, a container —
and **execs the activation inside it; on success it never returns**. The
hook replaces the `flox` process, so its exit status becomes the
activation's: a hook that can't hand off must exit non-zero and say why
on stderr. There is no "decline and continue unwrapped" path — an
environment that declares a wrapper either activates wrapped or not at
all.

The context a `session-wrap` hook receives:

| Field                           | Meaning                                              |
| ------------------------------- | ---------------------------------------------------- |
| `ctx_version`                   | Context schema version, `1`                          |
| `dot_flox_path`                 | Absolute path to the environment's `.flox` directory |
| `env_name`                      | The environment's name                               |
| `activation_mode`               | `dev` or `run`                                       |
| `rendered_env`                  | Store path of the rendered environment               |
| `lockfile_path`                 | Path to the environment's lockfile                   |
| `plugin_table`                  | The plugin's `[plugins.<name>]` table, or `null`     |
| `invocation_type`               | How `flox activate` was invoked, with its payload    |
| `stdin_is_tty`, `stdout_is_tty` | Whether stdin and stdout are terminals               |
| `inner_argv`                    | The host-side argv to re-exec under a boundary       |
| `wrap_scope`                    | The re-entry marker value (see below)                |

The context gives a wrapper two ways to re-enter the activation.
`inner_argv` is the `flox activate` invocation itself — an argv starting
with the absolute path of the `flox` binary — sufficient for a boundary
that shares the host filesystem and can simply re-exec `flox` under a
wrapper process. `invocation_type` is the structured form of how you
invoked activation — `"interactive"`, `{"shellcommand": "<string>"}`
for `-c`, or `{"execcommand": ["<cmd>", "<arg>", ...]}` for
`-- <cmd>` — from which a container boundary composes its own
in-boundary command.

Rules Flox enforces around the wrap:

* **One wrapper per manifest**, structurally (see the schema above).

* **Re-entry is detected, nesting is refused.** The hook exports
  `_FLOX_SESSION_WRAPPED=<wrap_scope>` on the wrapped process. When the
  same environment re-activates inside its own boundary, the marker
  matches and Flox skips the wrap; activating a *different* wrapping
  environment inside it is an error, because nested boundaries are
  unsupported:

  ```text theme={null}
  Cannot activate this environment inside another environment's session-wrap boundary.
  Exit the wrapped session first, then run 'flox activate' again.
  ```

* **In-place activation is refused.** `eval "$(flox activate)"` cannot
  hand your current shell to a wrapper:

  ```text theme={null}
  Cannot activate in-place an environment that declares a session-wrap plugin.
  An 'eval "$(flox activate)"' cannot hand the current shell to plugin '<name>'.
  Run 'flox activate' to enter a wrapped session instead.
  ```

* **Ephemeral activations skip the wrap.** The ephemeral activation
  that `flox services start` and `flox services restart` perform to
  launch a new process-compose instance is never wrapped.

* Stdio is inherited but not guaranteed to be a terminal —
  `flox activate -- cmd | tee` reaches the hook with stdout a pipe. A
  hook that wants to prompt must check the tty state the context
  provides and talk to the terminal directly (`/dev/tty` or stderr),
  never stdout.

### Writing and testing a hook

Hooks are testable without publishing anything. Build the plugin package
(a `[build]` target whose output ships the hook tree), install it into a
test environment by store path, declare it, and activate:

```console theme={null}
$ flox build plugin-myname
$ cd ../test-env
$ flox install /nix/store/...-plugin-myname-0.0.1
$ flox edit    # add [plugin-hooks] declaring your hook
$ FLOX_FEATURES_PLUGIN_HOOKS=true flox activate
```

The cache directory blessed for plugin state is
`<project>/.flox/cache/plugins/<plugin-name>/` — it survives across
activations and is not committed.

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

```console theme={null}
$ flox activate -v -- true 2>&1 | grep '+ source'
+ source /nix/store/...-flox-interpreter/etc/profile.d/0100_common-run-mode-paths.sh
...
+ source /nix/store/...-flox-interpreter/etc/profile.d/0800_cuda.sh
+ source /path/to/myenv/.flox/run/aarch64-darwin.myenv-dev/etc/profile.d/0900_vault-secrets.sh
```

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

  ```text theme={null}
  ++ flox_plugin_data vault-secrets
  + _data='{"DB_PASSWORD":"secret/prod-db","GH_TOKEN":"secret/github-work"}'
  ```

  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.

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

A `session-wrap` hook has a different debugging surface, since it runs
outside the traced activation script:

* Pass `-vv` (debug-level logging) and Flox logs the dispatch —
  `exec'ing session-wrap hook` — with the plugin name and the resolved
  hook path.
* The hook inherits your terminal, so anything it writes to stderr
  reaches you directly.

## 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 theme={null}
# 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.

`[plugin-hooks]` sections don't merge at all: an included environment's
declaration is dropped, as described in
[Consent and composition](#consent-and-composition).

## Subcommand extensions

<Warning>
  Subcommand extensions are a **beta** feature, available since Flox 1.14.1
  behind a feature flag: `flox config --set features.beta true`, or export
  `FLOX_FEATURES_BETA=true`. While in beta, `flox extension` is hidden from
  `flox --help`, and extensions can only be installed from a local
  directory.
</Warning>

Environment plugins extend what an *environment* does. Subcommand
extensions extend what the *`flox` command* does: an executable named
`flox-<name>` becomes `flox <name>`, the way `git-<name>` becomes
`git <name>`. An extension is installed per user rather than per
environment, and Flox never runs one on its own — it runs when you invoke
it. (These are unrelated to the [IDE extensions](/install-flox/ide-extensions)
that integrate editors and coding agents with Flox.)

### How dispatch works

When `flox <name>` doesn't match a built-in subcommand, Flox looks for an
executable `flox-<name>` — first in its managed extensions directory
(`flox-<name>/flox-<name>` under `$XDG_DATA_HOME/flox/extensions/`,
typically `~/.local/share/flox/extensions/`), then on `PATH` — and
replaces itself with it. Everything after the name is passed through verbatim, the
extension inherits your environment, and Flox adds three variables:

| Variable              | Value                                                                               |
| --------------------- | ----------------------------------------------------------------------------------- |
| `FLOX_EXTENSION_NAME` | The extension's name                                                                |
| `FLOX_EXTENSION_PATH` | The managed install directory, or the executable itself when it was found on `PATH` |
| `FLOX_BIN`            | The `flox` binary that dispatched it, for calling back into Flox                    |

Built-in command names are reserved: dispatch never fires for them, and
installing a `flox-install` is refused up front rather than leaving you
with an extension that can never run. Global options
placed before the name (`flox -v hello`) are dropped rather than
forwarded; anything after the name belongs to the extension.

### Installing, listing, and removing

```console theme={null}
$ cd ~/src
$ flox extension install --from-path ./flox-hello
✔ Installed flox-hello -> /home/me/.local/share/flox/extensions/flox-hello
$ flox hello world
Hello from hello
args: world
$ flox extension list
NAME                  PATH
hello                 /home/me/src/flox-hello
$ flox extension remove hello
✔ Removed flox-hello
```

`flox extension install .` installs the current directory. Reinstalling
with `--force` is how an extension updates; `remove` deletes the install
directory and any state kept inside it.

### Writing an extension

An extension is a directory — conventionally named `flox-<name>`, which
is how the name is derived when there is no manifest — containing an
executable `flox-<name>`, written in any language. An optional
`flox-extension.toml` at the source root names it explicitly:

```toml theme={null}
schema = "1"

[extension]
name = "hello"
description = "Says hello"
```

`name` is lowercase (`[a-z0-9][a-z0-9_-]*`) and must match the directory
when both are present; `description` is recorded but not yet shown by
`flox extension list`. Only the executable and `flox-extension.toml` are
copied on install; anything else in the source directory is left behind,
so keep an extension to one self-contained executable. The canonical
reference is [flox-hello-local](https://github.com/flox/flox-hello-local):
clone it and install from the working tree, and it becomes
`flox hello-local`.

### Combining a plugin and an extension

An environment plugin runs at activation but has no command surface of
its own; an extension has a command surface but no place in the
activation. Take the `vault-secrets` plugin above: it exports secrets
during `flox activate`, and a failed lookup surfaces only as an aborted
activation. A `flox-vault-secrets` extension, installed once per user,
gives it a command: `flox vault-secrets check` runs
`"$FLOX_BIN" list --config` to read the current environment's manifest,
pulls out the `[plugins.vault-secrets]` table, and reports which
references resolve before you activate. Invoked inside an activated
shell, the extension also inherits `FLOX_ENV_PROJECT` and
`FLOX_ENV_CACHE`, so it can read any state the plugin leaves under its
cache directory (by convention `$FLOX_ENV_CACHE/plugins/<plugin-name>/`).
Today that is two installs: `flox install` for the package,
`flox extension install` for the command.

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

With the `session-wrap` hook the space widens from configuration to
behavior: a [sandbox plugin](/concepts/sandboxing) runs the whole
session inside an isolation boundary — an ordinary installable package,
with no sandbox-specific code in Flox itself.

Flox doesn't distinguish any of these from a secrets plugin. `[plugins]`
is free-form storage plus a convention for reading it; what a given
plugin does with its table — and with its hook — is entirely up to its
author. And when a plugin needs a command of its own — to review state,
grant access, or trigger work — a
[subcommand extension](#subcommand-extensions) provides one.

## 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
* [Sandboxing](/concepts/sandboxing) — the OpenShell plugin, the first
  consumer of the `session-wrap` hook
* [Activating environments](/concepts/activation) — where `profile.d`
  scripts run relative to `hook` and `profile`
* [flox-hello-local](https://github.com/flox/flox-hello-local) — the
  reference subcommand extension, and the in-tree
  [extension guides](https://github.com/flox/flox/tree/main/cli/flox/src/beta/extensions/docs)
* [Composing environments](/concepts/composition) — how `include` merges
  manifests
