{"id":50308067,"url":"https://github.com/skyfe79/cyolo","last_synced_at":"2026-05-28T18:01:45.721Z","repository":{"id":359718870,"uuid":"1212470014","full_name":"skyfe79/cyolo","owner":"skyfe79","description":"Multi-account profile manager and config cleaner for Claude Code.","archived":false,"fork":false,"pushed_at":"2026-05-23T05:18:51.000Z","size":343,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-23T06:35:14.842Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skyfe79.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-16T12:12:30.000Z","updated_at":"2026-05-23T05:18:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/skyfe79/cyolo","commit_stats":null,"previous_names":["skyfe79/cyolo"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/skyfe79/cyolo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skyfe79%2Fcyolo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skyfe79%2Fcyolo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skyfe79%2Fcyolo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skyfe79%2Fcyolo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skyfe79","download_url":"https://codeload.github.com/skyfe79/cyolo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skyfe79%2Fcyolo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33619972,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-28T18:01:44.769Z","updated_at":"2026-05-28T18:01:45.702Z","avatar_url":"https://github.com/skyfe79.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cyolo\n\nMulti-account profile manager and config cleaner for Claude Code.\n\n## Motivation\n\n- You juggle multiple Claude Code accounts (personal, work, clients). A\n  naive setup means `/logout` + `/login` every time you switch, or\n  remembering to export `CLAUDE_CONFIG_DIR` in every shell.\n- Each new profile directory would otherwise need its own copy of the things\n  you set up once — `CLAUDE.md`, `settings.json`, `commands/`, `skills/`,\n  `agents/` — so npm-installed skills and shared prompts drift out of sync.\n- `~/.claude.json` grows without bound. Deleted projects stay in the\n  `projects` map; `~/.claude/projects/\u003cencoded-path\u003e/` keeps their session\n  history; multi-megabyte configs slow Claude Code startup for no reason.\n\n`cyolo` solves all three:\n\n- **Multi-account OAuth**: each profile's token lands in its own macOS\n  Keychain entry (Claude Code hashes `CLAUDE_CONFIG_DIR` into the service\n  name). Log in once per account; switch profiles with zero re-auth.\n- **Shared settings**: per-folder `.claude-profile.json` auto-detection via\n  walk-up, plus symlinks for the six common config items so plugins and\n  prompts stay consistent across accounts.\n- **`diet` cleanup**: reports orphaned project records, stale session\n  folders, and cache cruft; `--apply` reclaims the space.\n\n## Installation\n\n`cyolo` is a single Rust binary. From a clone of this repository, either use\nthe install script (a thin wrapper that runs the cargo command below and\nreports on `~/.cargo/bin` PATH status):\n\n```bash\n./install.sh            # release build\n./install.sh --debug    # dev build (faster, unoptimized)\n./install.sh --locked   # pin to Cargo.lock (CI-friendly)\n```\n\nor invoke cargo directly:\n\n```bash\ncargo install --path .\n```\n\nBoth drop the binary into `~/.cargo/bin/cyolo`. `~/.cyolo/` is used only for\nconfig and profile directories, not the binary.\n\n**MSRV: Rust 1.85+ is required** (the crate uses `edition = \"2024\"`). Check\nwith `rustc --version`; upgrade via `rustup update stable` if needed.\n\nOnce published to crates.io the installation will also work as:\n\n```bash\ncargo install cyolo   # future; not yet published\n```\n\nIf you were previously using the `cyolo()` zsh function, remove it from your\nshell rc file before using the binary — the binary replaces the function and\nsupports the same pass-through semantics.\n\n## Quickstart\n\n```bash\ncyolo profile add personal ~/.claude             # register the existing ~/.claude as a profile (no symlinks — it is the source)\ncyolo profile default personal                   # make it the fallback when no .claude-profile.json is found\ncyolo profile add work                           # creates ~/.claude-work/ + symlinks shared config + launches `claude` so you can /login as the work account\ncyolo profile list                               # shows \"* personal  skyfe79@gmail.com\" and \"  work  work@example.com\"\ncyolo                                            # runs `claude --dangerously-skip-permissions` with the resolved profile\n```\n\nIn a work project, drop a profile marker so every invocation from that tree\nuses the right account:\n\n```bash\ncd ~/work/client-a \u0026\u0026 cyolo profile init work\ncyolo                                            # resolves \"work\" via walk-up from anywhere beneath ~/work/client-a\n```\n\n## Command structure\n\ncyolo only owns three top-level verbs. Everything else is forwarded verbatim\nto `claude --dangerously-skip-permissions` with the resolved profile's\n`CLAUDE_CONFIG_DIR`:\n\n| Input | cyolo behavior |\n|---|---|\n| `cyolo help` · `cyolo --help` · `cyolo -h` | Prints cyolo's own help (handwritten) |\n| `cyolo profile ...` | Handled in-process via `clap` (10 subcommands — see below) |\n| `cyolo profile \u003csub\u003e --help` | Per-subcommand help (e.g. `cyolo profile add --help`) |\n| `cyolo diet ...` | Handled in-process via `clap` (see Usage — diet) |\n| `cyolo diet --help` | Diet flag reference |\n| `cyolo \u003canything else\u003e` | `claude --dangerously-skip-permissions \u003cargs\u003e` |\n\nThe rule is unambiguous: if the first argument is `help`, `--help`, `-h`,\n`profile`, or `diet`, cyolo handles it. Everything else — including\n`--version`, `-p \"...\"`, `-c`, plain prompts, or unknown verbs — is\ntransparent to claude.\n\nA consequence: `cyolo --version` prints **Claude Code's** version, not\ncyolo's. To see cyolo's own version, run `cyolo help` (the first line\nshows `cyolo \u003cVERSION\u003e`).\n\n**`cyolo update` was removed** — run `claude update` directly instead.\nUpgrading Claude Code is not part of cyolo's scope.\n\n## How multi-account OAuth actually works\n\nClaude Code stores its OAuth token in the macOS Keychain. The service name\nis composed dynamically from `CLAUDE_CONFIG_DIR`:\n\n```\nCLAUDE_CONFIG_DIR unset        → Claude Code-credentials\nCLAUDE_CONFIG_DIR=~/.claude-work → Claude Code-credentials-\u003csha256(\"/Users/you/.claude-work\")[:8]\u003e\n```\n\nBecause each profile directory hashes to a different suffix, **each profile\ngets its own distinct Keychain entry**. Two Anthropic accounts can coexist —\nno re-login when you switch profiles, no overwritten tokens. cyolo simply\nsets `CLAUDE_CONFIG_DIR` before launching `claude` and lets Claude Code\nitself pick the right Keychain entry.\n\nThe account identity (email, organization, subscription tier) is stored in\n`\u003cCLAUDE_CONFIG_DIR\u003e/.claude.json` under `oauthAccount`. `cyolo profile list`\nand `cyolo profile whoami` read this file to show you which account a profile\nis currently bound to.\n\n### Two-account tutorial\n\n```bash\ncyolo profile add personal ~/.claude         # register existing login as \"personal\" (no extra login needed)\ncyolo profile default personal\ncyolo profile add work                       # creates ~/.claude-work/, auto-opens `claude` → run /login with your second Anthropic account\ncyolo profile list                           # both profiles listed with their emails\ncd ~/work/project \u0026\u0026 cyolo profile init work # bind this tree to the work profile\ncyolo                                        # from inside ~/work/... → work account; elsewhere → personal\n```\n\nIf you skip the auto-login (`--no-login` on `cyolo profile add`), you can\nalways run `cyolo profile login \u003cname\u003e` later.\n\n## Usage — profile subcommands\n\nTen subcommands cover the full profile lifecycle. Every subcommand also\naccepts `--help` for a focused reference (e.g. `cyolo profile add --help`).\nTwo aliases are recognised: `rm` ⇌ `remove`, `list` ⇌ `ls`.\n\n### add\n\n```bash\ncyolo profile add \u003cname\u003e [config-dir] [--no-share] [--no-login]\n```\n\nRegister a new profile. `config-dir` defaults to `~/.claude-\u003cname\u003e`.\nMissing directories are created with `0700`. The six shared items are\nsymlinked from `~/.claude/` unless `--no-share` is given. Registering\n`~/.claude` itself creates no symlinks (it is the source). To mark the\nnew profile as the default, run `cyolo profile default \u003cname\u003e`\nafterward.\n\nImmediately after registration, `cyolo` launches `claude` with the new\n`CLAUDE_CONFIG_DIR` so you can run `/login` and bind the intended Anthropic\naccount to this profile's Keychain entry. Pass `--no-login` to skip the\nlaunch (useful when you are re-registering a profile that already has a\nvalid token, or when running in CI).\n\nOnce the login session (or `--no-login`) settles, `add` seeds\n`\u003cconfig_dir\u003e/.claude.json` with the `mcpServers` object from\n`~/.claude.json` — see [`sync-mcp`](#sync-mcp) for details. The first\ntime you see `↳ synced N User MCP server(s)` in the output, that is\nthis step running. **The sync runs even if `/login` exits abnormally**\n(e.g. Ctrl+C, network error) — a warning is printed and the profile is\nleft with MCPs seeded so a later `cyolo profile login \u003cname\u003e` only\nneeds to retry auth, not the MCP plumbing.\n\n```bash\ncyolo profile add client ~/.claude-client-a\ncyolo profile add scratch --no-login          # register without spawning claude\n```\n\n### login\n\n```bash\ncyolo profile login \u003cname\u003e\n```\n\nRe-run the interactive login flow for a registered profile. Useful when\na refresh token expires or when you want to swap the profile to a\ndifferent Anthropic account. Equivalent to the launch that `add` does by\ndefault.\n\n### whoami\n\n```bash\ncyolo profile whoami\n```\n\nLike `current`, but also prints the `oauthAccount.emailAddress` extracted\nfrom the resolved profile's `.claude.json`. If the profile has never been\nlogged in, the email line reads `(needs login — run cyolo profile login \u003cname\u003e)`.\n\n### rm\n\n```bash\ncyolo profile rm \u003cname\u003e\n```\n\nRemove a profile from `~/.cyolo/config.json`. The on-disk directory is\npreserved — delete it yourself with `rm -rf ~/.claude-\u003cname\u003e` if needed.\n\n### list\n\n```bash\ncyolo profile list\n```\n\nTabulate all registered profiles. The default is marked `*`. Each row also\nshows the email address stored in that profile's `.claude.json`, or\n`(needs login)` when the profile has no token yet (run\n`cyolo profile login \u003cname\u003e` to fix).\n\n```\n* personal -\u003e /Users/codingmax/.claude            skyfe79@gmail.com\n  work     -\u003e /Users/codingmax/.claude-work       work@example.com\n  client   -\u003e /Users/codingmax/.claude-client-a   (needs login)\n```\n\n### default\n\n```bash\ncyolo profile default [name | --unset]\n```\n\nWith no arguments, prints the current default. Given a registered name,\nsets it. `--unset` clears the default (no fallback during resolution).\n\n```bash\ncyolo profile default work\ncyolo profile default --unset\n```\n\n### init\n\n```bash\ncyolo profile init [name]\n```\n\nWrite `.claude-profile.json` into the current directory so walk-up detection\nresolves to `name` from this tree. Refuses to overwrite an existing file.\n\nResolution order:\n\n1. `name` argument given → use it.\n2. No argument, default profile set → use the default.\n3. No argument, no default, running on a TTY → **interactive menu** shows\n   all registered profiles with their emails, plus `n` (register a new\n   profile and `/login`), `d` (pin this directory to `~/.claude`), and\n   `q` (do nothing).\n4. No argument, no default, non-TTY → error (safe for CI / scripts).\n\n```bash\ncyolo profile init work      # explicit\ncyolo profile init           # picks default, or pops the menu\n```\n\nThe same menu fires on a bare interactive `cyolo` invocation when\nnothing resolves — see *Interactive picker when nothing is bound*\nbelow for the full option breakdown (including `d`'s MCP sync side\neffect and `q`'s clean-exit behavior).\n\nMenu example:\n\n```\nℹ no profile is bound to this directory. Pick one:\n\n  1) personal  skyfe79@gmail.com\n  2) work      work@example.com\n  3) client-a  (needs login)\n  n) new      register a new profile + /login\n  d) default  pin this directory to ~/.claude (Claude Code default)\n  q) quit     do nothing\n\nSelection: 2\nCreated .claude-profile.json (profile: work)\n```\n\n### Interactive picker when nothing is bound\n\nRunning a **bare** `cyolo` (no args) in a directory with no resolved profile\n(no walk-up `.claude-profile.json`, no default, no inline `config_dir`) now\ndrops you into the same picker that `cyolo profile init` uses. You can\nbind the directory to a profile in one step instead of aborting and re-running:\n\n```\n$ cyolo\nℹ no profile is bound to this directory. Pick one:\n\n  1) personal  skyfe79@gmail.com\n  2) work      work@example.com\n  n) new      register a new profile + /login\n  d) default  pin this directory to ~/.claude (Claude Code default)\n  q) quit     do nothing\n\nSelection: 2\nCreated .claude-profile.json (profile: work)\n↳ added .claude-profile.json to /Users/you/repo/.git/info/exclude\n# claude --dangerously-skip-permissions launches with CLAUDE_CONFIG_DIR=~/.claude-work\n```\n\nDetails:\n\n- **Scope**: only bare `cyolo` triggers the picker. Pass-through invocations\n  (`cyolo -p \"...\"`, `cyolo --version`, etc.) stay out of your way.\n- **TTY only**: without an interactive stdin/stdout, the picker is skipped\n  and a one-line stderr hint is printed instead (`ℹ no profile detected — run\n  \\`cyolo profile init\\` to bind this directory`), preserving scriptable behavior.\n- **Auto-excluded from git**: when the current directory sits inside a git\n  repository (including a worktree or submodule), `.claude-profile.json` is\n  appended to `\u003cgitdir\u003e/info/exclude` so it stays untracked without editing\n  the committed `.gitignore`. Idempotent — re-running does nothing if the\n  entry is already there. This applies to both the picker flow and any\n  explicit `cyolo profile init \u003cname\u003e`.\n- **Default** (`d`): pins this directory to `~/.claude` by writing a marker\n  with `{\"config_dir\": \"~/.claude\"}`. The tilde is kept literal and expanded\n  at resolution time, so the marker stays portable across machines. Useful\n  when you want this tree to follow Claude Code's default account regardless\n  of any `default` profile you set later via `cyolo profile default`.\n  Picking `d` also runs [`sync-mcp`](#sync-mcp) against `~/.claude/.claude.json`\n  so the first launch from that marker already carries your User MCPs\n  (Claude Code reads `\u003cCLAUDE_CONFIG_DIR\u003e/.claude.json`, not the env-unset\n  `~/.claude.json`, so without this sync the MCP list would look empty).\n- **Quit** (`q`): exits cyolo cleanly without writing a marker and without\n  launching claude. The original pass-through to `~/.claude` (unset\n  `CLAUDE_CONFIG_DIR`) is still in effect for the non-picker paths —\n  `cyolo \u003cargs...\u003e` and non-TTY invocations.\n- **New** (`n`): `add` registers a fresh profile and launches `claude /login`\n  so you can authenticate. When that session exits, cyolo stops — run `cyolo`\n  again to start a working session with the new profile.\n\n### current\n\n```bash\ncyolo profile current\n```\n\nPrint the profile that would be used by `cyolo` right now (walk-up →\ndefault → unset). Does not launch `claude`.\n\n```\nprofile: work\nconfig_dir: /Users/codingmax/.claude-work\nsource: /Users/codingmax/work/client-a/.claude-profile.json\n```\n\n### link\n\n```bash\ncyolo profile link \u003cname\u003e\n```\n\nIdempotently (re)create the six shared symlinks for an already-registered\nprofile. Use this after adding a new shared item in `~/.claude/` or if a\nsymlink is broken.\n\n### sync-mcp\n\n```bash\ncyolo profile sync-mcp \u003cname\u003e     # sync a single profile\ncyolo profile sync-mcp --all      # sync every registered profile + ~/.claude\n```\n\nCopy the `mcpServers` object out of `~/.claude.json` (Claude Code's\nuser-level config when `CLAUDE_CONFIG_DIR` is unset) into\n`\u003cconfig_dir\u003e/.claude.json` for the target profile. The write is atomic\n(temp + rename, `0o600`) and preserves every other key in the target\n(`oauthAccount`, `projects`, analytics caches, etc.) — only `mcpServers`\nis touched.\n\nWhy this exists: Claude Code looks for `mcpServers` inside whatever\ndirectory `CLAUDE_CONFIG_DIR` points at, which means a fresh profile\nshows an empty \"User MCPs\" section even though `~/.claude.json` has\nthem all. cyolo seeds the target automatically during `profile add`\nand when the picker's `d) default` option is selected; use\n`sync-mcp` to refresh a profile **after** you install a new MCP in\n`~/.claude.json`, or to retro-sync profiles that predate this feature.\n\n`--all` also targets `~/.claude/.claude.json` (not just registered\nprofiles) so that the picker's `d) default` flow — which sets\n`CLAUDE_CONFIG_DIR=~/.claude` — resolves to the same MCP list as the\nenv-unset default.\n\n## Usage — diet\n\n`diet` reports and reclaims orphaned Claude Code data. By default it is\nread-only.\n\n```bash\ncyolo diet                              # dry-run report, current profile\ncyolo diet --apply                      # actually remove orphaned entries + session folders\ncyolo diet --stale-days 90              # include projects idle ≥ 90 days (dry-run)\ncyolo diet --stale-days 90 --apply      # remove orphaned + prune stale history\ncyolo diet --cache                      # include cache dirs (statsig, shell-snapshots, file-history)\ncyolo diet --profile work               # operate on a specific registered profile\ncyolo diet --all                        # iterate every registered profile\n```\n\nSample dry-run report (tree format, matches the spec):\n\n```\n$ cyolo diet\ncyolo diet — analyzing /Users/codingmax/.claude\n\n~/.claude.json:                          1.2 MB  (6840 lines)\n  ├─ orphaned projects (5):              980 KB  (removable)\n  │   ├─ /Users/codingmax/Private/labs/test-bot      320 KB\n  │   ├─ /Users/codingmax/tmp/experiment             210 KB\n  │   └─ ... 3 more\n  └─ active configuration:               220 KB  (keep)\n\n~/.claude/projects/:                      847 MB\n  └─ orphaned session folders (5):       623 MB  (removable)\n\nTotal reclaimable: 624 MB\n\nRun with --apply to proceed.\n```\n\nSafety: `--apply` automatically writes a timestamped backup\n(`~/.claude.json.backup-\u003cYYYYMMDDHHMMSS\u003e`) and aborts if a `claude`\nprocess is already running.\n\n## How it works\n\n### Walk-up resolution\n\nAt every invocation `cyolo` searches the current directory upward for\n`.claude-profile.json`, the same way `git` finds `.git`. The first file\nfound wins. Without one, the default profile is used; without a default,\n`CLAUDE_CONFIG_DIR` is left unset and Claude Code falls back to\n`~/.claude` (matching the original `cyolo()` shell function exactly).\n\nException: a **bare interactive** `cyolo` invocation with no resolved\nprofile and no default intercepts that last step with the picker described\nin *Interactive picker when nothing is bound*. Pass-through invocations\n(`cyolo \u003cargs...\u003e`, non-TTY stdin/stdout) keep the silent `~/.claude`\nfallback.\n\n### Symlink-based sharing\n\nEach non-source profile directory (everything except `~/.claude` itself)\nis a plain directory with six symlinks back into `~/.claude/`:\n\n```\nCLAUDE.md             → ~/.claude/CLAUDE.md\nsettings.json         → ~/.claude/settings.json\nsettings.local.json   → ~/.claude/settings.local.json\ncommands/             → ~/.claude/commands\nskills/               → ~/.claude/skills\nagents/               → ~/.claude/agents\n```\n\nInstall a skill once (`cd ~/.claude/skills \u0026\u0026 npx install-some-skill`) and\nevery profile sees it. Credentials, session history, and runtime caches\nstay per-profile — they are never symlinked.\n\n### Diet orphan detection\n\n`diet` reads `~/.claude.json`, iterates the `projects` map, and flags\nevery key whose filesystem path no longer exists. For each orphan it also\nlocates the matching session folder under `~/.claude/projects/` (the key\nis path-encoded) and sums its size. `--apply` removes both the JSON\nentries and the session folders, atomically rewriting the config via a\ntemp file + `rename`.\n\n## Building from source\n\n```bash\ncargo build --release        # binary at target/release/cyolo\ncargo test                   # unit tests (all modules)\n```\n\nThere are no external build tools or codegen steps — a plain `cargo build`\nis sufficient.\n\n## Platform support\n\nmacOS and Linux only. Windows is **not supported**: the symlink model,\n`0700` permission enforcement, and `pgrep`-based running-process\ndetection all assume POSIX semantics. A WSL or MSYS2 environment may\nwork but is untested and unsupported.\n\n## License\n\nMIT — see [LICENSE](LICENSE) for the full text.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskyfe79%2Fcyolo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskyfe79%2Fcyolo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskyfe79%2Fcyolo/lists"}