Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: release
# regenerates a Homebrew tap formula (if HOMEBREW_TAP_TOKEN is set).
#
# Local smoke test before pushing a tag:
# bun build --compile --define 'process.env.HATS_VERSION="0.1.0"' src/index.ts --outfile hats
# bun build --compile --define 'process.env.HATS_VERSION="0.2.0"' src/index.ts --outfile hats
# ./hats --version && ./hats ls && ./hats which rc

on:
Expand Down Expand Up @@ -79,6 +79,7 @@ jobs:
- name: Package tarball
run: |
(cd build && tar czf ../hats-${{ matrix.target }}.tar.gz hats)
tar tzf hats-${{ matrix.target }}.tar.gz hats >/dev/null
# `shasum -a 256` (Perl script) is present on both macOS and Ubuntu;
# `sha256sum` is coreutils-only and missing on macOS runners.
shasum -a 256 hats-${{ matrix.target }}.tar.gz | tee hats-${{ matrix.target }}.tar.gz.sha256
Expand Down Expand Up @@ -170,6 +171,7 @@ jobs:

def install
bin.install "hats"
generate_completions_from_executable(bin/"hats", "completion")
end

test do
Expand All @@ -189,4 +191,4 @@ jobs:
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
git add Formula/hats.rb
git commit -m "hats ${VERSION}"
git push origin HEAD:main
git push origin HEAD:main
179 changes: 125 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,119 +4,190 @@

<h1 align="center">hats</h1>

<p align="center">Run Claude Code, Codex, and Gemini profiles side by side.</p>
<p align="center">Run multiple AI CLI setups side by side — one hat per terminal, zero shell pollution.</p>

<p align="center">
<a href="#install"><img src="https://img.shields.io/badge/install-Homebrew-FBB040?logo=homebrew&logoColor=111827" alt="Install with Homebrew"></a>
<a href="https://github.com/Colafornia/hats/actions/workflows/ci.yml"><img src="https://github.com/Colafornia/hats/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license"></a>
</p>

`hats` runs each profile in a clean child process. Use a company gateway in one
terminal, a personal subscription in another, and a local model in a third. It does
not switch global providers, pollute your shell, or copy credentials.
## Install

```bash
hats run work # one terminal
hats run personal # another terminal
curl -fsSL https://raw.githubusercontent.com/Colafornia/hats/main/install.sh | sh
```

## Install
No Node.js, Bun, `sudo`, or shell startup-file changes required.

Prefer Homebrew?

```bash
brew install colafornia/tap/hats
```

hats ships as a standalone binary. No Node.js or Bun required.
## Quick start

```bash
hats add work claude
hats work
```

That is the whole default workflow. A hat starts its CLI in a clean child process with
the config you chose. Your current shell and other terminals stay unchanged.

Create a hat by naming it and the CLI it should launch:

## Quick Start
```bash
hats add personal codex
hats personal
```

Create your first hat with the interactive setup:
To feed one env file to different CLIs, create both hats and point them at the same file
with `hats edit`:

```bash
hats add
hats add write claude
hats add review codex
hats edit
```

Then run the name you chose with `hats run <name>`.
```toml
[profiles.write]
launch = "claude"
env_file = "~/.config/company-ai.env"

[profiles.review]
launch = "codex"
env_file = "~/.config/company-ai.env"
```

```bash
hats write
hats review # another terminal, same env file, independent process
```

`hats add` without arguments opens a short wizard. Use `hats edit` for hand-written
config and advanced env references.

## Why

Most AI CLI switchers mutate global state: they export provider env vars, rewrite shared
tool config, or silently change what every terminal will use next. That breaks down when
you need more than one setup open at once.

`hats` makes the switch explicit:
`hats` makes each launch explicit and local to one child process. It also removes
inherited provider credentials such as `ANTHROPIC_*`, `OPENAI_*`, and `CODEX_*` unless
the selected hat adds them back intentionally.

```bash
hats run company-claude
hats run personal-codex
hats run local-claude
```
Use hats for company gateways, personal subscriptions, local models, or any CLI that
needs a repeatable per-process environment. See
[Advanced configuration](docs/advanced.md) for env references, shared env files, local
models, and hand-written config.

Each command creates one isolated child process. The parent shell keeps none of the
profile's credentials, `ANTHROPIC_*`, `OPENAI_*`, or `CODEX_*` values.
## Run multiple subscriptions side by side

Use hats when you want to:
Multiple subscriptions are optional. The simple `hats <name>` workflow above does not
require isolated CLI homes.

- run multiple AI coding subscriptions or OAuth accounts at the same time
- keep company gateways, personal accounts, and local models from leaking into each other
- replace global switchers that rewrite shared `~/.claude` or `~/.codex` config
- launch any CLI with one profile and zero residue in the current shell
hats supports isolated accounts for **Codex** and **Claude Code**. Each isolated hat
gets its own login, config, history, and settings, so work and personal subscriptions can
run at the same time without overwriting each other.

## Common Setups
### Two Codex accounts

### Codex official subscription
```bash
hats add codex-work codex --isolated
hats add codex-personal codex --isolated

Use your normal `~/.codex` login:
hats codex-work login
hats codex-personal login

```bash
hats add codex codex
hats run codex
hats codex-work
# In another terminal:
hats codex-personal
```

### Multiple Codex accounts

Use a second Codex or ChatGPT account without touching the default `~/.codex`:
### Two Claude Code accounts

```bash
hats add codex-personal codex --home
hats run codex-personal login
hats run codex-personal
hats add claude-work claude --isolated
hats add claude-personal claude --isolated

hats claude-work
# In another terminal:
hats claude-personal
```

`--home` gives this hat its own CLI home under `~/.config/hats/homes/<name>`.
Codex opens `login`; Claude Code opens its onboarding flow on first run. Complete each
login once, then launch that account anytime with its hat name.

See [Advanced configuration](docs/advanced.md) for gateways, local models, shared env
files, and hand-written config.
### How isolation works

## Commands
`--isolated` creates a dedicated CLI home under `~/.config/hats/homes/<name>`. hats
also removes inherited provider credentials from the child process, preventing a
shell-level API key from silently overriding the selected OAuth account. Add a key
explicitly to the hat only when that override is intentional.

Use a recent Claude Code release: isolated Claude accounts rely on its per-directory
keychain storage.

### Other CLIs

hats can launch any CLI with per-process env and config. Credential-home isolation is
currently available for Codex and Claude Code. For tools with shared credential storage,
hats fails clearly instead of claiming the accounts are separated:

- Gemini uses a fixed keychain entry. Use explicit env configuration with
`GEMINI_CLI_HOME` and `GEMINI_FORCE_FILE_STORAGE=true` if you accept that manual
setup.
- OpenCode stores credentials outside its config home. Use provider keys through the
hat's `env` or `env_file`; redirecting `XDG_DATA_HOME` would affect every XDG app in
the child process and is not recommended.

hats does not manage OAuth or report login state. The underlying CLI remains responsible
for login and token refresh.

### Start here
## Commands

```text
hats add [<name> <command...>] create a hat
hats run <profile> [args...] run the profile's launch command
hats <hat> [args...] launch a hat (same as hats run <hat>)
hats edit open the config in $EDITOR
hats ls list profiles
hats ls list hats
```

<details>
<summary>More</summary>

```text
hats show profiles and first-run hints
hats show hats and first-run hints
hats init write an example config
hats add <name> <command...> --home
hats exec <profile> -- <cmd> run another command with the profile env
hats which <profile> inspect a profile, with secrets masked
hats setenv <profile> --file .env merge env vars from KEY=value lines
hats rm <profile> delete a profile entry
hats add <name> <command...> --isolated
hats exec <hat> -- <cmd> run another command with the hat's env
hats which <hat> inspect a hat, with secrets masked
hats setenv <hat> --file .env merge env vars from KEY=value lines
hats rm <hat> delete a hat
hats completion <shell> output Bash, Zsh, or Fish completion code
```

</details>

`hats add` without arguments opens a short wizard: name, launch command, and whether
this hat needs a separate login.
## Shell completion

Homebrew enables completion automatically. For other installs, add the command for
your shell to its startup file:

```zsh
eval "$(hats completion zsh)"
```

```bash
eval "$(hats completion bash)"
```

```fish
hats completion fish | source
```

## Non-goals

Expand All @@ -125,7 +196,7 @@ this hat needs a separate login.
- No OAuth management. The underlying CLI still owns login and refresh.
- No automatic `.zshrc` migration.
- No GUI desktop app launching in v0.1.
- No interactive profile picker. Switching stays explicit: `hats run <name>`.
- No interactive hat picker. Switching stays explicit: `hats <name>`.

## License

Expand Down
43 changes: 43 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/sh
set -eu

case "$(uname -s)" in
Darwin) os=darwin ;;
Linux) os=linux ;;
*) echo "hats: unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac

case "$(uname -m)" in
arm64|aarch64) arch=arm64 ;;
x86_64|amd64) arch=x64 ;;
*) echo "hats: unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

asset="hats-$os-$arch.tar.gz"
if [ -n "${HATS_VERSION:-}" ]; then
version=${HATS_VERSION#v}
base="https://github.com/Colafornia/hats/releases/download/v$version"
else
base="https://github.com/Colafornia/hats/releases/latest/download"
fi

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
curl -fsSL "$base/$asset" -o "$tmp/$asset"
curl -fsSL "$base/SHA256SUMS" -o "$tmp/SHA256SUMS"

expected=$(awk -v file="$asset" '$2 == file { print $1 }' "$tmp/SHA256SUMS")
[ -n "$expected" ] || { echo "hats: checksum missing for $asset" >&2; exit 1; }
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$tmp/$asset" | awk '{ print $1 }')
else
actual=$(shasum -a 256 "$tmp/$asset" | awk '{ print $1 }')
fi
[ "$actual" = "$expected" ] || { echo "hats: checksum verification failed" >&2; exit 1; }

tar xzf "$tmp/$asset" -C "$tmp" hats
install_dir=${HATS_INSTALL_DIR:-"$HOME/.local/bin"}
mkdir -p "$install_dir"
install -m 755 "$tmp/hats" "$install_dir/hats"
echo "installed hats to $install_dir/hats"
echo "Tip: enable Tab completion: https://github.com/Colafornia/hats#shell-completion"
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hats",
"version": "0.1.0",
"version": "0.2.0",
"description": "Per-terminal / per-process config isolator for Claude Code, Codex, and any CLI — switch hats without polluting your shell.",
"type": "module",
"bin": {
Expand Down Expand Up @@ -34,4 +34,4 @@
"tsx": "^4.19.0",
"typescript": "^5.6.0"
}
}
}
Loading