Apply agent account selection to CLI subprocesses (#14233)

* Apply agent account selection to CLI subprocesses

* Fix account dispatch edge cases and remove redundant shell wrappers

* Use the current account when launching a new agent session
This commit is contained in:
David Heinemeier Hansson authored and GitHub committed 2026-10-04 09:01:06 -04:00
1 parent 5657a91f8d
commit f9646299bf
15 files changed
+351 -67

No files matched your search

+15 -7
View File
@@ -136,19 +136,27 @@ pi)
;;
esac
# Claude, Codex, and Grok start as the active subscription account, the same
# as the shell functions in default/bash/fns/agent-accounts. The home travels
# in the command itself, since a terminal launched through uwsm doesn't
# inherit this process's environment.
# A new session uses the current selection rather than its parent's account
# pin. Explicit provider homes still override the selection. Carry the new pin
# in the command, since terminals launched through uwsm don't inherit our env.
case "$agent" in
claude) home_env="CLAUDE_CONFIG_DIR" ;;
codex) home_env="CODEX_HOME" ;;
grok) home_env="GROK_HOME" ;;
*) home_env="" ;;
esac
if [[ -n $home_env && -z ${!home_env:-} ]]; then
home=$(omarchy-agent-account-home "$agent")
[[ -n $home ]] && command=(env "$home_env=$home" "${command[@]}")
if [[ -n $home_env ]]; then
account_pin="OMARCHY_AGENT_${agent^^}_HOME"
if [[ -v $account_pin && ${!home_env:-} == "${!account_pin}" ]]; then
unset "$home_env"
fi
unset "$account_pin"
if [[ -z ${!home_env:-} ]]; then
home=$(omarchy-agent-account-home "$agent")
account_env=(env -u "$home_env" "$account_pin=$home")
[[ -n $home ]] && account_env+=("$home_env=$home")
command=("${account_env[@]}" "${command[@]}")
fi
fi
if [[ $inline == "true" ]]; then
+1 -1
View File
@@ -138,7 +138,7 @@ esac
# The CLI's own home, whatever this shell has pointed it at.
login() {
interruptible env -u CLAUDE_CONFIG_DIR -u CODEX_HOME -u GROK_HOME "${login_command[@]}"
interruptible env -u CLAUDE_CONFIG_DIR -u CODEX_HOME -u GROK_HOME "OMARCHY_AGENT_${provider^^}_HOME=" "${login_command[@]}"
}
# Signing an account that's already here in again, where it lives: the
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# omarchy:summary=Run Claude, Codex, or Grok as the selected subscription account
# omarchy:args=<claude|codex|grok> [args...]
# omarchy:hidden=true
set -euo pipefail
case "${1:-}" in
claude) account_env="CLAUDE_CONFIG_DIR" ;;
codex) account_env="CODEX_HOME" ;;
grok) account_env="GROK_HOME" ;;
*)
echo "Usage: omarchy-agent-account-exec <claude|codex|grok> [args...]" >&2
exit 1
;;
esac
provider=$1
shift
# mise removes its wrapper dispatch directories before calling us, so exec
# finds the real CLI (or its first-run installer) rather than this wrapper.
# Pin the resolved home for children, including an implicit Main account.
# Explicit provider homes still take precedence.
if [[ -z ${!account_env:-} ]]; then
account_home=$(omarchy-agent-account-home "$provider")
export "OMARCHY_AGENT_${provider^^}_HOME=$account_home"
if [[ -n $account_home ]]; then
export "$account_env=$account_home"
fi
fi
# Resolve before restoring dispatch on PATH: mise removes the wrapper paths to
# avoid recursion, but the agent's subprocesses need them for other providers.
if ! agent_binary=$(type -P "$provider"); then
# A mise dispatcher can exist before its CLI or first-run installer does.
export MISE_MINIMUM_RELEASE_AGE=0
mise use -g --quiet "$provider"
agent_binary=$(mise which "$provider")
fi
export PATH="${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/command-wrappers/bin:$PATH"
exec "$agent_binary" "$@"
+7
View File
@@ -13,6 +13,13 @@ registry="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/agents/accounts/${1:-}.j
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
echo "Usage: omarchy-agent-account-home <claude|codex|grok>" >&2
exit 1
fi
# A running session pins its home, including an empty value for Main. Claude's
# implicit default also selects ~/.claude.json, so Main must stay implicit.
account_pin="OMARCHY_AGENT_${1^^}_HOME"
if [[ -v $account_pin ]]; then
printf '%s' "${!account_pin}"
elif [[ -f $registry ]]; then
exec omarchy-agent-account-state home "$1"
fi
+21
View File
@@ -25,6 +25,27 @@ if [ "$OMARCHY_PATH" != /usr/share/omarchy ]; then
esac
fi
# mise's command wrappers must precede real tool directories inherited from a
# parent shell, so scripts follow account switches without Bash functions.
# mise activate also keeps this directory ahead of its managed tool binaries.
omarchy_wrapper_dir="${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/command-wrappers/bin"
omarchy_wrapper_path=$omarchy_wrapper_dir
if [ -n "${PATH:-}" ]; then
omarchy_remaining_path=$PATH
while :; do
omarchy_path_entry=${omarchy_remaining_path%%:*}
if [ "$omarchy_path_entry" != "$omarchy_wrapper_dir" ]; then
omarchy_wrapper_path="$omarchy_wrapper_path:$omarchy_path_entry"
fi
case "$omarchy_remaining_path" in
*:*) omarchy_remaining_path=${omarchy_remaining_path#*:} ;;
*) break ;;
esac
done
fi
PATH=$omarchy_wrapper_path
unset omarchy_wrapper_dir omarchy_wrapper_path omarchy_remaining_path omarchy_path_entry
# User-level tool paths, appended so system binaries keep precedence. This is
# what lets login shells (ssh, bash -lc) and the uwsm session find mise-managed
# tools without an interactive rc. Keep these directories in sync with the PAM
-35
View File
@@ -1,35 +0,0 @@
# Start Claude Code, Codex, and Grok as the active subscription account, so
# `cx`, `cy`, `tdl` and a plain `claude` all follow `omarchy agent account use`.
# An explicitly set CLAUDE_CONFIG_DIR / CODEX_HOME / GROK_HOME still wins.
claude() {
local home=""
[[ -z ${CLAUDE_CONFIG_DIR:-} ]] && home=$(omarchy-agent-account-home claude)
if [[ -n $home ]]; then
CLAUDE_CONFIG_DIR="$home" command claude "$@"
else
command claude "$@"
fi
}
codex() {
local home=""
[[ -z ${CODEX_HOME:-} ]] && home=$(omarchy-agent-account-home codex)
if [[ -n $home ]]; then
CODEX_HOME="$home" command codex "$@"
else
command codex "$@"
fi
}
grok() {
local home=""
[[ -z ${GROK_HOME:-} ]] && home=$(omarchy-agent-account-home grok)
if [[ -n $home ]]; then
GROK_HOME="$home" command grok "$@"
else
command grok "$@"
fi
}
+1 -4
View File
@@ -172,10 +172,7 @@ Single source of truth for `OMARCHY_PATH` and dev-link-aware `PATH`. It:
- Prepends `$OMARCHY_PATH/bin` to `PATH` **only when** `OMARCHY_PATH` is
not `/usr/share/omarchy`. On a production install the binaries are
already on `PATH` as `/usr/bin/omarchy-*` via the `omarchy` package.
- Appends `~/.local/share/mise/shims` and `~/.local/bin` so login shells and
the uwsm session find mise-managed tools — kept in sync with the PAM `PATH`
line written by `install/config/ssh-command-path.sh`, which covers SSH
commands that run no shell setup at all.
- Prepends mise's `command-wrappers/bin` so subscription account dispatch runs before inherited tool binaries, then appends `~/.local/share/mise/shims` and `~/.local/bin` so login shells and the uwsm session find mise-managed tools. The existing mise tool shims also dispatch these wrappers for SSH commands that run no shell setup; the PAM path needs no additional entry. `etc/mise/conf.d/omarchy-agent-accounts.toml` declares the Claude, Codex, and Grok wrappers; `mise reshim` builds them during user setup and migration. Each invocation resolves the selected account through `omarchy-agent-account-exec`, while an explicit provider home takes precedence. Credentials and running sessions remain in their original account homes.
Sourced by every entry point that needs the env set:
@@ -0,0 +1,13 @@
# Resolve the active subscription at execution time, including in subprocesses.
# User and project mise config can override these system defaults.
[wrappers.claude]
command = "omarchy-agent-account-exec"
args = ["claude"]
[wrappers.codex]
command = "omarchy-agent-account-exec"
args = ["codex"]
[wrappers.grok]
command = "omarchy-agent-account-exec"
args = ["grok"]
+3
View File
@@ -24,3 +24,6 @@ omarchy-mise-install github:OpenRouterLabs/ori-releases ori
if omarchy-cmd-missing muse; then
omarchy-mise-install "http:muse[url=https://api.meta.ai/muse-launcher.sh,bin=muse,version_list_url=https://api.meta.ai/muse-code/channels/muse-stable,version_json_path=.version]" muse
fi
# Build the account dispatchers declared by /etc/mise/conf.d.
mise reshim
+2 -2
View File
@@ -46,11 +46,11 @@ Left-click the bar icon for the panel, right-click to launch your default agent.
If you pay for more than one Claude, Codex, or Grok plan — a personal Max and a work one, say — Omarchy can keep them all signed in and move between them. Add a subscription with the + in the agents panel, which walks you through it right there, or with `omarchy agent account add claude Work` (or `codex`, or `grok`). It installs the CLI if it's missing. The first account for a provider signs in through your normal browser, right where the CLI always looks. Every one after that signs in through a private window, so the account your browser is already signed in to doesn't get picked up by mistake; sign in there as the account you're adding, and close any private window you already had open first. Never log out of the CLI to do this: logging out revokes the saved sign-in, and that account would have to be added again.
One account per provider is active, and every new session starts as it: `cx`, `cy`, a plain `claude`, `codex`, or `grok`, and anything launched through `omarchy agent`. Switch with `omarchy agent account use work` (and back with `omarchy agent account use main`), or in the panel by pressing an account's number and then `Enter` (or clicking its Use button). Sessions that are already running stay on the account they started with. Conversation history, settings, skills, and plugins are shared across accounts, so `claude --continue` in the same folder picks up where you left off on the new account.
One account per provider is active, and every new session starts as it: `cx`, `cy`, a plain `claude`, `codex`, or `grok`, anything launched through `omarchy agent`, and scripts or editors that find the CLI on `PATH`. Switch with `omarchy agent account use work` (and back with `omarchy agent account use main`), or in the panel by pressing an account's number and then `Enter` (or clicking its Use button). Sessions that are already running stay on the account they started with. Conversation history, settings, skills, and plugins are shared across accounts, so `claude --continue` in the same folder picks up where you left off on the new account.
With more than one account, the panel shows each account's limits side by side, with the active one marked. By default Omarchy notifies you when the active account passes 95% of a limit, and clicking the notification switches to the account with the most headroom. Hover an account's line in the panel and click the Autoswitch that appears beside Use (or press `m`), or run `omarchy agent account mode auto`, to have it switch on its own instead; while it's on, Autoswitch takes Use's place, and clicking it again goes back to notifying. Add a number to change the threshold, as in `omarchy agent account mode auto 90`. If every account is over the threshold, you'll hear about it once, along with which account frees up first. While an active account is within 15 points of its threshold (80% at the default), its limits are checked every three minutes rather than every 15.
`omarchy agent account list` shows every account and its latest limits, `omarchy agent account remove work` forgets one, and `omarchy agent account rename primary Hey` renames one (or click an account's name in the panel). These commands work on your default agent's accounts; to reach the other provider, name it first, as in `omarchy agent account use codex side`. The login you already had in `~/.claude` or `~/.codex` is the _Main_ account and stays where it is. An account left unused for more than about eight hours shows its last known limits until a session runs in it again. MCP servers you had when an account was added come along to it, but ones you add later with `claude mcp add` only reach the account you added them in. Claude Desktop and editors that start `claude` themselves don't follow the switch.
`omarchy agent account list` shows every account and its latest limits, `omarchy agent account remove work` forgets one, and `omarchy agent account rename primary Hey` renames one (or click an account's name in the panel). These commands work on your default agent's accounts; to reach the other provider, name it first, as in `omarchy agent account use codex side`. The login you already had in `~/.claude` or `~/.codex` is the _Main_ account and stays where it is. An account left unused for more than about eight hours shows its last known limits until a session runs in it again. MCP servers you had when an account was added come along to it, but ones you add later with `claude mcp add` only reach the account you added them in. Claude Desktop and tools configured with an absolute path to a CLI binary do not follow the switch. An explicit `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `GROK_HOME` takes precedence over the selected account.
### Crash diagnosis
+5
View File
@@ -0,0 +1,5 @@
echo "Make agent account selection apply to scripts and subprocesses"
# The settings package ships the mise wrapper declarations. Build the dispatch
# links for this user without installing or changing any agent version.
mise reshim
+206
View File
@@ -0,0 +1,206 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command mise
require_command python3
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
account_test_home="$test_tmp/home"
real_bin="$test_tmp/real/bin"
mise_data="$test_tmp/data/mise"
registry_dir="$account_test_home/.local/state/omarchy/agents/accounts"
mkdir -p "$account_test_home" "$real_bin" "$registry_dir"
# Use real mise dispatch and real account resolution, with fake agent binaries.
# No shell functions, login startup files, credentials, or network are involved.
for provider in claude codex grok; do
cat >"$real_bin/$provider" <<'SH'
#!/bin/bash
case "${0##*/}" in
claude) printf '%s\n' "${CLAUDE_CONFIG_DIR:-default}" ;;
codex) printf '%s\n' "${CODEX_HOME:-default}" ;;
grok) printf '%s\n' "${GROK_HOME:-default}" ;;
esac
if (($#)); then
printf '%s\n' "$@"
fi
if [[ ${OMARCHY_TEST_SWITCH_ACCOUNT:-} == "yes" ]]; then
unset OMARCHY_TEST_SWITCH_ACCOUNT
omarchy-agent-account-state use "${0##*/}" "${OMARCHY_TEST_SWITCH_TO:-side}" >/dev/null
if [[ ${OMARCHY_TEST_NEW_SESSION:-} == "yes" ]]; then
omarchy-agent --inline
else
"${0##*/}"
fi
fi
if [[ -n ${OMARCHY_TEST_AGENT_CHILD:-} ]]; then
child=$OMARCHY_TEST_AGENT_CHILD
unset OMARCHY_TEST_AGENT_CHILD
"$child"
fi
exit "${OMARCHY_TEST_AGENT_EXIT:-0}"
SH
chmod +x "$real_bin/$provider"
done
run_isolated() {
env -i HOME="$account_test_home" \
XDG_CONFIG_HOME="$test_tmp/config" XDG_DATA_HOME="$test_tmp/data" XDG_CACHE_HOME="$test_tmp/cache" \
MISE_DATA_DIR="$mise_data" MISE_SYSTEM_CONFIG_DIR="$ROOT/etc/mise" MISE_OFFLINE=1 \
OMARCHY_PATH="$ROOT" PATH="$mise_data/command-wrappers/bin:$ROOT/bin:$real_bin:/usr/bin" \
"$@"
}
select_account() {
local provider=$1
local selected=$2
printf '{"active":"%s","accounts":[{"id":"main","primary":true},{"id":"side","home":"%s"}]}\n' \
"$selected" "$test_tmp/accounts/$provider side" >"$registry_dir/$provider.json"
}
for helper in omarchy-agent-usage-update omarchy-notification-send; do
printf '#!/bin/bash\nexit 0\n' >"$real_bin/$helper"
chmod +x "$real_bin/$helper"
done
cd "$test_tmp"
run_isolated mise reshim
for provider in claude codex grok; do
select_account "$provider" side
account_dir="$test_tmp/accounts/$provider side"
output=$(run_isolated "$provider" 'a spaced prompt' '' '--flag=$literal')
expected=$(printf '%s\n' "$account_dir" 'a spaced prompt' '' '--flag=$literal')
[[ $output == "$expected" ]] || fail "$provider dispatch selects the active account and preserves arguments" "$output"
pass "$provider dispatch selects the active account and preserves arguments"
case "$provider" in
claude) account_env=CLAUDE_CONFIG_DIR ;;
codex) account_env=CODEX_HOME ;;
grok) account_env=GROK_HOME ;;
esac
output=$(run_isolated env "$account_env=/explicit/account" "$provider")
[[ $output == /explicit/account ]] || fail "$provider preserves an explicit account home" "$output"
pass "$provider preserves an explicit account home"
select_account "$provider" main
output=$(run_isolated "$provider")
[[ $output == default ]] || fail "$provider picks up switching back to Main without rebuilding shims" "$output"
pass "$provider picks up switching back to Main without rebuilding shims"
output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes "$provider")
[[ $output == $'default\ndefault' ]] || fail "$provider Main sessions keep their account after a switch" "$output"
pass "$provider Main sessions keep their account after a switch"
mkdir -p "$account_test_home/.config/omarchy/defaults"
printf '%s\n' "$provider" >"$account_test_home/.config/omarchy/defaults/agent"
select_account "$provider" main
output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes OMARCHY_TEST_NEW_SESSION=yes "$provider")
[[ $(sed -n '2p' <<<"$output") == "$account_dir" ]] || fail "$provider new sessions follow a switch from Main" "$output"
pass "$provider new sessions follow a switch from Main"
output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes OMARCHY_TEST_SWITCH_TO=main OMARCHY_TEST_NEW_SESSION=yes "$provider")
[[ $(sed -n '2p' <<<"$output") == default ]] || fail "$provider new sessions follow a switch to Main" "$output"
pass "$provider new sessions follow a switch to Main"
select_account "$provider" side
output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes OMARCHY_TEST_SWITCH_TO=main "$provider")
[[ $output == "$(printf '%s\n' "$account_dir" "$account_dir")" ]] || fail "$provider side sessions keep their account after a switch" "$output"
pass "$provider side sessions keep their account after a switch"
output=$(run_isolated env "OMARCHY_AGENT_${provider^^}_HOME=" "$account_env=/explicit/account" omarchy-agent --inline)
[[ ${output%%$'\n'*} == /explicit/account ]] || fail "$provider new sessions preserve explicit account overrides" "$output"
pass "$provider new sessions preserve explicit account overrides"
select_account "$provider" side
output=$(run_isolated omarchy-agent-account-add --reauth :primary "$provider" </dev/null)
grep -Fxq default <<<"$output" || fail "$provider primary reauthentication bypasses the active side account" "$output"
pass "$provider primary reauthentication bypasses the active side account"
rm "$registry_dir/$provider.json"
output=$(run_isolated "$provider")
[[ $output == default ]] || fail "$provider works without an account registry" "$output"
pass "$provider works without an account registry"
done
select_account claude side
output=$(run_isolated python3 -c 'import subprocess; print(subprocess.check_output(["claude", "-p", "review"], text=True), end="")')
[[ $output == "$(printf '%s\n' "$test_tmp/accounts/claude side" -p review)" ]] ||
fail "a Python subprocess follows the selected Claude subscription" "$output"
pass "a Python subprocess follows the selected Claude subscription"
select_account codex side
output=$(run_isolated env OMARCHY_TEST_AGENT_CHILD=codex claude)
expected=$(printf '%s\n' "$test_tmp/accounts/claude side" "$test_tmp/accounts/codex side")
[[ $output == "$expected" ]] || fail "an agent's subprocess still dispatches the other provider's selected account" "$output"
pass "an agent's subprocess still dispatches the other provider's selected account"
# Give mise a real installed Claude tool directory, not just a system fallback.
ln -s "$real_bin/claude" "$test_tmp/real/claude"
run_isolated mise link claude@1.0.0 "$test_tmp/real" >/dev/null
mkdir -p "$test_tmp/config/mise"
printf '[tools]\nclaude = "1.0.0"\n' >"$test_tmp/config/mise/config.toml"
run_isolated mise trust "$test_tmp/config/mise/config.toml" >/dev/null 2>&1
# Existing tool shims used by SSH already dispatch wrappers, including with a
# relocated mise data directory. No additional stock-only PAM path is needed.
run_isolated mise reshim
output=$(run_isolated env PATH="$mise_data/shims:$ROOT/bin:/usr/bin" claude)
[[ $output == "$test_tmp/accounts/claude side" ]] || fail "SSH-style mise tool shims route through account dispatch in a custom data directory" "$output"
pass "SSH-style mise tool shims route through account dispatch in a custom data directory"
# Full PATH activation must also prefer the dispatcher over installed tools.
output=$(run_isolated bash -c 'eval "$(mise env -s bash)"; [[ $PATH == *"/installs/claude/"* ]] || exit 99; claude')
[[ $output == "$test_tmp/accounts/claude side" ]] || fail "mise PATH activation retains account dispatch" "$output"
pass "mise PATH activation retains account dispatch"
status=0
run_isolated env OMARCHY_TEST_AGENT_EXIT=42 claude >/dev/null || status=$?
(( status == 42 )) || fail "account dispatch preserves the agent exit status" "$status"
pass "account dispatch preserves the agent exit status"
# A dispatcher alone must not prevent the CLI from being installed on first use.
mv "$real_bin/grok" "$test_tmp/grok-fixture"
cat >"$real_bin/mise" <<'SH'
#!/bin/bash
case "$1" in
use)
[[ ${OMARCHY_TEST_INSTALL_FAIL:-} != "yes" ]] || exit 42
[[ $* == "use -g --quiet grok" ]] || exit 99
cp "$HOME/../grok-fixture" "$HOME/../real/bin/grok"
;;
which)
[[ ${MISE_MINIMUM_RELEASE_AGE:-} == "0" ]] || exit 99
printf '%s\n' "$HOME/../real/bin/grok"
;;
*) exit 99 ;;
esac
SH
chmod +x "$real_bin/mise"
select_account grok side
output=$(run_isolated grok)
[[ $output == "$test_tmp/accounts/grok side" ]] || fail "a dispatcher without a CLI installs it and uses the selected account" "$output"
pass "a dispatcher without a CLI installs it and uses the selected account"
rm "$real_bin/grok"
status=0
run_isolated env OMARCHY_TEST_INSTALL_FAIL=yes grok >/dev/null || status=$?
(( status == 42 )) || fail "a failed first-run install stops dispatch" "$status"
pass "a failed first-run install stops dispatch"
rm "$real_bin/mise"
# Migration only rebuilds native mise dispatch; account files stay untouched.
registry_before=$(sha256sum "$registry_dir"/*.json)
for attempt in 1 2; do
run_isolated bash -euo pipefail "$ROOT/migrations/1791112479.sh" >/dev/null
for provider in claude codex grok; do
[[ -L $mise_data/command-wrappers/bin/$provider ]] || fail "migration builds every account dispatcher"
done
done
[[ $(sha256sum "$registry_dir"/*.json) == "$registry_before" ]] || fail "migration preserves account state"
pass "migration rebuilds dispatchers idempotently without changing account state"
+5 -7
View File
@@ -201,8 +201,7 @@ OMARCHY_TEST_LOGIN_UUID=u-grok-2 OMARCHY_TEST_LOGIN_EMAIL=side@example.com \
grep -qx -- "--private https://auth.x.ai/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
fail "a second Grok login opens in a private window" "$(cat "$OMARCHY_TEST_BROWSER_LOG")"
omarchy-agent-account-use grok side >/dev/null
source "$ROOT/default/bash/fns/agent-accounts"
[[ $(grok --version) == "grok home=$accounts/grok/side args=--version" ]] || fail "grok starts as the active Grok account" "$(grok --version)"
[[ $(omarchy-agent-account-exec grok --version) == "grok home=$accounts/grok/side args=--version" ]] || fail "grok starts as the active Grok account" "$(omarchy-agent-account-exec grok --version)"
omarchy-agent-account-use grok main >/dev/null
pass "Grok accounts are added and used like the others"
@@ -213,11 +212,10 @@ omarchy-agent-account-use claude work >/dev/null
grep -q "New Claude sessions now use Work (Max 5x)" "$notifications" || fail "switching says where new sessions go"
pass "use makes an account active and says so"
source "$ROOT/default/bash/fns/agent-accounts"
[[ $(claude --version) == "claude home=$work args=--version" ]] || fail "claude at a prompt starts as the active account"
[[ $(CLAUDE_CONFIG_DIR=/elsewhere claude) == "claude home=/elsewhere args=" ]] || fail "an explicit CLAUDE_CONFIG_DIR wins over the active account"
[[ $(codex) == "codex home=default args=" ]] || fail "codex stays on its primary until switched"
pass "shell launches follow the active account"
[[ $(omarchy-agent-account-exec claude --version) == "claude home=$work args=--version" ]] || fail "claude at a prompt starts as the active account"
[[ $(CLAUDE_CONFIG_DIR=/elsewhere omarchy-agent-account-exec claude) == "claude home=/elsewhere args=" ]] || fail "an explicit CLAUDE_CONFIG_DIR wins over the active account"
[[ $(omarchy-agent-account-exec codex) == "codex home=default args=" ]] || fail "codex stays on its primary until switched"
pass "account dispatch follows the active account"
[[ $(OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent --inline) == "claude home=$work args=--permission-mode auto" ]] ||
fail "omarchy-agent starts Claude as the active account"
+6 -6
View File
@@ -781,11 +781,11 @@ assert_launch pi pi "Review this project"
assert_launch omp omp --auto-approve -- "Review this project"
assert_launch opencode opencode --auto --prompt "Review this project"
assert_launch ori ori code --interactive --prompt "Review this project"
assert_launch claude claude --permission-mode auto -- "Review this project"
assert_launch codex codex --approve-for-me -- "Review this project"
assert_launch claude env -u CLAUDE_CONFIG_DIR OMARCHY_AGENT_CLAUDE_HOME= claude --permission-mode auto -- "Review this project"
assert_launch codex env -u CODEX_HOME OMARCHY_AGENT_CODEX_HOME= codex --approve-for-me -- "Review this project"
assert_launch muse muse --approval-mode never -- "Review this project"
assert_launch crush crush run "Review this project"
assert_launch grok grok --permission-mode bypassPermissions -- "Review this project"
assert_launch grok env -u GROK_HOME OMARCHY_AGENT_GROK_HOME= grok --permission-mode bypassPermissions -- "Review this project"
assert_launch cursor-agent cursor-agent --yolo --trust agent -- "Review this project"
assert_launch hermes env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=Review this project"
assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project"
@@ -809,11 +809,11 @@ assert_bypass pi pi
assert_bypass omp omp --auto-approve
assert_bypass opencode opencode --auto
assert_bypass ori ori code
assert_bypass claude claude --permission-mode auto
assert_bypass codex codex --approve-for-me
assert_bypass claude env -u CLAUDE_CONFIG_DIR OMARCHY_AGENT_CLAUDE_HOME= claude --permission-mode auto
assert_bypass codex env -u CODEX_HOME OMARCHY_AGENT_CODEX_HOME= codex --approve-for-me
assert_bypass muse muse --approval-mode never
assert_bypass crush crush --yolo
assert_bypass grok grok --permission-mode bypassPermissions
assert_bypass grok env -u GROK_HOME OMARCHY_AGENT_GROK_HOME= grok --permission-mode bypassPermissions
assert_bypass cursor-agent cursor-agent --yolo --trust
assert_bypass hermes hermes --yolo
assert_bypass agy agy --dangerously-skip-permissions
+23 -5
View File
@@ -4,6 +4,8 @@ set -euo pipefail
source "$(dirname "$0")/base-test.sh"
unset MISE_DATA_DIR XDG_DATA_HOME
run_bootstrap() {
local shell_bin="$1"
local bootstrap="$2"
@@ -56,7 +58,7 @@ pass "env-bootstrap resolves default OMARCHY_PATH"
assert_path_present "$default_path" "$tmpdir/unrelated/bin" "env-bootstrap preserves PATH entries in default mode"
assert_path_present "$default_path" "$home/.local/share/mise/shims" "env-bootstrap appends mise shims"
assert_path_present "$default_path" "$home/.local/bin" "env-bootstrap appends ~/.local/bin"
assert_path_first "$default_path" "$tmpdir/unrelated/bin" "env-bootstrap appends user-level paths after existing entries"
assert_path_first "$default_path" "$home/.local/share/mise/command-wrappers/bin" "env-bootstrap puts command wrappers ahead of inherited tool binaries"
printf 'export OMARCHY_PATH="%s"\n' "$tmpdir/active" >"$tmpdir/omarchy.conf"
mapfile -t linked_result < <(run_bootstrap bash "$bootstrap" "$home" "$tmpdir/unrelated/bin:/usr/bin")
@@ -64,23 +66,39 @@ linked_path=${linked_result[1]}
[[ ${linked_result[0]} == "$tmpdir/active" ]] || fail "env-bootstrap resolves linked OMARCHY_PATH" "actual: ${linked_result[0]}"
pass "env-bootstrap resolves linked OMARCHY_PATH"
assert_path_first "$linked_path" "$tmpdir/active/bin" "env-bootstrap prepends active checkout bin in linked mode"
assert_path_first "$linked_path" "$home/.local/share/mise/command-wrappers/bin" "env-bootstrap keeps command wrappers first in linked mode"
assert_path_present "$linked_path" "$tmpdir/active/bin" "env-bootstrap includes the active checkout bin in linked mode"
assert_path_present "$linked_path" "$tmpdir/unrelated/bin" "env-bootstrap preserves unrelated PATH entries in linked mode"
mapfile -t linked_duplicate_result < <(run_bootstrap bash "$bootstrap" "$home" "$tmpdir/active/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin")
linked_duplicate_path=${linked_duplicate_result[1]}
[[ $linked_duplicate_path == "$tmpdir/active/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap does not duplicate PATH entries" "actual PATH: $linked_duplicate_path"
[[ $linked_duplicate_path == "$home/.local/share/mise/command-wrappers/bin:$tmpdir/active/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap does not duplicate PATH entries" "actual PATH: $linked_duplicate_path"
pass "env-bootstrap does not duplicate PATH entries"
# An empty PATH must not produce empty entries (a bare ":" means the cwd)
mapfile -t empty_path_result < <(run_bootstrap bash "$bootstrap" "$home" "")
empty_path=${empty_path_result[1]}
[[ $empty_path == "$tmpdir/active/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap builds a clean PATH from an empty one" "actual PATH: $empty_path"
[[ $empty_path == "$home/.local/share/mise/command-wrappers/bin:$tmpdir/active/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap builds a clean PATH from an empty one" "actual PATH: $empty_path"
pass "env-bootstrap builds a clean PATH from an empty one"
if command -v zsh >/dev/null 2>&1; then
mapfile -t zsh_result < <(run_bootstrap zsh "$bootstrap" "$home" "$tmpdir/unrelated/bin:/usr/bin")
zsh_path=${zsh_result[1]}
assert_path_first "$zsh_path" "$tmpdir/active/bin" "env-bootstrap works when sourced by zsh"
assert_path_first "$zsh_path" "$home/.local/share/mise/command-wrappers/bin" "env-bootstrap works when sourced by zsh"
assert_path_present "$zsh_path" "$tmpdir/unrelated/bin" "env-bootstrap zsh mode preserves unrelated PATH entries"
fi
mapfile -t repeated_result < <(run_bootstrap bash "$bootstrap" "$home" "$linked_duplicate_path")
[[ ${repeated_result[1]} == "$linked_duplicate_path" ]] || fail "repeated bootstrap leaves the command wrapper path unchanged"
pass "repeated bootstrap leaves the command wrapper path unchanged"
mapfile -t custom_data_result < <(MISE_DATA_DIR="$tmpdir/custom-mise" run_bootstrap bash "$bootstrap" "$home" "/usr/bin")
assert_path_first "${custom_data_result[1]}" "$tmpdir/custom-mise/command-wrappers/bin" "env-bootstrap honors a custom mise data directory"
# A parent agent or mise activation may have put real tool bins before a wrapper
# directory that was already present. Move the existing entry rather than skip it.
inherited_path="$tmpdir/managed/bin:$home/.local/share/mise/command-wrappers/bin:/usr/bin"
mapfile -t reordered_result < <(run_bootstrap bash "$bootstrap" "$home" "$inherited_path")
[[ ${reordered_result[1]} == "$home/.local/share/mise/command-wrappers/bin:$tmpdir/active/bin:$tmpdir/managed/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] ||
fail "bootstrap moves existing wrappers ahead of inherited managed tool paths without duplication" "${reordered_result[1]}"
pass "bootstrap moves existing wrappers ahead of inherited managed tool paths without duplication"