Skip to content
Back to skills

Stacks Plugins

ASecurity

Use when working with the Stacks preload chain and Bun plugins - the env plugin, the framework preloader, how auto-imports get injected into globalThis, why a command does or does not see framework globals, the bun-plugin-stx static-serve plugin, or writing a Bun plugin. Covers bunfig.toml preload and storage/framework/defaults/resources/plugins/preloader.ts.

  • 631 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added June 5, 2026
developmenttypescriptgobashapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 3, 2026

npx -y skills add stacksjs/stacks --skill stacks-plugins --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stacks Plugins?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stacks Plugins
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/stacksjs-stacks-plugins/badge)](https://www.skillsdirectory.com/skills/stacksjs-stacks-plugins)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stacks-plugins
description: Use when working with the Stacks preload chain and Bun plugins - the env plugin, the framework preloader, how auto-imports get injected into globalThis, why a command does or does not see framework globals, the bun-plugin-stx static-serve plugin, or writing a Bun plugin. Covers bunfig.toml preload and storage/framework/defaults/resources/plugins/preloader.ts.
license: MIT
compatibility: Bun >= 1.3.0, TypeScript
allowed-tools: Read Edit Write Bash Grep Glob
---

# Stacks Plugins and the Preload Chain

Stacks has **no `@stacksjs/plugins` package**. "Plugins" here means Bun's native
plugin API plus the two modules `bunfig.toml` preloads before any Stacks code
runs. If you are chasing "why is `User` undefined in this command" or "why did
`.env` not decrypt", this is the skill.

## Key Paths

- Preload declaration: `bunfig.toml` (`preload`, at the root and under `[test]`)
- Env plugin: `storage/framework/core/env/plugin.ts`
- Framework preloader: `storage/framework/defaults/resources/plugins/preloader.ts`
- Static-serve plugin: `bunfig.toml`, `[serve.static] plugins = ["bun-plugin-stx"]`

## The preload chain

`bunfig.toml` preloads two modules, **in this order**:

```toml
preload = [
  "./storage/framework/core/env/plugin.ts",
  "./storage/framework/defaults/resources/plugins/preloader.ts",
]
```

1. **Env plugin** loads `.env` and decrypts dotenvx-style encrypted values, so
   everything downstream reads plaintext from `process.env`.
2. **Preloader** loads env files for the resolved environment, then injects the
   framework's server auto-imports into `globalThis`.

Order is load-bearing: the preloader reads decrypted secrets, so it has to run
second.

Tests preload a different pair - the env plugin and `tests/setup.ts` - so the
suite gets production env semantics without the auto-import graph.

## What the preloader does

### 1. Resolves the environment

Before loading any `.env` file it works out `APP_ENV`, because that decides
*which* file gets decrypted:

- `deploy staging` and `deploy --staging` both resolve to `staging`. Both forms
  are handled; CI uses the flag form.
- `cloud:remove`, `cloud:destroy`, `cloud:cleanup`, `undeploy` force `production`.
- Bare `deploy` defaults to `production`.

### 2. Loads and decrypts env files

Gated on **`isRepl` / `isPostinstall` only** - never on the fast-command list
below. Fast commands (`migrate`, `build`, `seed`, ...) genuinely need decrypted
config; gating them here caused stacksjs/stacks#2048, where an encrypted
`.env.<env>` silently never decrypted even with the key present.

Postinstall skips because `@stacksjs/env` may not be linked yet mid-install.

### 3. Injects auto-imports into `globalThis`

Loads ~24 framework packages plus everything in `app/Jobs/` and
`resources/functions/`, assigning every named export onto `globalThis`. That is
why `await User.find(1)` and `response.json(...)` work in an action with no
import statement. See `stacks-auto-imports` for the full manifest.

A `protectedGlobals` set guards the runtime: `process`, `fetch`, `Promise`,
`Bun`, `console` and friends are never overwritten, whatever a package exports.

## The fast-command skip (the usual gotcha)

Auto-import injection is **skipped** when the first CLI argument matches:

```
dev  build  test  lint  migrate  fresh  seed  generate  make
key:generate  scaffold:crud  version  help  --version  -v  --help  -h
```

Prefix matches count, so `generate:migrations` skips along with `generate`.

Two reasons: cold-start speed, and correctness. Codegen commands must not pull
the router and ORM graph in before `bun-query-builder` can diff schemas - a
broken `@stacksjs/bun-router` install used to make `generate:migrations` exit 1
with no output at all, because the preloader died loading `@stacksjs/router`.

**Consequence:** inside those commands, framework globals do not exist. Import
what you need explicitly.

The REPL skips too (`!process.argv[1]`), as does anything running under
`npm_lifecycle_event === 'postinstall'`.

### Opting back in

The preloader exports `loadAutoImports()` so a server entrypoint can pull the
graph in deliberately:

```ts
const { loadAutoImports } = await import('../../../defaults/resources/plugins/preloader.ts')
await loadAutoImports()
```

`dev/api.ts` does exactly this. Bun consumes `--watch`, so a directly-run server
script sees an empty `argv.slice(2)` and would otherwise be indistinguishable
from a bare invocation.

## Writing a Bun plugin

Use Bun's native API directly - there is no framework wrapper to import:

```ts
import { plugin } from 'bun'

plugin({
  name: 'my-loader',
  setup(build) {
    build.onLoad({ filter: /\.custom$/ }, async (args) => {
      const contents = await Bun.file(args.path).text()
      return { contents: transform(contents), loader: 'ts' }
    })
  },
})
```

Register it by adding the module to `preload` in `bunfig.toml`. A plugin only
affects the process that preloaded it.

## Gotchas

- **No `@stacksjs/plugins` package.** Import `plugin` from `bun`.
- **Preload order matters.** Env decryption has to precede the preloader.
- **Fast commands have no globals.** If a `make:*` or `generate:*` command needs
  a framework symbol, import it - do not rely on auto-imports.
- **`bun-plugin-stx` is a static-serve plugin**, wired through
  `[serve.static] plugins` in `bunfig.toml`. The preloader's own
  `import 'bun-plugin-stx'` line is commented out; stx compilation in the dev
  server runs through the stx package, not that import.
- **Preloader failures are silent per package.** Each import sits in its own
  `try {} catch {}`, so a package that fails to build leaves its globals
  undefined instead of crashing the process. A mysteriously undefined global is
  usually an unbuilt package - check `storage/framework/core/<pkg>/dist`.
- **Adding a global?** Export it from a package already in the preloader's list,
  or from `resources/functions/`, then run `buddy generate --types` so the
  declarations match the runtime.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.