Created
June 1, 2026 02:37
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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