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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ocenv-0.1.0.tar.gz.
File metadata
- Download URL: ocenv-0.1.0.tar.gz
- Upload date:
- Size: 45.0 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5bacb07df3193a534c2fc200da8bbeff6ea8cbf04bda845ec63e1f338e125c2
|
|
| MD5 |
e08fc87a0ec830c31c8b0145ed7d468a
|
|
| BLAKE2b-256 |
97342789519fcd3f9c1880030e744050347692c17cf7588a4c139d0495c3679e
|
File details
Details for the file ocenv-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ocenv-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.2 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b76ab159ca053760f9ade7bdf5e7bb2dbb80f601addba0a287e007a873876ab
|
|
| MD5 |
74ec9605971a3f891cdd5cddffe92a51
|
|
| BLAKE2b-256 |
18cb148b1984fdbe82924abe3258cda2dfe74917038fc77c01f6ec5732352c4d
|