Skip to content

Instantly share code, notes, and snippets.

@laughingman7743
Created June 1, 2026 02:37
Show Gist options
  • Select an option

  • Save laughingman7743/566ca6703f021ff3fbedb62ad91adefc to your computer and use it in GitHub Desktop.

Select an option

Save laughingman7743/566ca6703f021ff3fbedb62ad91adefc to your computer and use it in GitHub Desktop.
claude-profile — switch Claude Code config profiles (CLAUDE_CONFIG_DIR) per shell session or per directory (fish + mise). e.g. Max plan in one repo, API/proxy in another.
# claude-profile — switch Claude Code config profiles (the CLAUDE_CONFIG_DIR it uses).
#
# Idea: Claude Code reads its config/auth from $CLAUDE_CONFIG_DIR (default: ~/.claude).
# This function switches that per shell session or per directory, so you can run e.g.
# a Max-plan account in one repo and an API/proxy setup in another.
#
# "default" = no override -> ~/.claude
# "<name>" = ~/.config/claude/profiles/<name>/ (own settings.json + auth;
# symlink CLAUDE.md/plugins/skills/projects from ~/.claude to share them)
#
# Install : save as ~/.config/fish/functions/claude-profile.fish (fish autoloads it)
# Requires: fish; mise (activated) for per-directory auto-switch; python3 for the `mcp` subcommands
# Usage : claude-profile -h
#
# Per-directory auto-switch: drop a mise.local.toml in a repo with
# [env]
# CLAUDE_CONFIG_DIR = "{{env.HOME}}/.config/claude/profiles/<name>"
# (`claude-profile set <name>` writes that for you). No secrets live in this file.
function claude-profile --description 'Manage Claude Code profiles: switch session and configure per-directory (mise)'
set -l profiles_dir "$HOME/.config/claude/profiles"
set -l file mise.local.toml
set -l sub $argv[1]
switch "$sub"
# ---- help ----
case -h --help help
printf '%s\n' \
'claude-profile — switch Claude Code config profile' \
'' \
'USAGE:' \
' claude-profile [status] show session profile + current directory resolution' \
' claude-profile list list available profiles' \
' claude-profile init <name> create a new profile by copying ~/.claude settings (skips if it exists)' \
' claude-profile <name> switch THIS shell session to <name>' \
' claude-profile use <name> same as above (explicit)' \
' claude-profile default switch session back to the base config (~/.claude)' \
' claude-profile set <name> configure the CURRENT directory to use <name> (persistent, via mise)' \
' claude-profile set default remove the directory override (back to ~/.claude)' \
' claude-profile mcp list [prof] show MCP servers in default vs a profile' \
' claude-profile mcp sync <srv> [prof] copy one MCP server def from ~/.claude.json into a profile' \
' claude-profile -h | --help show this help' \
'' \
'NOTES:' \
' - "default" = no CLAUDE_CONFIG_DIR override; Claude Code uses ~/.claude.' \
" - any other <name> maps to $profiles_dir/<name>/ (run 'claude-profile list')." \
' - "session" lasts only for the current shell; "set" persists per-directory via mise.local.toml.' \
' - "set" is idempotent (same profile = no-op) and git-safe (touches .git/info/exclude only inside a git repo).'
return 0
# ---- status: show session profile + what this directory resolves to ----
case '' status
if set -q CLAUDE_CONFIG_DIR
echo "session : "(basename "$CLAUDE_CONFIG_DIR")" ($CLAUDE_CONFIG_DIR)"
else
echo "session : default (~/.claude)"
end
set -l dir_ccd (mise env 2>/dev/null | string replace -rf '^set -gx CLAUDE_CONFIG_DIR ' '')
if test -n "$dir_ccd"
echo "directory : "(basename "$dir_ccd")" (via mise, $PWD)"
else
echo "directory : default (~/.claude, no mise override in $PWD)"
end
return 0
# ---- list available profiles ----
case list
echo "default (~/.claude)"
for d in $profiles_dir/*/
test -d "$d"; and echo (basename "$d")
end
return 0
# ---- init: create a new profile by copying ~/.claude settings (skip if exists) ----
case init
set -l name $argv[2]
if test -z "$name"
echo "usage: claude-profile init <name> (copies ~/.claude settings into a new profile)" >&2
return 1
end
if test "$name" = default
echo "claude-profile: 'default' is reserved (it means ~/.claude) — pick another name" >&2
return 1
end
set -l dest "$profiles_dir/$name"
if test -e "$dest"
echo "✓ profile '$name' already exists at $dest — skip (not overwritten)"
return 0
end
mkdir -p "$dest"
if test -f "$HOME/.claude/settings.json"
cp "$HOME/.claude/settings.json" "$dest/settings.json"
end
for item in CLAUDE.md plugins skills projects
if test -e "$HOME/.claude/$item"; and not test -e "$dest/$item"
ln -s "$HOME/.claude/$item" "$dest/$item"
end
end
echo "→ created profile '$name' at $dest"
echo " copied settings.json; symlinked CLAUDE.md, plugins, skills, projects (auth NOT copied)"
echo " next: edit $dest/settings.json if needed, then 'claude-profile $name' (run 'claude /login' for an OAuth account)"
return 0
# ---- mcp: list / sync individual MCP server defs from default into a profile ----
case mcp
set -l action $argv[2]
set -l src "$HOME/.claude.json"
# resolve target profile (explicit arg wins, else current session profile)
function __cp_resolve_target -V profiles_dir -a prof
if test -n "$prof"
if test "$prof" = default
echo "claude-profile: 'default' (~/.claude) is the source, not a sync target" >&2
return 1
end
if not test -d "$profiles_dir/$prof"
echo "claude-profile: profile '$prof' not found at $profiles_dir/$prof" >&2
return 1
end
echo "$profiles_dir/$prof/.claude.json"
echo "$prof"
else if set -q CLAUDE_CONFIG_DIR
echo "$CLAUDE_CONFIG_DIR/.claude.json"
basename "$CLAUDE_CONFIG_DIR"
else
echo "claude-profile: you're on 'default' (the source). switch first (claude-profile <name>) or pass a target profile." >&2
return 1
end
end
switch "$action"
case list
set -l resolved (__cp_resolve_target $argv[3]); or begin
functions -e __cp_resolve_target
return 1
end
set -l dst $resolved[1]
set -l label $resolved[2]
python3 -c '
import json,sys,os
src,dst,label=sys.argv[1],sys.argv[2],sys.argv[3]
def names(p):
return None if not os.path.exists(p) else sorted((json.load(open(p)).get("mcpServers") or {}).keys())
s=names(src) or []
d=names(dst)
print(" default (~/.claude): "+(", ".join(s) if s else "(none)"))
if d is None:
print(" "+label+": (no .claude.json yet — never launched)"); d=[]
else:
print(" "+label+": "+(", ".join(d) if d else "(none)"))
miss=[x for x in s if x not in d]
print(" missing in "+label+": "+(", ".join(miss) if miss else "(none — all synced)"))
' "$src" "$dst" "$label"
functions -e __cp_resolve_target
return 0
case sync
set -l name $argv[3]
if test -z "$name"
echo "usage: claude-profile mcp sync <server-name> [profile]" >&2
functions -e __cp_resolve_target
return 1
end
set -l resolved (__cp_resolve_target $argv[4]); or begin
functions -e __cp_resolve_target
return 1
end
functions -e __cp_resolve_target
set -l dst $resolved[1]
set -l label $resolved[2]
set -l out (python3 -c '
import json,sys,os
src,dst,name=sys.argv[1],sys.argv[2],sys.argv[3]
s=json.load(open(src)) if os.path.exists(src) else {}
sm=s.get("mcpServers") or {}
if name not in sm:
print("|".join(sorted(sm.keys()))); sys.exit(2)
d=json.load(open(dst)) if os.path.exists(dst) else {}
dm=d.get("mcpServers")
if not isinstance(dm,dict): dm={}
if dm.get(name)==sm[name]:
sys.exit(3)
dm[name]=sm[name]; d["mcpServers"]=dm
tmp=dst+".cptmp"
json.dump(d,open(tmp,"w"),indent=2,ensure_ascii=False)
os.replace(tmp,dst)
print(sm[name].get("type","?")); sys.exit(0)
' "$src" "$dst" "$name")
set -l rc $status
switch $rc
case 0
echo "→ synced MCP '$name' (type=$out) into $label"
echo " $dst"
echo " note: OAuth servers (sse/http) still need auth in this profile — run claude there and /mcp"
case 2
echo "claude-profile: '$name' not found in default (~/.claude.json)" >&2
echo " available: "(string replace -a '|' ', ' -- $out) >&2
return 1
case 3
echo "✓ '$name' already in $label and identical — skip"
case '*'
echo "claude-profile: sync failed (python rc=$rc)" >&2
return 1
end
return 0
case '' '*'
echo "usage: claude-profile mcp <list|sync> ..." >&2
echo " claude-profile mcp list [profile]" >&2
echo " claude-profile mcp sync <server-name> [profile]" >&2
functions -q __cp_resolve_target; and functions -e __cp_resolve_target
return 1
end
# ---- set: configure the CURRENT directory to use a profile (idempotent) ----
case set
set -l name $argv[2]
if test -z "$name"
echo "usage: claude-profile set <profile> (default = remove override)" >&2
return 1
end
# default => remove any override here
if test "$name" = default
if test -f $file; and grep -qE '^[[:space:]]*CLAUDE_CONFIG_DIR[[:space:]]*=' $file
sed -i '' -E '/^[[:space:]]*CLAUDE_CONFIG_DIR[[:space:]]*=/d' $file
set -l rest (grep -vE '^[[:space:]]*(#.*)?$' $file | grep -vE '^[[:space:]]*\[env\][[:space:]]*$')
if test -z "$rest"
rm -f $file
echo "→ removed override in $PWD (deleted empty $file, now default ~/.claude)"
else
mise trust $file >/dev/null 2>&1
echo "→ removed override in $PWD/$file (now default ~/.claude)"
end
else
echo "✓ already default here (no override) — skip"
end
return 0
end
if not test -d "$profiles_dir/$name"
echo "claude-profile: profile '$name' not found at $profiles_dir/$name" >&2
echo "try: claude-profile list" >&2
return 1
end
set -l target "{{env.HOME}}/.config/claude/profiles/$name"
# idempotent: already pointing at this profile? do nothing.
if test -f $file; and grep -qE "CLAUDE_CONFIG_DIR[[:space:]]*=[[:space:]]*\"[^\"]*/profiles/$name\"" $file
echo "$PWD/$file already set to '$name' — skip"
return 0
end
if not test -f $file
printf '# Personal (untracked) mise overrides — not committed.\n[env]\nCLAUDE_CONFIG_DIR = "%s"\n' "$target" >$file
else
# drop any existing CLAUDE_CONFIG_DIR line, then (re)insert under [env]
awk -v line="CLAUDE_CONFIG_DIR = \"$target\"" '
/^[[:space:]]*CLAUDE_CONFIG_DIR[[:space:]]*=/ { next }
/^\[env\]/ { print; print line; ins=1; next }
{ print }
END { if (!ins) { print ""; print "[env]"; print line } }
' $file >$file.cptmp; and mv $file.cptmp $file
end
# keep git clean — only if inside a git work tree (skip otherwise)
if git rev-parse --is-inside-work-tree >/dev/null 2>&1
set -l info_excl (git rev-parse --git-dir 2>/dev/null)/info/exclude
if test -n "$info_excl"; and not grep -qxF "$file" "$info_excl" 2>/dev/null
echo "$file" >>"$info_excl"
echo " (added '$file' to .git/info/exclude)"
end
end
mise trust $file >/dev/null 2>&1
echo "→ configured $PWD/$file to use '$name'"
echo " open a new shell or 'cd .' to apply, then run: claude"
return 0
# ---- use <name>: switch this shell session ----
case use
set sub $argv[2]
if test -z "$sub"
echo "usage: claude-profile use <profile>" >&2
return 1
end
end
# ---- session switch (reached via `use <name>` or bare `<name>`) ----
if test "$sub" = default
set -e CLAUDE_CONFIG_DIR
echo "→ session: default (~/.claude)"
else if test -d "$profiles_dir/$sub"
set -gx CLAUDE_CONFIG_DIR "$profiles_dir/$sub"
echo "→ session: $sub ($CLAUDE_CONFIG_DIR)"
else
echo "claude-profile: profile '$sub' not found at $profiles_dir/$sub" >&2
echo "try: claude-profile list (or -h for help)" >&2
return 1
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment