Skip to content

Instantly share code, notes, and snippets.

@inooid
Forked from locxter/oc-profile.md
Created April 1, 2026 09:51
Show Gist options
  • Select an option

  • Save inooid/39ead937d188a2847a2bf9a5be338467 to your computer and use it in GitHub Desktop.

Select an option

Save inooid/39ead937d188a2847a2bf9a5be338467 to your computer and use it in GitHub Desktop.
OpenCode Profile Switcher

OpenCode Profile Switcher — Installation & Usage Guide

A POSIX shell script for switching between multiple opencode authentication accounts on the same provider — without manually editing auth.json every time.


How the Script Works

The Problem It Solves

opencode stores your login credentials in a single file:

~/.local/share/opencode/auth.json

To switch accounts you currently have to: quit opencode → edit auth.json → reopen opencode. This script automates that by maintaining named profile snapshots of auth.json alongside the live file.

File Layout

~/.local/share/opencode/
├── auth.json               ← live file opencode reads
├── current_profile.txt     ← tracks which profile is active (managed by the script)
├── auth-work.json          ← example saved profile "work"
├── auth-personal.json      ← example saved profile "personal"
└── auth-client-acme.json   ← example saved profile "client-acme"

Profile names may only contain letters, numbers, hyphens, and underscores (e.g. work, personal, client-acme).

What Each Part of the Script Does

Function Purpose
get_paths Sets the base directory (~/.local/share/opencode by default, or $OPENCODE_PROFILE_BASE_DIR) and derives all file paths from it
ensure_base_dir Creates the base directory if it doesn't exist yet
load_profiles Scans for auth-*.json files and builds a sorted list of profile names
get_current_status Reads current_profile.txt to report which profile is active, and cross-checks that the corresponding auth-<name>.json file still exists
validate_profile_name Rejects empty names or names with special characters (spaces, dots, slashes, etc.)
switch_to_profile Copies auth-<name>.jsonauth.json and records the name in current_profile.txt. Also warns if opencode is currently running
save_current_to_profile Copies auth.jsonauth-<name>.json and records the name in current_profile.txt
interactive_menu Presents the numbered menu when no CLI arguments are given
main Entry point — routes to interactive mode or a direct CLI subcommand

Safety

  • No network access — the script never contacts any server.
  • No elevated privileges — it only reads and writes files inside your home directory.
  • No package dependencies — it is pure POSIX sh; it works on macOS (zsh/dash/bash), Linux, and BSD out of the box.
  • Non-destructive saves — saving a profile copies auth.json; it does not delete or truncate anything.
  • Overwrite guard — the script asks for confirmation before overwriting an existing profile.
  • opencode running check — if opencode is detected as a running process, the script warns you to close it before switching (opencode reads auth.json on launch, so switching while it is open has no effect).
  • ⚠️ auth.json contains credentials — the profile files hold the same sensitive data as auth.json. The script stores them in ~/.local/share/opencode/ (mode 700 by default on macOS). If you want extra protection, see Security Hardening below.

Installation

1. Download

Save the script anywhere on your $PATH. A good location on macOS:

# Create a personal bin directory if you don't have one
mkdir -p ~/.local/bin

# Copy the script there
cp oc-profiles.sh ~/.local/bin/oc-profiles
chmod +x ~/.local/bin/oc-profiles

Make sure ~/.local/bin is in your PATH. Add this to your ~/.zshrc (or ~/.bash_profile) if it isn't already:

export PATH="$HOME/.local/bin:$PATH"

Then reload your shell:

source ~/.zshrc   # or: source ~/.bash_profile

2. Verify

oc-profiles --help

Usage

Interactive Mode (recommended for beginners)

Run with no arguments to get the numbered menu:

oc-profiles

You will see something like:

=== OpenCode Profile Switcher ===
Base directory : /Users/you/.local/share/opencode
Current profile: work

1) Switch to a profile
2) Save current auth.json to a profile
3) List all profiles
q) Quit

Choose an action [1-3, q]:

CLI Subcommands (for scripts and power users)

Save the current auth.json as a named profile

oc-profiles save work
oc-profiles save personal
oc-profiles save client-acme

Do this once per account after logging in through opencode normally. You only need to do it once per account — from then on the script can switch between them instantly.

Switch to a saved profile

Always quit opencode first, then switch, then reopen opencode.

oc-profiles switch personal

List all saved profiles

oc-profiles list

Output:

Current profile: work

Available profiles:
  * work          (active)
    personal
    client-acme

Show the current profile name only

oc-profiles current

Typical Workflow: Setting Up Multiple Accounts

# Step 1 — Log in to your first account via opencode normally, then save it
oc-profiles save work

# Step 2 — Log out of that account in opencode and log in to your second account
# (or manually replace auth.json with credentials for the second account)
oc-profiles save personal

# Step 3 — From now on, to switch accounts:
#   a) Quit opencode
#   b) Run:
oc-profiles switch personal
#   c) Reopen opencode — it will use the personal account

Security Hardening

The profile files contain the same credentials as auth.json. If you want to make them harder to read by other processes or users on the same machine:

# Lock down the opencode directory to your user only
chmod 700 ~/.local/share/opencode

# Lock down each profile file
chmod 600 ~/.local/share/opencode/auth-*.json

Custom Base Directory

If you store opencode data elsewhere, override the path:

OPENCODE_PROFILE_BASE_DIR="/Volumes/EncryptedDrive/opencode" oc-profiles list

You can make this permanent by adding to your shell config:

export OPENCODE_PROFILE_BASE_DIR="/Volumes/EncryptedDrive/opencode"

Troubleshooting

Symptom Fix
Profile not found: 'xyz' Run opencode-profiles list to see available profile names (case-sensitive)
No auth.json found Log in via opencode first so it creates an auth.json, then save it as a profile
Switched profile but opencode still shows the old account You switched while opencode was running — quit it, switch, then reopen
current_profile.txt says a profile name but the file is missing The profile file was deleted manually; save it again with opencode-profiles save <name>
Permission denied writing to the directory Check that you own ~/.local/share/opencode (ls -la ~/.local/share/)

Uninstalling

# Remove the script
rm ~/.local/bin/oc-profiles

# Optionally remove the profile files (keeps auth.json intact)
rm ~/.local/share/opencode/auth-*.json
rm ~/.local/share/opencode/current_profile.txt

Quick Reference

oc-profiles                  # interactive menu
oc-profiles save <name>      # save current auth.json as a profile
oc-profiles switch <name>    # switch to a saved profile
oc-profiles list             # list all profiles + current
oc-profiles current          # print current profile name only
oc-profiles --help           # show usage
#!/bin/sh
# Switch between different opencode auth profiles under ~/.local/share/opencode/auth.json
# Profiles are saved as auth-<profile_name>.json in the same directory.
# The currently active profile name is tracked in current_profile.txt.
#
# Usage:
# ./oc-profiles.sh # interactive menu
# ./oc-profiles.sh switch <profile> # switch directly
# ./oc-profile.sh save <profile> # save current auth to profile
# ./oc-profile.sh list # list available profiles
# ./oc-profile.sh current # show current profile
# --------------------------------------------------------------------------- #
# Utility helpers
# --------------------------------------------------------------------------- #
die() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
info() {
printf '%s\n' "$1"
}
warn() {
printf 'Warning: %s\n' "$1" >&2
}
# --------------------------------------------------------------------------- #
# Path / state setup
# --------------------------------------------------------------------------- #
get_paths() {
BASE_DIR=${OPENCODE_PROFILE_BASE_DIR:-"$HOME/.local/share/opencode"}
AUTH_FILE="$BASE_DIR/auth.json"
CURRENT_PROFILE_FILE="$BASE_DIR/current_profile.txt"
PROFILES=
PROFILE_COUNT=0
CURRENT_RECORDED_PROFILE=
VALID_CURRENT_PROFILE=
CURRENT_STATUS=
}
ensure_base_dir() {
if [ ! -d "$BASE_DIR" ]; then
mkdir -p "$BASE_DIR" || die "Failed to create base directory: $BASE_DIR"
fi
}
# --------------------------------------------------------------------------- #
# Profile list loading
# --------------------------------------------------------------------------- #
load_profiles() {
PROFILES=
PROFILE_COUNT=0
for path in "$BASE_DIR"/auth-*.json; do
[ -f "$path" ] || continue
name=$(basename "$path")
name=${name#auth-}
name=${name%.json}
if [ -z "$PROFILES" ]; then
PROFILES="$name"
else
PROFILES=$(printf '%s\n%s' "$PROFILES" "$name")
fi
done
if [ -n "$PROFILES" ]; then
PROFILES=$(printf '%s\n' "$PROFILES" | sort)
# Count lines safely
PROFILE_COUNT=$(printf '%s\n' "$PROFILES" | wc -l | tr -d ' ')
fi
}
# --------------------------------------------------------------------------- #
# Current profile status
# --------------------------------------------------------------------------- #
get_current_status() {
CURRENT_RECORDED_PROFILE=
VALID_CURRENT_PROFILE=
CURRENT_STATUS=
if [ -f "$CURRENT_PROFILE_FILE" ]; then
IFS= read -r CURRENT_RECORDED_PROFILE < "$CURRENT_PROFILE_FILE" || CURRENT_RECORDED_PROFILE=
if [ -n "$CURRENT_RECORDED_PROFILE" ] && [ -f "$BASE_DIR/auth-$CURRENT_RECORDED_PROFILE.json" ]; then
VALID_CURRENT_PROFILE="$CURRENT_RECORDED_PROFILE"
CURRENT_STATUS="$CURRENT_RECORDED_PROFILE"
elif [ -n "$CURRENT_RECORDED_PROFILE" ]; then
CURRENT_STATUS="$CURRENT_RECORDED_PROFILE (profile file missing)"
else
CURRENT_STATUS="unset (empty tracking file)"
fi
elif [ -f "$AUTH_FILE" ]; then
CURRENT_STATUS="unknown (auth.json exists but no profile was ever saved via this tool)"
else
CURRENT_STATUS="unset (no auth.json)"
fi
}
# --------------------------------------------------------------------------- #
# Validation
# --------------------------------------------------------------------------- #
validate_profile_name() {
_name=$1
case $_name in
'')
info 'Profile name cannot be empty.'
return 1
;;
*[!A-Za-z0-9_-]*)
info 'Profile name may only contain letters, numbers, underscores, and hyphens.'
return 1
;;
*)
return 0
;;
esac
}
# --------------------------------------------------------------------------- #
# Core operations
# --------------------------------------------------------------------------- #
write_current_profile() {
printf '%s\n' "$1" > "$CURRENT_PROFILE_FILE" \
|| die 'Failed to update current_profile.txt'
}
switch_to_profile() {
_profile=$1
_source="$BASE_DIR/auth-$_profile.json"
[ -f "$_source" ] || die "Profile not found: '$_profile'. Run with 'list' to see available profiles."
# Warn if opencode may be running (best-effort; pgrep may not exist everywhere)
if command -v pgrep > /dev/null 2>&1 && pgrep -x opencode > /dev/null 2>&1; then
warn "opencode appears to be running. Close it before switching profiles, then restart it."
fi
cp "$_source" "$AUTH_FILE" || die "Failed to copy profile to auth.json"
write_current_profile "$_profile"
info "Switched to profile '$_profile'."
}
save_current_to_profile() {
_profile=$1
_target="$BASE_DIR/auth-$_profile.json"
[ -f "$AUTH_FILE" ] || die "No auth.json found at '$AUTH_FILE'. Nothing to save."
cp "$AUTH_FILE" "$_target" || die "Failed to save profile '$_profile'"
write_current_profile "$_profile"
info "Saved current auth.json to profile '$_profile'."
}
# --------------------------------------------------------------------------- #
# CLI mode (non-interactive)
# --------------------------------------------------------------------------- #
cmd_list() {
load_profiles
get_current_status
info "Current profile: $CURRENT_STATUS"
info ""
if [ "$PROFILE_COUNT" -eq 0 ]; then
info "No saved profiles found in $BASE_DIR"
info "Tip: run with 'save <name>' to save your current auth.json as a profile."
else
info "Available profiles:"
printf '%s\n' "$PROFILES" | while IFS= read -r p; do
if [ "$p" = "$VALID_CURRENT_PROFILE" ]; then
printf ' * %s (active)\n' "$p"
else
printf ' %s\n' "$p"
fi
done
fi
}
cmd_current() {
get_current_status
info "$CURRENT_STATUS"
}
cmd_switch() {
_target=$1
[ -n "$_target" ] || die "Usage: $0 switch <profile_name>"
validate_profile_name "$_target" || exit 1
switch_to_profile "$_target"
}
cmd_save() {
_name=$1
[ -n "$_name" ] || die "Usage: $0 save <profile_name>"
validate_profile_name "$_name" || exit 1
_target="$BASE_DIR/auth-$_name.json"
if [ -f "$_target" ]; then
printf "Profile '%s' already exists. Overwrite? [y/N]: " "$_name"
IFS= read -r _ans || _ans=
case $_ans in
y|yes) ;;
*) info "Aborted."; exit 0 ;;
esac
fi
save_current_to_profile "$_name"
}
# --------------------------------------------------------------------------- #
# Interactive menu
# --------------------------------------------------------------------------- #
print_header() {
printf '\n=== OpenCode Profile Switcher ===\n'
printf 'Base directory : %s\n' "$BASE_DIR"
printf 'Current profile: %s\n\n' "$CURRENT_STATUS"
}
print_profile_list() {
if [ "$PROFILE_COUNT" -eq 0 ]; then
return 0
fi
_idx=1
printf '%s\n' "$PROFILES" | while IFS= read -r _p; do
if [ "$_p" = "$VALID_CURRENT_PROFILE" ]; then
printf '%s) %s (active)\n' "$_idx" "$_p"
else
printf '%s) %s\n' "$_idx" "$_p"
fi
_idx=$((_idx + 1))
done
}
prompt_profile_selection() {
if [ "$PROFILE_COUNT" -eq 0 ]; then
info "No saved profiles found. Use option 2 to save the current auth.json first."
return 1
fi
print_profile_list
while :; do
printf 'Select a profile [1-%s, q]: ' "$PROFILE_COUNT"
IFS= read -r _sel || return 1
case $_sel in
q|quit) return 1 ;;
''|*[!0-9]*)
info 'Please enter a number or q.'
;;
*)
if [ "$_sel" -lt 1 ] || [ "$_sel" -gt "$PROFILE_COUNT" ]; then
info "Please enter a number from 1 to $PROFILE_COUNT."
else
_idx=1
_chosen=
printf '%s\n' "$PROFILES" | while IFS= read -r _p; do
if [ "$_idx" -eq "$_sel" ]; then
printf '%s' "$_p"
fi
_idx=$((_idx + 1))
done | { IFS= read -r _chosen && switch_to_profile "$_chosen"; }
return 0
fi
;;
esac
done
}
prompt_save_profile_name() {
[ -f "$AUTH_FILE" ] || { info "No auth.json found. Nothing to save."; return 1; }
while :; do
if [ -n "$VALID_CURRENT_PROFILE" ]; then
printf 'Profile name [%s]: ' "$VALID_CURRENT_PROFILE"
else
printf 'Profile name: '
fi
IFS= read -r _name || return 1
case $_name in
q|quit) return 1 ;;
esac
if [ -z "$_name" ] && [ -n "$VALID_CURRENT_PROFILE" ]; then
_name="$VALID_CURRENT_PROFILE"
fi
validate_profile_name "$_name" || continue
_target="$BASE_DIR/auth-$_name.json"
if [ -f "$_target" ]; then
printf "Profile '%s' already exists. Overwrite? [y/N]: " "$_name"
IFS= read -r _ans || return 1
case $_ans in
y|yes) ;;
q|quit) return 1 ;;
*) continue ;;
esac
fi
save_current_to_profile "$_name"
return 0
done
}
interactive_menu() {
while :; do
load_profiles
get_current_status
print_header
printf '1) Switch to a profile\n'
printf '2) Save current auth.json to a profile\n'
printf '3) List all profiles\n'
printf 'q) Quit\n'
printf '\nChoose an action [1-3, q]: '
IFS= read -r _action || _action=q
printf '\n'
case $_action in
1) prompt_profile_selection || : ;;
2) prompt_save_profile_name || : ;;
3) cmd_list ;;
q|quit) info "Goodbye."; exit 0 ;;
*) info "Invalid choice. Please enter 1, 2, 3, or q." ;;
esac
done
}
# --------------------------------------------------------------------------- #
# Entry point
# --------------------------------------------------------------------------- #
main() {
get_paths
ensure_base_dir
case ${1:-} in
switch) cmd_switch "${2:-}" ;;
save) cmd_save "${2:-}" ;;
list) cmd_list ;;
current) cmd_current ;;
''|-i) interactive_menu ;;
-h|--help)
printf 'Usage: %s [switch|save|list|current] [profile_name]\n' "$0"
printf '\n (no args) interactive menu\n'
printf ' switch <name> switch to a saved profile\n'
printf ' save <name> save current auth.json as a profile\n'
printf ' list list all profiles\n'
printf ' current print current profile name\n'
exit 0
;;
*)
die "Unknown command: '$1'. Run with --help for usage."
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment