Coding Agentのコンテキストを切り替えるツールを作った
業務用と個人用で異なるアカウントでCoding Agentを使う際に、ブラウザのプロファイルのように切り替える仕組みが欲しかったので作った
私は個人でChatGPT PlusとClaude Maxを契約しているが、会社でもCoding Agentの契約がある。
ブラウザではプロファイル設定で簡単に切り替えられるもののローカルで動くCoding agentはそうもいかない。業務開始時に毎回ログインし直せばいいが、切り替えを忘れると事故になるしそもそも毎回のログインが面倒くさい。業務用に切り替えようと思ったら個人用のブラウザで認証画面が開いて……、面倒。
これを解決する良い手段がある。Claude CodeはCLAUDE_CONFIG_DIR、CodexはCODEX_HOMEをそれぞれ認証情報とかのベースディレクトリとして使用する。つまり、この環境変数を一発で切り替え、別でシェルを開いても現在使っているCLAUDE_CONFIG_DIRがそのまま使われるようにすれば良い。そうすれば朝に一発切り替え、退勤後に切り替えるだけで話が済む。
実装
本実装は公開リポジトリにある。
Build software better, togetherGitHub is where people build software. More than 150 million people use GitHub to discover, fork, and contribute to over 420 million projects.github.com
やっていることは単純で、$HOME/.local/state/agentenv/currentに現在の環境名を記録して、agentenv loadでその環境がロードされる。switchサブコマンドを使うと切り替えられる。
DEFAULT_ENV=default
BASE_DIR="$HOME/.local/share/agentenv"
STATE_FILE="$HOME/.local/state/agentenv/current"
# ---- path helpers ---------------------------------------------------------
env_claude_dir() {
if [ "$1" = "$DEFAULT_ENV" ]; then printf '%s\n' "$HOME/.claude";
else printf '%s\n' "$BASE_DIR/$1/claude"; fi
}
env_codex_dir() {
if [ "$1" = "$DEFAULT_ENV" ]; then printf '%s\n' "$HOME/.codex";
else printf '%s\n' "$BASE_DIR/$1/codex"; fi
}
list_envs() {
{
printf '%s\n' "$DEFAULT_ENV"
if [ -d "$BASE_DIR" ]; then
for d in "$BASE_DIR"/*/; do
[ -d "$d" ] || continue
basename "$d"
done
fi
} | sort -u
}
env_exists() {
[ "$1" = "$DEFAULT_ENV" ] && return 0
[ -d "$BASE_DIR/$1" ]
}
current_env() {
if [ -s "$STATE_FILE" ]; then head -n1 "$STATE_FILE"; else printf '%s\n' "$DEFAULT_ENV"; fi
}
# ---- environment creation -------------------------------------------------
# Create a named environment's Claude/Codex config dirs. The directories
# alone are what make the env exist (env_exists checks for the dir); each CLI
# populates its own config on first run. Idempotent.
create_env() {
mkdir -p "" ""
}
# ---- shell code emission --------------------------------------------------
# Print eval-able code that points the shell at $1's profile. Default unsets
# everything (tools fall back to ~/.claude, ~/.codex; starship badge hides);
# other envs export the three vars.
print_exports() {
local name="$1" shell="$2" claude codex
if [ "$name" = "$DEFAULT_ENV" ]; then
if [ "$shell" = fish ]; then
printf 'set -e CLAUDE_CONFIG_DIR\nset -e CODEX_HOME\nset -e AGENTENV\n'
else
printf 'unset CLAUDE_CONFIG_DIR CODEX_HOME AGENTENV\n'
fi
return
fi
claude=""
codex=""
if [ "$shell" = fish ]; then
printf 'set -gx CLAUDE_CONFIG_DIR %q\nset -gx CODEX_HOME %q\nset -gx AGENTENV %q\n' \
"$claude" "$codex" "$name"
else
printf 'export CLAUDE_CONFIG_DIR=%q\nexport CODEX_HOME=%q\nexport AGENTENV=%q\n' \
"$claude" "$codex" "$name"
fi
}
# ---- .agentenv resolution -------------------------------------------------
# Walk up from PWD looking for a .agentenv file; its first non-empty line is
# the env name. Fall back to the saved state, then to default.
resolve_load_env() {
local dir="$PWD" name
while [ -n "$dir" ]; do
if [ -f "$dir/.agentenv" ]; then
name=""
if [ -n "$name" ]; then printf '%s\n' "$name"; return; fi
fi
[ "$dir" = "/" ] && break
dir=""
done
current_env
}
select_env() {
list_envs | "''" || true
}
write_state() {
mkdir -p ""
printf '%s\n' "$1" > "$STATE_FILE"
}
# ---- subcommands ----------------------------------------------------------
cmd_switch() {
local name="$1"
if [ -z "$name" ]; then
name=""
[ -n "$name" ] || return 0
fi
if ! env_exists "$name"; then
create_env "$name"
printf 'created environment: %s\n' "$name" >&2
fi
write_state "$name"
printf 'switched to: %s\n' "$name" >&2
print_exports "$name" "$SHELL_KIND"
}
cmd_load() {
local name
name=""
env_exists "$name" || create_env "$name"
# Skip redundant re-exports (e.g. chpwd hook firing without a real change).
[ "$name" = "''" ] && return 0
print_exports "$name" "$SHELL_KIND"
}
cmd_delete() {
local name="$1"
if [ -z "$name" ]; then
name=""
[ -n "$name" ] || return 0
fi
if [ "$name" = "$DEFAULT_ENV" ]; then
printf 'refusing to delete the default environment\n' >&2
return 1
fi
if ! env_exists "$name"; then
printf 'unknown environment: %s\n' "$name" >&2
return 1
fi
rm -rf "''/$name"
printf 'deleted environment: %s\n' "$name" >&2
if [ "$name" = "" ]; then
write_state "$DEFAULT_ENV"
print_exports "$DEFAULT_ENV" "$SHELL_KIND"
fi
}
cmd_list() {
# --raw prints bare names (one per line), for scripting / shell completion.
if [ "''" = "--raw" ]; then
list_envs
return
fi
local cur
cur=""
while IFS= read -r name; do
if [ "$name" = "$cur" ]; then printf '* %s\n' "$name"; else printf ' %s\n' "$name"; fi
done <
}
usage() {
cat <<'USAGE'
Usage: agentenv <command> [name]
Commands:
switch [name] Switch to (and create if missing) an environment.
With no name, pick one with $AGENTENV_SELECTOR (default sk).
delete [name] Delete an environment (the default environment is protected).
load Apply the env from a parent .agentenv file or saved state.
list [--raw] List environments; the current one is marked with '*'.
--raw prints bare names, one per line.
With no command, behaves like `switch`.
USAGE
}
# ---- argument parsing -----------------------------------------------------
SHELL_KIND=posix
ARGS=()
while [ "$#" -gt 0 ]; do
case "$1" in
--shell) SHELL_KIND="$2"; shift 2 ;;
--shell=*) SHELL_KIND="''"; shift ;;
*) ARGS+=("$1"); shift ;;
esac
done
set -- "''"
case "''" in
switch) cmd_switch "''" ;;
load) cmd_load ;;
delete) cmd_delete "''" ;;
list) cmd_list "''" ;;
-h | --help | help) usage ;;
*) usage >&2; exit 1 ;;
esac
これだけでは毎回agentenv loadを打つ必要があって面倒なので補完とhookを設定する。自分はnix-darwinを使っているので下記のように設定している。
{
lib,
pkgs,
...
}:
let
# agentenv switches the Claude Code / Codex profile of the current shell
# between named environments (see derivations/agentenv). `default` maps to the
# tools' own ~/.claude / ~/.codex and is protected; other envs live under
# ~/.local/share/agentenv/<name>.
agentenv = pkgs.callPackage ../derivations/agentenv { };
in
{
home = {
packages = [ agentenv ];
# fish completions: subcommands, then existing env names (switch also accepts
# new names, so the list is just a hint).
file.".config/fish/completions/agentenv.fish".text = ''
complete -c agentenv -f
complete -c agentenv -n __fish_use_subcommand -a switch -d 'Switch to (and create) an environment'
complete -c agentenv -n __fish_use_subcommand -a delete -d 'Delete an environment'
complete -c agentenv -n __fish_use_subcommand -a load -d 'Apply the env from .agentenv or saved state'
complete -c agentenv -n __fish_use_subcommand -a list -d 'List environments'
complete -c agentenv -n '__fish_seen_subcommand_from switch delete' -a '(command agentenv list --raw)'
'';
};
# The binary cannot mutate its parent shell, so the wrappers eval the eval-able
# code it prints to stdout (switch / load only); human messages go to stderr.
# The hooks call `agentenv load` on startup and on every directory change so a
# parent .agentenv file or the saved state is applied automatically.
programs = {
zsh.initContent = ''
agentenv() {
case "''${1:-}" in
switch | load | "")
local _out
_out="$(command agentenv --shell posix "$@")" || return
[ -n "$_out" ] && eval "$_out"
;;
*)
command agentenv "$@"
;;
esac
}
_agentenv_load() {
local _out
_out="$(command agentenv load --shell posix)" || return
[ -n "$_out" ] && eval "$_out"
}
autoload -Uz add-zsh-hook
add-zsh-hook chpwd _agentenv_load
_agentenv_load
_agentenv() {
local -a subcmds
subcmds=(
'switch:Switch to (and create) an environment'
'delete:Delete an environment'
'load:Apply the env from .agentenv or saved state'
'list:List environments'
)
if (( CURRENT == 2 )); then
_describe -t commands 'agentenv command' subcmds
elif (( CURRENT == 3 )); then
case $words[2] in
switch | delete) compadd -- ''${(f)"$(command agentenv list --raw)"} ;;
esac
fi
}
compdef _agentenv agentenv
'';
fish = {
functions = {
agentenv = {
description = "Switch the Claude/Codex profile of the current shell";
# Pipe to `source` (not `eval (...)`) so multi-line output keeps its
# newlines; fish command substitution would collapse them into spaces.
body = ''
switch "$argv[1]"
case switch load ""
command agentenv --shell fish $argv | source
case "*"
command agentenv $argv
end
'';
};
_agentenv_load = {
description = "Apply the agentenv profile for the current directory";
onVariable = "PWD";
body = ''
command agentenv load --shell fish | source
'';
};
};
# Run the initial load after fish.nix's interactiveShellInit so the saved
# state / .agentenv is applied at startup (the PWD hook only fires on change).
interactiveShellInit = lib.mkAfter ''
_agentenv_load
'';
};
};
}
コメント
コメントはまだありません