Wivuu.Tabard
0.5.0
dotnet tool install --global Wivuu.Tabard --version 0.5.0
dotnet new tool-manifest
dotnet tool install --local Wivuu.Tabard --version 0.5.0
#tool dotnet:?package=Wivuu.Tabard&version=0.5.0
nuke :add-package Wivuu.Tabard --version 0.5.0
tabard
A Claude Code profile switcher. Launch claude through tabard and pick which login to use.
Install
macOS / Linux — one line, no package manager:
curl -fsSL https://raw.githubusercontent.com/wivuu/wivuu.tabard/master/install.sh | sh
Windows — one line in PowerShell:
irm https://raw.githubusercontent.com/wivuu/wivuu.tabard/master/install.ps1 | iex
macOS / Linux — Homebrew, from this repo's own tap:
brew tap wivuu/tabard https://github.com/wivuu/wivuu.tabard
brew install tabard
Anywhere with .NET — as a global .NET tool:
dotnet tool install -g Wivuu.Tabard
The scripts and Homebrew all install the same native binary, so none of them need a .NET runtime;
the scripts land it in ~/.local/bin (%LOCALAPPDATA%\Programs\tabard on Windows, which they add
to your user PATH) after checking it against the release's own SHA256SUMS.txt. Re-run the same
line to upgrade — it installs over the copy already on your PATH, or tells you to use brew or
dotnet tool if one of those owns it. The .NET tool is portable IL instead: it needs the .NET 10
runtime (or newer) and lands in ~/.dotnet/tools, which has to be on your PATH. The package id is
Wivuu.Tabard either way; the command is always plain tabard. For a raw binary or a build from
source, see other ways to install.
Usage
tabard # pick a profile, then launch claude
tabard use work # launch a specific profile
tabard switch work # repoint ~/.claude at a profile, without launching
tabard add personal # create a profile (Anthropic or OpenRouter)
tabard ls # list profiles — never changes anything
| Command | What it does |
|---|---|
tabard [claude args...] |
Pick a profile, then launch |
tabard use <name> [-- ...] |
Launch a specific profile |
tabard switch [name] |
Point ~/.claude at a profile without launching |
tabard add <name> |
Create a profile, choosing Anthropic or OpenRouter |
tabard rm <name> |
Delete a profile |
tabard ls |
List profiles |
tabard openrouter <cmd> |
Configure a profile's OpenRouter settings |
tabard completion install |
Install tab completion for zsh, bash or PowerShell |
tabard -- <claude args> |
Force everything through to claude |
tabard --help is tabard's help; tabard -- --help reaches Claude Code's.
First run adopts your existing login, so there is nothing to set up: ~/.claude is moved to
~/.tabard/profiles/default and linked back. With one profile there is no picker — tabard goes
straight to claude. With more than one you get a picker, where the number beside a profile
launches it outright:
Select a Claude profile
1 > work erik@example.com - max - valid 7h
2 personal erik@gmail.com - pro - refresh due
up/down move enter/1-9 launch o reorder r rename x x delete q quit
Tab completion
tabard completion install
Tab now completes profile names, subcommands and flags — tabard use <tab> offers your profiles,
tabard openrouter set work --<tab> offers the model flags, and the flag after them offers model
slugs. zsh, bash and PowerShell are covered; the shell is taken from $SHELL unless you name one
(tabard completion install bash).
Installing writes the script to ~/.tabard/completions/ and appends one guarded line to your
~/.zshrc ($ZDOTDIR is honoured), your ~/.bashrc — or ~/.bash_profile on macOS, where a
terminal starts a login shell — or your PowerShell $PROFILE. It only ever appends, never
rewrites, and adds nothing twice, so it is safe to run again after an upgrade. Open a new shell to
pick it up.
Homebrew installs the zsh and bash completions itself, so brew install tabard needs none of this.
To load it without touching a startup file:
eval "$(tabard completion zsh)" # zsh
eval "$(tabard completion bash)" # bash
tabard completion pwsh | Out-String | Invoke-Expression # PowerShell
The script itself holds no list of profiles or commands — every tab press asks tabard, so a
profile you add or rename completes straight away and an upgrade needs no re-install.
How it works
Claude Code reads its whole config root from one directory (~/.claude by default), and the
CLAUDE_CONFIG_DIR environment variable redirects all of it — settings, .credentials.json,
CLAUDE.md, project configs, history.
So tabard does not copy credentials in and out of ~/.claude. Each profile is just a directory
under ~/.tabard/profiles/<name>, and switching means setting CLAUDE_CONFIG_DIR and exec'ing
claude. Two consequences worth understanding:
- No sync-back problem. Claude Code rotates refresh tokens in place. A design that copies
credentials into
~/.claudeon launch and copies them back on exit will silently kill saved profiles the first time a rotation isn't captured. Pointing at the directory avoids the whole class of bug. - Concurrent sessions work. Two terminals, two profiles, no clobbering.
Profiles are discovered by listing directories, so the set of profiles can't drift out of sync
with what's actually on disk. There are two state files: ~/.tabard/last, which records your
last choice so a bare claude can follow it, and ~/.tabard/order, one profile name per line,
which records the order you arranged the picker in. Both are hints over that directory listing —
neither can conjure a profile that isn't there or hide one that is, so a name either file holds
for a profile that has gone is simply ignored. Deleting either is harmless.
Behaviour
First run moves ~/.claude to ~/.tabard/profiles/default and links ~/.claude back at it.
Moving rather than copying keeps tokens and expiry intact with no second copy to go stale.
~/.claude.json gets the same treatment.
The picker keeps the order you put profiles in rather than floating whatever you launched last to
the top, so the frame is the same every run — same rows, same numbers, same cursor on row one.
Pressing 1-9 launches that profile in one keystroke. o enters reorder mode, where up/down
carry the highlighted profile instead of the cursor and o or enter are done; the new order is
saved as you go, so there is nothing to confirm and nothing to undo — move it back. A profile
created since you last arranged things, or created behind tabard's back, appears at the bottom
rather than pushing an arranged one down. Rows past the ninth get no number.
In the picker, x arms the highlighted row, a second x deletes it. Any other key disarms. r turns the
highlighted row into a text field holding its current name — enter renames the profile's directory, esc
leaves it alone, and a name that is already taken or would not travel is reported without leaving the field.
If the window is
too short for every profile the list scrolls and the help line says how many are off-screen; if
it is too short for a frame at all (under seven rows), tabard prints the list and asks you to name
one rather than draw something you can't read.
After a launch, ~/.claude is repointed at whichever profile you chose, so a bare claude
invocation stays consistent with your last choice. tabard ls never adopts or repoints anything —
it is safe to run first just to see what tabard would do.
tabard switch is that repointing on its own: it records the choice in ~/.tabard/last, moves the
~/.claude and ~/.claude.json links, and exits without starting anything. That's what you want
when the next claude is going to be typed by something other than tabard — an editor's integrated
terminal, a shell alias, a script. With a name it switches straight to it; without one it opens the
same picker, where enter and 1-9 switch instead of launching. It creates nothing, so with no
profiles yet it tells you to run tabard add <name>; with exactly one it just repairs the links.
OpenRouter profiles
A profile does not have to be an Anthropic login. Claude Code will talk to
OpenRouter given the right environment, and since it reads settings.json
out of CLAUDE_CONFIG_DIR, that environment can live inside the profile — no wrapper, no
exported variables, and a bare claude behaves exactly like tabard use <name>.
tabard add <name> asks which provider you want. Picking OpenRouter runs a short wizard: the API
key (from $OPENROUTER_API_KEY if it is set, otherwise typed in with the echo masked), then how to
map Claude Code's model tiers, then a summary to confirm. The key is checked against OpenRouter
before anything is written, so a typo is caught there rather than on your first prompt.
What it writes to ~/.tabard/profiles/<name>/settings.json:
{
"env": {
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api",
"ANTHROPIC_AUTH_TOKEN": "sk-or-v1-...",
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "~anthropic/claude-opus-latest",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "~anthropic/claude-sonnet-latest",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "~anthropic/claude-haiku-latest",
"ANTHROPIC_DEFAULT_FABLE_MODEL": "~anthropic/claude-fable-latest",
"CLAUDE_CODE_SUBAGENT_MODEL": "~anthropic/claude-opus-latest"
}
}
The empty ANTHROPIC_API_KEY is not an oversight — it stops Claude Code trying its own
authentication first. The five model slots default to OpenRouter's floating
~vendor/model-latest aliases so /model opus|sonnet|haiku|fable keeps meaning what it means and
nothing pins a version that ages out. openrouter/auto is offered too, if you would rather
OpenRouter picked per request.
The merge is non-destructive: every other key in settings.json, and every environment entry
tabard didn't set, is written back verbatim.
tabard openrouter add <name> Create an OpenRouter profile and launch it
tabard openrouter set <name> Change which models the profile uses
tabard openrouter key <name> Replace the profile's API key
tabard openrouter show <name> Print the settings, with the key redacted
tabard openrouter models [term] List the models OpenRouter offers
Everything the wizard asks can be given up front instead, which is what CI and dotfiles want:
tabard add cheap --openrouter --model qwen/qwen3-coder < /dev/null # key from $OPENROUTER_API_KEY
tabard openrouter add work --model auto --opus anthropic/claude-opus-5
tabard openrouter set work --sonnet qwen/qwen3-coder # leaves the other tiers alone
pbpaste | tabard openrouter key work --key-stdin
--model sets every tier at once (auto is shorthand for openrouter/auto), and the per-tier
flags — --opus, --sonnet, --haiku, --fable, --subagent — override it. There is
deliberately no --key flag: a key on the command line ends up in your shell history and in
anyone's ps output, so it comes from $OPENROUTER_API_KEY, from --key-stdin, or from the
masked prompt.
Two things worth knowing:
- A new profile is a fresh config directory, so Claude Code still runs its first-run onboarding the first time you launch it. That is Claude Code's, not tabard's.
- The key sits in plaintext in
settings.json,chmod 0600inside the0700profile directory — the same protection an OAuth token in the same directory gets. If a profile has both a saved Claude login and OpenRouter settings, the settings win; tabard says so when it notices, and/logoutinside Claude Code clears the login if the two disagree.
The model list comes from OpenRouter at wizard time, filtered to models that can call tools — Claude Code cannot work without those — plus the aliases and routers, which declare no parameters and would otherwise be filtered out of their own defaults. If OpenRouter can't be reached, a small built-in list is shown instead and any slug can still be set with a flag.
Other ways to install
The NuGet package id has to sit under the reserved Wivuu.* prefix; the command doesn't, which is
why dotnet tool install -g Wivuu.Tabard gives you a plain tabard.
To upgrade or remove what you installed above:
curl -fsSL https://raw.githubusercontent.com/wivuu/wivuu.tabard/master/install.sh | sh # upgrade
rm ~/.local/bin/tabard # remove
brew upgrade tabard ; brew uninstall tabard
dotnet tool update -g Wivuu.Tabard
dotnet tool uninstall -g Wivuu.Tabard
The install scripts take a few options, though curl | sh and irm | iex both need help passing
them — sh -s -- for the former, an explicit script block for the latter:
curl -fsSL https://raw.githubusercontent.com/wivuu/wivuu.tabard/master/install.sh | sh -s -- --dir ~/bin
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/wivuu/wivuu.tabard/master/install.ps1))) -InstallDir C:\bin
--version/-Version pins a release instead of taking the latest, --dir/-InstallDir picks the
directory, --force/-Force installs even when Homebrew or dotnet tool owns the tabard already
on your PATH, and -NoPath leaves the Windows user PATH alone. Each has an environment variable
equivalent (TABARD_VERSION, TABARD_INSTALL_DIR, TABARD_FORCE) for the piped forms.
For no runtime dependency and no package manager, grab a native binary for your platform from
the latest release — linux-x64,
linux-arm64, osx-arm64, osx-x64 and win-x64 are published with a SHA256SUMS.txt — and
drop it on your PATH. This is the same binary Homebrew installs.
From a checkout instead:
dotnet pack src/Wivuu.Tabard.CLI -c Release
dotnet tool install -g --add-source ./nupkg Wivuu.Tabard
dotnet tool install takes a package id, not a directory, so the local package feed
(--add-source ./nupkg) is what makes installing from source work — and dotnet tool update needs
the same --add-source to find it again. RollForward is Major, so any runtime from .NET 10 up
will run it.
Or build the native binary yourself and drop it on your PATH:
dotnet publish src/Wivuu.Tabard.CLI -c Release -r osx-arm64 # or linux-x64, win-x64
PublishAot turns on whenever a RID is given, so this produces a single ~2.4 MB native
executable. It stays off otherwise, which is what keeps dotnet pack producing a portable
tool package.
Notes
CLAUDE_CONFIG_DIRis always passed fully expanded. A~left in that value has been written literally by some versions, stranding credentials in a~folder under the working directory.- On Windows, real symlinks need Developer Mode or elevation, so tabard falls back to a directory
junction (
mklink /J), which doesn't. Files have no junction equivalent, so~/.claude.jsonlinking may fail there — tabard warns and carries on. - Profile directories are
chmod 0700on Unix, and asettings.jsontabard writes is0600. - Deleting a profile removes the directory first and drops the links only once that has worked, so
a delete that fails part-way can't leave you with no
~/.claudeat all. If the profile you deleted was the linked one,~/.claudeis repointed at a survivor. ~/.claudeand~/.claude.jsonare only ever replaced when they are links into~/.tabard/profiles. A real directory, or a link aimed anywhere else, is yours — tabard leaves it alone and says so.~/.claude.jsonis linked into the chosen profile even before that file exists. The dangling link is deliberate: Claude Code creates the file the first time it writes, and it lands inside the profile rather than being shared.- If
~/.claudeholds aclaude migrate-installerinstall (alocal/directory that~/.local/bin/claudepoints into) and the profile you're switching to doesn't, tabard leaves the link where it is rather than breaking theclaudecommand. The session still gets the right profile throughCLAUDE_CONFIG_DIR.
Contributing
Building, testing, CI and the release process are documented in CONTRIBUTING.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.