Wivuu.Tabard 0.5.0

dotnet tool install --global Wivuu.Tabard --version 0.5.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Wivuu.Tabard --version 0.5.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Wivuu.Tabard&version=0.5.0
                    
nuke :add-package Wivuu.Tabard --version 0.5.0
                    

tabard

CI NuGet

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 ~/.claude on 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 0600 inside the 0700 profile 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 /logout inside 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 releaselinux-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_DIR is 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.json linking may fail there — tabard warns and carries on.
  • Profile directories are chmod 0700 on Unix, and a settings.json tabard writes is 0600.
  • 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 ~/.claude at all. If the profile you deleted was the linked one, ~/.claude is repointed at a survivor.
  • ~/.claude and ~/.claude.json are 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.json is 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 ~/.claude holds a claude migrate-installer install (a local/ directory that ~/.local/bin/claude points into) and the profile you're switching to doesn't, tabard leaves the link where it is rather than breaking the claude command. The session still gets the right profile through CLAUDE_CONFIG_DIR.

Contributing

Building, testing, CI and the release process are documented in CONTRIBUTING.md.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.5.0 45 8/12/2026
0.4.0 102 7/26/2026
0.3.0 96 7/26/2026
0.2.0 95 7/26/2026
0.1.1 98 7/25/2026
0.1.0 102 7/25/2026