ocenv
Run multiple "flavors" of opencode side by side - personal vs. work auth, company-specific providers, per-project config - from a single declarative file instead of a pile of hand-maintained shell aliases.
opencode's OPENCODE_CONFIG is a single file pointer, not a stack, so you cannot express
"always include my company's providers and this project's config". ocenv replaces the
aliases with a model of identities, layers, and profiles, composes layered
config, and launches opencode in a fully isolated environment.
Concepts
- Identity - an isolated opencode footprint. Redirects
XDG_DATA_HOMEso auth (auth.json) and the session db are per-identity, and injects the composed config viaOPENCODE_CONFIG. State, cache, and your shared~/.config/opencodestay shared, so the redirect does not leak into other XDG-aware tools (e.g.nvim) launched from inside opencode.opencode auth loginis done once per identity. Setisolate_config = truefor a hermetic identity that also suppresses the shared global config. - Layer - a named, reusable fragment contributing a config payload and/or environment
variables. The config payload is either inline in
ocenv.tomlor a reference to an external JSON/JSONC file (e.g. your company's providers);envis independent, so anenv-only layer is a valid, reusable secrets/env bundle. - Profile - the launchable unit: an identity + an ordered list of layers + extra env + a working directory. Launching folds the layers' config and env into one, and starts opencode.
Layer/profile env composes left-to-right (later wins), with profile.env applied last.
ocenv's own managed vars (XDG_DATA_HOME, OPENCODE_CONFIG, and XDG_CONFIG_HOME for a
hermetic identity) are pinned last of all, so env cannot accidentally break isolation - if a
layer/profile tries to set one, it is ignored and ocenv warns.
Layers are merged with opencode's own semantics (objects deep-merge, arrays replace, and
instructions is concatenated + de-duplicated), and injected via OPENCODE_CONFIG - which
opencode loads below project config, so a project's own .opencode config still wins,
exactly as normal.
Getting started
Requires Python 3.13+, uv, and opencode on your PATH.
# install
uv sync
# 1. create a config
mkdir -p ~/.config/ocenv
cp examples/ocenv.toml ~/.config/ocenv/ocenv.toml # then edit to taste
# 2. see what a profile resolves to (no opencode launched)
uv run ocenv --profile work show
# 3. inspect the isolated environment
uv run ocenv --profile work env
# 4. authenticate the identity once
uv run ocenv --profile work opencode -- auth login
# 5. launch opencode with the composed, isolated environment
uv run ocenv --profile work opencode
Set a profile for the whole shell with the OCENV_PROFILE environment variable so you can
drop the flag:
export OCENV_PROFILE=work
uv run ocenv opencode
CLI
Profile selection is a global option (aws-cli style) and must come before the command:
ocenv --profile <p> opencode [-- <opencode args...>] # compose + launch opencode
ocenv --profile <p> show # print the composed config as JSON
ocenv --profile <p> env # print the resolved env (export lines)
ocenv list # list profiles + identities
Everything after -- is passed straight through to opencode:
ocenv --profile work opencode -- run "summarize this repo"
ocenv --profile work opencode -- debug config # opencode's own resolved config
--profile / -p also reads from the OCENV_PROFILE env var. Point ocenv at a specific
config file with --config <path>; otherwise it is discovered at
$XDG_CONFIG_HOME/ocenv/ocenv.toml (falling back to ~/.config/ocenv/ocenv.toml).
Configuration
ocenv.toml has three sections - layers, identities, and profiles:
[layers.personal-base.config] # inline opencode fragment
model = "anthropic/claude-opus-4-8"
[layers.company-providers]
file = "~/workspaces/acme/.opencode/providers.json" # external JSON/JSONC file
[layers.company-secrets.env] # env-only layer, reusable bundle
JIRA_URL = "https://jira.acme.example"
[identities.work]
root = "~/.local/share/ocenv/identities/work" # optional; defaults to a managed path
isolate_config = true # optional; suppress shared global config
[profiles.work]
identity = "work"
layers = ["personal-base", "company-providers", "company-secrets"] # order: later wins
cwd = "~/workspaces/acme"
[profiles.work.env]
SOME_TOKEN = "..." # applied after all layer env
See the examples/ directory for complete, commented configurations.
How isolation works
opencode resolves its storage locations from the raw XDG_* env vars, and those are
inherited by every process opencode spawns (the bash tool, $EDITOR, an embedded terminal,
nvim). Overriding all four would therefore redirect those tools too - so ocenv keeps the
redirect deliberately narrow:
XDG_DATA_HOME→<identity>/data. This is the only store forauth.json(and the session db), and opencode offers no other override for it, so this redirect is unavoidable. It is the one leak that remains:nvim's data (plugins) is redirected if you launch it from inside opencode, though its config/state/cache are untouched.OPENCODE_CONFIG→ the composed config file. This is opencode-specific (not an XDG var, so no leak) and loads below project config, preserving "project wins".- state and cache are left shared (
XDG_STATE_HOME/XDG_CACHE_HOMEuntouched). - your shared
~/.config/opencodestill applies as opencode's global base. Setisolate_config = trueon an identity to suppress it: ocenv then pointsXDG_CONFIG_HOMEat a managed empty dir (this redirect is inherited by child tools, so it is opt-in).
ocenv also scrubs opencode's other config-pointer env vars (OPENCODE_CONFIG_CONTENT,
OPENCODE_CONFIG_DIR, OPENCODE_DISABLE_PROJECT_CONFIG) from the launched environment, so
a stray value in your shell cannot leak config into an otherwise isolated identity.
OPENCODE_CONFIG is not scrubbed because ocenv sets it itself.
Development
uv sync
uv run pytest # unit + integration tests
Integration tests exec the real opencode binary and assert against opencode debug config
/ opencode debug paths; they skip automatically when opencode is not installed.
See DESIGN.md for the full design rationale and opencode config-resolution
details.
Metadata
Release files for ocenv 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ocenv-0.1.0.tar.gz | 45.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ocenv-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.1 kB
Release files / ocenv-0.1.0.tar.gz
| Download URL | ocenv-0.1.0.tar.gz |
|---|---|
| Size | 45.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b5bacb07df3193a534c2fc200da8bbeff6ea8cbf04bda845ec63e1f338e125c2
|
|
BLAKE2b-256 checksum How to use checksums |
97342789519fcd3f9c1880030e744050347692c17cf7588a4c139d0495c3679e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.31 {"installer":{"name":"uv","version":"0.11.31","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ocenv-0.1.0-py3-none-any.whl
| Download URL | ocenv-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5b76ab159ca053760f9ade7bdf5e7bb2dbb80f601addba0a287e007a873876ab
|
|
BLAKE2b-256 checksum How to use checksums |
18cb148b1984fdbe82924abe3258cda2dfe74917038fc77c01f6ec5732352c4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.31 {"installer":{"name":"uv","version":"0.11.31","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|