Skip to content

Instantly share code, notes, and snippets.

@jwarkentin
Last active June 9, 2026 02:25
Show Gist options
  • Select an option

  • Save jwarkentin/225ffcfba14023315bdca24c0f7ee3f6 to your computer and use it in GitHub Desktop.

Select an option

Save jwarkentin/225ffcfba14023315bdca24c0f7ee3f6 to your computer and use it in GitHub Desktop.
Manages Claude/Codex credential files to make it quick and easy to switch between accounts when a session limit is hit (only works on Linux since Mac uses keychain)
#!/usr/bin/env bash
# cas - Claude/Codex Account Switch
# Switches a tool's live auth file to an aliased credentials file.
#
# Claude aliases: ~/.claude/.credentials.<alias>.json
# Codex aliases: ~/.codex/auth.<alias>.json
#
# The selected alias carries a ".cur" tag. If a tool replaces the symlink
# during token refresh, cas uses that marker to recover the refreshed file.
set -euo pipefail
TOOLS=(claude codex)
TOOL=""
TOOL_DIR=""
LINK_NAME=""
FILE_PREFIX=""
usage() {
cat >&2 <<'EOF'
Usage:
cas <tool> <alias> Switch <tool> to <alias>
cas <tool> Show the current account for <tool>
cas Show current accounts
cas -h, --help Show this help
Tool names support unique partial matches:
cas cl work # Claude
cas co 2 # Codex
When only ~/.claude or ~/.codex exists, a single argument is treated as the
alias for that tool:
cas work
EOF
}
configure_tool() {
TOOL="$1"
case "$TOOL" in
claude)
TOOL_DIR="$HOME/.claude"
LINK_NAME=".credentials.json"
FILE_PREFIX=".credentials."
;;
codex)
TOOL_DIR="$HOME/.codex"
LINK_NAME="auth.json"
FILE_PREFIX="auth."
;;
*)
echo "Error: unsupported tool '$TOOL'." >&2
exit 1
;;
esac
}
match_tool() {
local requested="$1" tool match=""
for tool in "${TOOLS[@]}"; do
if [[ "$tool" == "$requested"* ]]; then
if [ -n "$match" ]; then
echo "Error: tool '$requested' is ambiguous ($match, $tool)." >&2
return 1
fi
match="$tool"
fi
done
if [ -z "$match" ]; then
echo "Error: unknown tool '$requested'. Expected claude or codex." >&2
return 1
fi
echo "$match"
}
existing_tools() {
local tool
for tool in "${TOOLS[@]}"; do
case "$tool" in
claude) [ -d "$HOME/.claude" ] && echo "$tool" ;;
codex) [ -d "$HOME/.codex" ] && echo "$tool" ;;
esac
done
}
link_path() {
echo "$TOOL_DIR/$LINK_NAME"
}
alias_path() {
echo "$TOOL_DIR/${FILE_PREFIX}$1.json"
}
current_alias_path() {
echo "$TOOL_DIR/${FILE_PREFIX}$1.cur.json"
}
# Echo the alias currently marked ".cur", or return 1 if none.
find_cur() {
local f base
for f in "$TOOL_DIR"/"${FILE_PREFIX}"*.cur.json; do
[ -e "$f" ] || continue
base="${f##*/}"
base="${base#"$FILE_PREFIX"}"
echo "${base%.cur.json}"
return 0
done
return 1
}
clear_marker() {
local f
for f in "$TOOL_DIR"/"${FILE_PREFIX}"*.cur.json; do
[ -e "$f" ] || continue
mv -f "$f" "${f%.cur.json}.json"
done
}
# If the tool replaced its symlink with a real file, preserve the refreshed
# credentials under the alias marked current.
heal_if_clobbered() {
local link cur
link="$(link_path)"
if [ -e "$link" ] && [ ! -L "$link" ]; then
if cur="$(find_cur)"; then
mv -f "$link" "$(current_alias_path "$cur")"
echo "Recovered refreshed $TOOL credentials into ${FILE_PREFIX}${cur}.cur.json"
else
echo "Error: $link is a regular file and no $TOOL account is marked current." >&2
echo "Cannot determine which aliased file it belongs to; resolve manually." >&2
exit 1
fi
fi
}
show_current() {
local link tgt alias cur
link="$(link_path)"
if [ -L "$link" ]; then
tgt="$(basename "$(readlink "$link")")"
alias="${tgt#"$FILE_PREFIX"}"
alias="${alias%.cur.json}"
alias="${alias%.json}"
echo "$TOOL: $alias"
elif [ -e "$link" ]; then
if cur="$(find_cur)"; then
echo "$TOOL: $cur ($link is a plain file after token refresh)" >&2
else
echo "$TOOL: no current marker ($link is a plain file)" >&2
fi
else
echo "$TOOL: no account selected" >&2
fi
}
list_aliases() {
local f base
for f in "$TOOL_DIR"/"${FILE_PREFIX}"*.json; do
[ -e "$f" ] || continue
base="${f##*/}"
base="${base#"$FILE_PREFIX"}"
base="${base%.cur.json}"
base="${base%.json}"
[ "$f" = "$(link_path)" ] || echo " $base"
done | sort -u >&2
}
switch_account() {
local alias="$1" plain current link
plain="$(alias_path "$alias")"
current="$(current_alias_path "$alias")"
link="$(link_path)"
if [ ! -f "$plain" ] && [ ! -f "$current" ]; then
echo "Error: no $TOOL credentials file for alias '$alias'." >&2
echo "Available aliases:" >&2
list_aliases
exit 1
fi
heal_if_clobbered
clear_marker
mv -f "$plain" "$current"
ln -sfn "${FILE_PREFIX}${alias}.cur.json" "$link"
echo "Switched $TOOL to account '$alias' ($LINK_NAME -> ${FILE_PREFIX}${alias}.cur.json)"
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
if [ $# -gt 2 ]; then
usage
exit 1
fi
mapfile -t existing < <(existing_tools)
case $# in
0)
if [ "${#existing[@]}" -eq 0 ]; then
echo "No ~/.claude or ~/.codex directory exists." >&2
exit 1
fi
for tool in "${existing[@]}"; do
configure_tool "$tool"
show_current
done
;;
1)
if [ "${#existing[@]}" -eq 1 ]; then
configure_tool "${existing[0]}"
switch_account "$1"
else
selected_tool="$(match_tool "$1")" || exit 1
configure_tool "$selected_tool"
show_current
fi
;;
2)
selected_tool="$(match_tool "$1")" || exit 1
configure_tool "$selected_tool"
switch_account "$2"
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment