Juggling work and personal opencode accounts means hand-swapping env vars and clobbering your
sessions. ocp gives each project its own auth, session history, and config — and switches
automatically based on the directory you're in. Think direnv for opencode.
- 🔐 Multiple accounts, simultaneously — separate
auth.jsonper profile; no more logging out. - 📁 Auto-switched per directory — drop a
.ocprofilein a repo and you're on the right account. - 🪶 Zero dependencies — one Bash script, no daemon, no config format to learn.
Without ocp, this means manually exporting OPENCODE_CONFIG_DIR / XDG_DATA_HOME for every project
and remembering to reset them — one slip and you've cross-contaminated accounts or sessions.
ocp lets you run opencode under named profiles — work, personal, client — each with its own
authentication, session history, and configuration (opencode.json, agents, skills, plugins, and its own
oh-my-openagent section). Pick a profile explicitly, set a global default, or let it switch automatically
based on the directory you're in. It is a single, dependency-free Bash script that sets four environment
variables opencode and omo already understand — nothing more.
- Isolated auth — separate
auth.jsonper profile; stay logged into multiple accounts at once. - Isolated sessions — separate session database per profile.
- Isolated config — separate
opencode.json,AGENTS.md, agents, skills, and plugins, plus a per-profile omo section. - Automatic per-directory profiles — a
.ocprofilefile activates a profile for a whole project tree. - Bring your own secrets — plain env files, macOS Keychain, 1Password, or any wrapper command.
- Zero runtime dependencies — one Bash script; no daemons, no config format to learn.
curl -fsSL https://raw.githubusercontent.com/xterr/ocp/main/install.sh | bashPass a profile name (defaults to default):
curl -fsSL https://raw.githubusercontent.com/xterr/ocp/main/install.sh | bash -s -- workThe installer downloads ocp from the latest release into
~/.local/bin, creates the profile, sets it as the default, and wires the shell integration into the
right rc file. Use --no-shell to skip the rc change, or --version <x.y.z> to pin a specific release.
Works with zsh, bash, and fish. ocp itself is a Bash script, so it needs Bash ≥ 4 installed as its
interpreter (macOS ships 3.2 — brew install bash) — but not as your login shell. The installer
detects your shell and adds the matching integration.
Update anytime with ocp self-update (or ocp self-update <x.y.z> to pin a version).
Manual install
# download the latest release binary onto your PATH:
curl -fsSL https://github.com/xterr/ocp/releases/latest/download/ocp -o ~/.local/bin/ocp
chmod +x ~/.local/bin/ocp
# zsh / bash — add to ~/.zshrc or ~/.bashrc:
eval "$(ocp init-shell)"
# fish — add to ~/.config/fish/config.fish:
ocp init-shell --shell fish | sourceinit-shell auto-detects your shell from $SHELL; pass --shell zsh|bash|fish|posix to override.
opencode and oh-my-openagent must be available too.
ocp create work --description "Work account" # scaffold a profile
ocp use work # set the global default
ocp launch -p work -- auth login # authenticate (once per profile)
opencode # runs under the active profileAfter eval "$(ocp init-shell)" is in your shell, plain opencode is profile-aware.
opencode reads its directories from environment variables. ocp sets three, scoped to the launched
process:
| What | Variable | Resolves to | Isolated |
|---|---|---|---|
| config | OPENCODE_CONFIG_DIR |
<profile>/config |
yes |
| auth + sessions | XDG_DATA_HOME |
<profile>/data |
yes |
| prompt history + recent models | XDG_STATE_HOME |
<profile>/data/state |
yes |
| omo section | OMO_PROFILE |
<name> |
yes, by section |
| model + package cache | (untouched) | shared | shared on purpose |
Agents, skills, plugins, auth, and sessions are children of the first two directories, so they are isolated automatically.
XDG_STATE_HOME matters because opencode keeps your typed prompt history, frecency ranking,
recent-model list, and plugin metadata there. Left shared, one profile's prompts and models surface in
another. It is nested inside data/, so ocp remove --purge-data still cleans it up.
The cache stays shared deliberately: it holds public model catalogs and version-keyed package downloads (hundreds of MB), none of it account-bearing.
omo needs its own variable. Since omo 5.x it reads a single, home-anchored ~/.omo/omo.jsonc
(plus project-level .omo/omo.jsonc) and offers no variable to relocate that file, so
OPENCODE_CONFIG_DIR no longer isolates it. omo instead selects a section from the file's top-level
profiles object, and ocp exports OMO_PROFILE=<profile> to pick it:
The profile layer merges over the base, so each section only declares what differs. If
profiles.<name> is missing, omo does not error — it silently falls back to the base config, so
ocp create warns and ocp resolve shows which section is active. Set OMO_PROFILE in a profile's
env file to point at a differently-named section.
Drop a .ocprofile file in a project and any opencode launched within it uses that profile:
cd ~/work/some-repo
ocp pin work # writes ./.ocprofile
opencode # uses the 'work' profileocp walks up from the current directory to find the nearest .ocprofile. It only selects one of your
profiles — it never runs code.
-p <name>flag$OCP_PROFILE- nearest
.ocprofile - global default (
ocp use) - none → plain opencode
If the resolved profile no longer exists, an explicitly requested one (-p, $OCP_PROFILE, or a
.ocprofile) is a hard error, while a stale global default is ignored — opencode just runs without
a profile (case 5).
Each profile has two optional hooks. Edit them with ocp edit <name> --what env|manifest.
env file — <profile>/env, sourced as Bash before launch. Use static values or pull from any store:
OPENAI_API_KEY=sk-... # static
export ANTHROPIC_API_KEY="$(security find-generic-password \
-a "$USER" -s opencode-work -w)" # macOS Keychain
export OPENAI_API_KEY="$(op read 'op://Work/openai/credential')" # 1PasswordWRAPPER — a command prefix in the manifest, with {profile_dir} / {config_dir} / {data_dir}
substitution, for tools that inject env into a child process:
ocp create work --wrapper 'op run --no-masking --env-file={profile_dir}/secrets.env --'| Command | Description |
|---|---|
ocp create <name> [flags] |
Create a self-contained profile |
ocp list |
List profiles; marks the default and the one active here |
ocp current |
Print the profile active in this directory |
ocp resolve [dir] |
Show the resolved profile, its source, and paths |
ocp use <name> |
Set the global default profile |
ocp rename <old> <new> |
Rename a profile (repoints the default if it pointed at it) |
ocp launch [-p name] -- <args> |
Run opencode under a profile (isolated env) |
ocp pin <name> [dir] |
Write a .ocprofile |
ocp edit <name> [--what …] |
Open a profile's config / manifest / env / data |
ocp path <name> |
Print a profile's directories |
ocp remove <name> [--purge-data] |
Remove a profile |
ocp init-shell [flags] |
Print shell integration to eval |
ocp self-update [version] |
Update ocp to the latest release (or a specific version) |
ocp migrate [--check] |
Migrate on-disk state to the current schema version (usually automatic) |
create flags: --from <dir>, --seed-auth, --env-file <file>, --wrapper <cmd>, --description <text>,
--force. Run ocp <command> --help for details.
~/.config/ocp/
├── ocp.json # managed state: $schema, version, default profile
└── profiles/<name>/
├── profile.env # manifest: DESCRIPTION, WRAPPER, DEFAULT_ARGS
├── env # optional: sourced before launch
├── config/ # OPENCODE_CONFIG_DIR (opencode.json, agents, skills, …)
└── data/
├── opencode/ # XDG_DATA_HOME (auth.json, sessions, …)
└── state/ # XDG_STATE_HOME (prompt history, recent models, …)
Override the root with OCP_HOME. omo config lives outside this tree, in ~/.omo/omo.jsonc under
profiles.<name>.
The script is generated with bashly. Edit files under src/, then:
bashly generate # development build
bashly generate --env production # slimmer release buildThe generated ocp is not committed — it is a build artifact produced by bashly generate and
attached to each GitHub release by CI. Never hand-edit it. Help-text colors live in settings.yml;
runtime color follows your terminal and the NO_COLOR standard.
src/bashly.yml's version: field is the single source of truth. To cut a release:
# 1. bump `version:` in src/bashly.yml, then regenerate and commit
bashly generate
git commit -am "release 1.2.3"
# 2. tag with the exact version (no v prefix) and push the tag
git tag 1.2.3
git push origin main 1.2.3Pushing the tag triggers release.yml: it verifies the tag matches
src/bashly.yml, builds the production ocp with bashly, and publishes a GitHub Release with ocp
attached. The docs site redeploys to GitHub Pages automatically on every push to main that touches
docs/.
rm ~/.local/bin/ocp # remove the command
rm -rf ~/.config/ocp # remove all profiles, auth, and sessions (destructive)Then remove the # ocp block from your shell rc.
MIT © xterr
