opencode2-direnv
v2026.908.2
Published
OpenCode 2 plugin that automatically loads direnv environment variables
Maintainers
Readme
opencode2-direnv
Seamless direnv integration for OpenCode 2
Automatically load direnv environment variables into OpenCode 2 sessions — and keep them in sync while you work.
Fork of @simonwjackson/opencode-direnv, ported to the OpenCode 2 plugin API. OpenCode 1 users should use the original package.
Overview
The plugin detects .envrc files and keeps the OpenCode server environment in sync with your devshell:
- Automatic Detection — Searches for
.envrcfrom the project directory up to the git root - Shell Injection — Injects the current direnv export into every shell OpenCode spawns (via the
shell.create.beforehook), so agent commands always see the devshell - Live Reloading — Re-applies the devshell when
.envrc/flake.nix/flake.lockchange (debounced) or a new session starts, and removes variables that are dropped - Fallback Sync — Also reconciles
process.envso other subprocesses (LSP, MCP) inherit the devshell - Graceful Degradation — Silently skips if direnv is not installed or no
.envrcexists; warns in the logs when a.envrcis blocked
Requirements
- OpenCode 2 (
opencode2, beta) - direnv >= 2.0 in PATH
Installation
Add the plugin to your OpenCode 2 configuration (plugins, not the v1 plugin key):
Project-level (./opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode2-direnv"]
}Global (~/.config/opencode/opencode.json):
{
"plugins": ["opencode2-direnv"]
}Then allow your .envrc once:
direnv allowUsage
Start opencode2 in a project with an .envrc. Environment variables are loaded at startup, re-synced per session, and reloaded (debounced ~1.5s) when devshell files change. Outcomes are logged with a direnv: prefix:
direnv: environment loaded
direnv: reloaded (+2 ~1)
direnv: .envrc is blocked. Run `direnv allow` to enable.Local development
To dogfood this repository's working tree instead of the published package (this repo's own setup), disable the published plugin and load a local dev instance with a distinct plugin id:
// opencode.json (project)
{
"plugins": ["-opencode2-direnv"]
}// .opencode/plugins/dev/index.ts — auto-discovered, not published
const plugin = (await import(`../../../src/index.js?dev=${Date.now()}`)).default
export default { ...plugin, id: "opencode2-direnv-dev" }Three details matter:
- Distinct id — without it the local instance's own
opencode2-direnvid re-enables the disabled package ("a later id re-enables a disabled plugin") and then fails withDuplicate plugin ID. - Cache-busted dynamic import — OpenCode cache-busts only the entrypoint
file (
index.ts?mtime=…); a static import ofsrc/index.jsis served from the module cache for the server's lifetime, sosrc/edits would silently run stale code until a service restart. The per-evaluation query makes every wrapper reload import the current working tree. .opencode/plugins/— configured path entries must be directories on current betas; direct files load only from.opencode/plugins/.
In other projects (without a globally installed copy), a plain directory entry also works and needs no disable or distinct id:
{
"plugins": ["/path/to/opencode-direnv"]
}The root index.ts re-exports src/index.ts; with the plain entry, edits are
picked up on restart. Typecheck, test, and build with:
npm ci
npx tsc --noEmit
npm test
npm run buildHow It Works
plugin setup ────▶ direnv export json ──▶ process.env reconcile
│ │
├── shell.create.before hook ────────────┴─▶ every spawned shell gets the devshell
├── session.created event ──▶ re-sync per session (e.g. after `direnv allow`)
└── filesystem.changed event (.envrc/flake.nix/flake.lock) ──▶ debounced reload- Discovery — Searches upward from the plugin's location directory, stopping at the git root
- Export — Runs
direnv export json(fast when nothing changed; direnv's own watch caches it) - Reconcile — Applies additions/changes and removes dropped keys; explicit direnv unsets (
nullvalues) are honored. Keys are reference-counted across parallel projects, so one project's reload can't delete another's live vars; fully released keys revert to their pre-plugin values - Inject — The
shell.create.beforehook copies the cached export into the environment of every shell spawned below the devshell directory
Troubleshooting
# Verify direnv is installed and .envrc is allowed
which direnv && direnv status
# Verify the export works in the project directory
direnv export jsonIf the plugin does not activate, check the server logs (opencode2 run --standalone --print-logs --log-level debug ...) for direnv: lines or plugin load errors.
Limitations
- Reloads are debounced (~1.5s); run
direnv reloadfor an immediate refresh .envrcmust be explicitly allowed (direnv allow) — a blocked file is reported in the logs- A changed
use flakere-evaluates in the background; the session is never blocked
License
MIT — originally by @simonwjackson, ported to the OpenCode 2 plugin API by @ankarhem.
