Version: 1.0 Date: 2026-03-06 Author: PRD-engineer Agent Status: Draft Location: /home/tanwa/hats/PRD.md
hats v1.0 is a complete rewrite of the multi-account CLI tool for Claude Code. The current v0.2.x architecture stores all account credentials as sibling files in a shared ~/.claude/ directory and swaps the active .credentials.json file under flock before launching Claude Code. This design has fundamental flaws: race conditions between concurrent sessions, credential corruption from save-back logic, profile contamination across accounts, and a growing pile of defensive workarounds (v0.2.1 through v0.2.3 are all bug fixes for these systemic issues).
v1.0 eliminates the entire class of credential-swapping bugs by giving each account its own complete CLAUDE_CONFIG_DIR directory. Claude Code's CLAUDE_CONFIG_DIR environment variable is set to point at the account's directory, so Claude reads and writes credentials, state, and cache files in isolation. No file swapping, no locking, no save-back, no contamination. Concurrent sessions are inherently safe because they never touch each other's files.
The new architecture introduces a base/ template directory that holds shared resources (settings, hooks, MCP config, CLAUDE.md, agents, skills, etc.) which account directories symlink to. Users can selectively unlink any resource to make it account-specific, or link it back to shared. The migration from v0.2.x is handled by hats init, which restructures ~/.claude/ into the new ~/.hats/ layout and creates a backwards-compatible symlink so bare claude continues to work.
- Race conditions on concurrent sessions: Two
hats swapcalls starting near-simultaneously can read each other's credentials because the swap window (copy + launch) is not atomic relative to Claude's file read - Credential corruption: The save-back logic (copying refreshed tokens back to the account file after Claude exits) can overwrite valid credentials with stale or failed-auth data
- Profile contamination: The cached identity in
~/.claude.jsonis shared across all accounts, causing wrong-user displays and requiring defensive save/restore logic - Growing complexity: v0.2.1, v0.2.2, and v0.2.3 are all bug fixes for the credential-swapping architecture. Each fix adds more edge-case handling without fixing the root cause
- No shared config management: Users who want the same
settings.jsonorCLAUDE.mdacross accounts must manually keep them in sync - flock dependency: The
flockrequirement breaks macOS compatibility and adds unnecessary complexity
- Users with multiple Claude Code subscriptions cannot safely run concurrent sessions, limiting productivity
- The credential corruption bugs erode trust in the tool — users discover auth failures hours after the corruption happened
- macOS users cannot use hats at all without installing third-party
flock - The fragile architecture discourages contributors and makes the codebase harder to maintain
- Pattern: Single-file bash script (780 lines), self-contained with
{ }guard for atomic script loading - Entry Point:
/home/tanwa/hats/hats:720—casestatement routing commands - Core Modules: Helper functions (lines 22-179), command implementations (lines 182-717)
- Script Guard: Lines 8-11 —
{ }block ensures the entire script is read into memory before execution, protecting against mid-session upgrades
- Credential management:
_creds_file()at line 23,_accounts()at line 26 — file-naming convention for per-account credentials - Profile management:
_save_profile()at line 68,_restore_profile()at line 87 — identity swap logic (will be eliminated in v1.0) - Shell integration:
cmd_shell_init()at line 694 — generates shell functions per account (pattern to preserve) - Token inspection:
_token_info()at line 43 — python3-based token parsing (reuse as-is) - Install script:
/home/tanwa/hats/install.sh— atomic install via temp file + mv (pattern to preserve) - Version bump:
/home/tanwa/hats/bump— sed-based version update (reuse as-is)
- Tech Stack: Bash (set -euo pipefail), Python3 (JSON parsing only), coreutils
- Data Layer: Flat JSON files with
chmod 600permissions - External Dependencies:
flock(util-linux) for locking,python3for JSON,claudeCLI - Testing Infrastructure: None implemented. Test plan exists at
/home/tanwa/hats/TEST_PLAN.mddescribing BATS-core framework (133 planned tests, zero written)
- Existing symlinks in ~/.claude/: User has
agents,skills, andCLAUDE.mdsymlinked to external paths (/home/tanwa/opt/scripts/) — migration MUST preserve these - ~/.claude.json state file: Lives OUTSIDE
~/.claude/at~/.claude.json— but withCLAUDE_CONFIG_DIR, Claude Code uses$CLAUDE_CONFIG_DIR/.claude.jsoninstead, which eliminates the shared-state problem - Large directories:
session-env/(282 entries),file-history/(72 entries),debug/(73K) — these should be symlinked to base, not copied per account - Existing accounts: User has 3 accounts (shannon, monet, debussy) that need migration
- Security warnings state files: Multiple
security_warnings_state_*.jsonfiles in~/.claude/— should be treated as shared resources
- flock dependency: Lines 126-131, 389-393, 408, 424 — entire locking mechanism is unnecessary with per-directory isolation
- Save-back logic: Lines 407-409 — token refresh save-back is the root cause of credential corruption
- Profile swap: Lines 68-119 — profile save/restore is a workaround for shared state file
- Credential contamination detection: Lines 541-598 — the
hats fixcontamination detector is a symptom of the architecture, not a feature - Stash/unstash commands: Lines 670-692 — workaround for adding new accounts; unnecessary when each account has its own directory
Replace the credential-swapping architecture with per-account CLAUDE_CONFIG_DIR directories. Each account gets a complete Claude configuration directory with its own credentials and state. Shared resources (settings, hooks, etc.) are symlinked to a base/ template directory. Running an account is simply CLAUDE_CONFIG_DIR=~/.hats/claude/<name> claude "$@" — no swapping, no locking, no save-back.
- As a multi-account user, I want to run concurrent Claude Code sessions under different accounts so that I can work on multiple projects simultaneously without credential conflicts
- As a user, I want my settings, hooks, and MCP config shared across all accounts so that I only configure them once
- As a user, I want to selectively isolate specific config files per account (e.g., different CLAUDE.md for work vs personal) so that I can customize per-account behavior
- As a user, I want
hats initto migrate my existing v0.2.x setup so that I don't lose my accounts or configuration - As a user, I want bare
claudeto work as my default account so that my workflow is unchanged - As a macOS user, I want hats to work without
flockso that I can use the tool without third-party dependencies - As a user, I want to see which resources are linked (shared) vs isolated per account so that I understand my configuration
- As a user, I want to add a new account by simply running
hats add <name>which triggersclaude auth loginin the new account's isolated directory
- Zero credential corruption in concurrent sessions (inherent from architecture)
- All existing v0.2.x accounts migrated successfully via
hats init - Bare
claudecommand works as default account via~/.claudesymlink - No
flockdependency — works on macOS and Linux without extra packages - All shared resources properly symlinked, credentials properly isolated
- Shell integration (
hats shell-init) generates working account functions -
hats link/hats unlinkcorrectly toggle resource sharing - Token inspection (
hats list) shows auth status for all accounts - Existing symlinks in
~/.claude/(agents, skills, CLAUDE.md pointing to external paths) preserved through migration - File permissions (600) maintained on credential files
Complete rewrite of the hats script. The { } guard pattern from line 8-11 is preserved. Python3 usage for JSON parsing is preserved. The _token_info() function is reused with minimal changes. Everything else is new.
~/.hats/
├── config.toml # global hats config (default account, provider settings)
├── claude/
│ ├── base/ # template — never run directly
│ │ ├── settings.json
│ │ ├── hooks.json
│ │ ├── .mcp.json
│ │ ├── CLAUDE.md → /home/tanwa/opt/scripts/CLAUDE.md (external symlink preserved)
│ │ ├── agents → /home/tanwa/opt/scripts/claude-agents (external symlink preserved)
│ │ ├── skills → /home/tanwa/opt/scripts/claude/skills (external symlink preserved)
│ │ ├── projects/
│ │ ├── plugins/
│ │ ├── debug/
│ │ ├── session-env/
│ │ ├── shell-snapshots/
│ │ ├── file-history/
│ │ ├── downloads/
│ │ ├── cache/
│ │ ├── hooks/
│ │ ├── stats-cache.json
│ │ └── ...everything else from ~/.claude/
│ ├── shannon/ # runnable account
│ │ ├── .credentials.json # ISOLATED (own copy)
│ │ ├── .claude.json # ISOLATED (own copy)
│ │ ├── settings.json → ../base/settings.json
│ │ ├── hooks.json → ../base/hooks.json
│ │ ├── .mcp.json → ../base/.mcp.json
│ │ ├── CLAUDE.md → ../base/CLAUDE.md
│ │ ├── agents → ../base/agents
│ │ ├── skills → ../base/skills
│ │ ├── projects → ../base/projects
│ │ └── ...all other resources → ../base/...
│ ├── monet/ # same structure
│ └── debussy/ # same structure
# hats swap shannon -- --model opus
CLAUDE_CONFIG_DIR="$HOME/.hats/claude/shannon" claude --model opus
# Shell function generated by shell-init:
shannon() { CLAUDE_CONFIG_DIR="$HOME/.hats/claude/shannon" claude "$@"; }That's it. No credential swapping, no locking, no save-back, no profile restore.
~/.claude → ~/.hats/claude/shannon/ # symlinkWhen user runs bare claude, it reads ~/.claude/ which resolves to the default account's directory. Changing the default updates the symlink target.
v0.2.x Architecture:
┌──────────────────────────────┐
│ ~/.claude/ │
│ .credentials.json (SHARED)│ ← flock swap before launch
│ .credentials.X.json (each) │ ← copy to/from shared on swap
│ .profile.X.json (each) │ ← save/restore identity
│ settings.json (SHARED)│
│ CLAUDE.md (SHARED)│
└──────────────────────────────┘
↓ race condition window
claude reads .credentials.json
v1.0 Architecture:
┌──────────────────────────────┐
│ ~/.hats/claude/ │
│ base/ (template) │ ← never run directly
│ shannon/ (account) │ ← CLAUDE_CONFIG_DIR points here
│ .credentials.json (OWN) │ ← claude reads THIS directly
│ .claude.json (OWN) │ ← claude writes THIS directly
│ settings.json → base/ │ ← shared via symlink
│ monet/ (account) │ ← completely independent
│ .credentials.json (OWN) │
│ ... │
└──────────────────────────────┘
↓ no race — each session has own dir
CLAUDE_CONFIG_DIR=shannon/ claude
- Location: Parsed inline in the
hatsscript - Purpose: Store default account name, provider path prefix
- Format: Simple TOML (parsed with grep/sed, no external dependency)
[hats]
default = "shannon"
[provider.claude]
path = "~/.hats/claude"_link_resource(): Create relative symlink from account dir to base dir for a resource_unlink_resource(): Copy resource from base (or current symlink target) into account dir, breaking the symlink_is_linked(): Check if a resource in an account dir is a symlink to base_list_resources(): Enumerate all resources in base, showing linked/isolated status per account
- Handles v0.2.x to v1.0 migration
- Detects existing
~/.hats/(idempotent) - Preserves external symlinks (CLAUDE.md, agents, skills)
- Creates account directories from existing
.credentials.<name>.jsonfiles - Creates backup before migration
Purpose: First-time setup or migration from v0.2.x
Flow:
- Check if
~/.hats/already exists (idempotent — report status and exit) - Create
~/.hats/claude/base/ - Move all contents of
~/.claude/to~/.hats/claude/base/EXCEPT:.credentials.*.jsonfiles (per-account credentials).profile.*.jsonfiles (per-account profiles — v0.2.x only, discarded).credentials.json(active credentials — discarded, replaced by per-account).credentials.lock(flock lock — discarded).credentials.json.stash(stash — discarded with warning).credentials.json.bak(backup — discarded).credentials.json.lock(old lock — discarded)
- Preserve external symlinks: if
base/CLAUDE.mdis a symlink to an external path, keep it as-is - For each
.credentials.<name>.jsonfound: a. Create~/.hats/claude/<name>/b. Move.credentials.<name>.jsonto<name>/.credentials.jsonc. Create empty.claude.jsonin<name>/(or extract from~/.claude.jsonif it's the current account) d. Create relative symlinks for all other resources:<name>/settings.json → ../base/settings.jsone. Setchmod 600on.credentials.json - Determine default account (from
~/.config/hats/defaultif exists, else first account) - Create
~/.hats/config.tomlwith default - Remove
~/.claude/directory (now empty except discarded files) - Create symlink:
~/.claude → ~/.hats/claude/<default>/ - Migrate vault: move
~/.config/hats/vault/to~/.hats/vault/(if exists)
Safety:
- Create
~/.hats/claude/base.migrating/first, rename tobase/on success - If migration fails mid-way,
~/.claude/still has original files - Only remove
~/.claude/after ALL account directories are created and verified
Edge cases:
- No existing accounts: create base from ~/.claude, prompt user to run
hats add <name> - ~/.hats already exists: report status, don't modify
- ~/.claude is already a symlink: detect, report, handle (may be from a previous partial migration)
Purpose: Create a new account directory and authenticate
Flow:
- Validate name (alphanumeric, hyphens, underscores, dots — no path separators)
- Check
~/.hats/claude/<name>/doesn't already exist - Create
~/.hats/claude/<name>/ - Create relative symlinks for all resources in
base/except.credentials.jsonand.claude.json - Run
CLAUDE_CONFIG_DIR=~/.hats/claude/<name> claude auth login - Verify
.credentials.jsonwas created in the account dir - Set
chmod 600on.credentials.json - If this is the only account, set as default
Rollback: If auth login fails or is cancelled, remove the account directory.
Purpose: Remove an account directory
Flow:
- Check account exists
- Warn if it's the default account
- Confirm removal (unless
--forceflag) - Remove
~/.hats/claude/<name>/directory - If was default, clear default (prompt to set new one)
Purpose: Show all accounts with auth status
Output format:
hats v1.0 — Claude Code Accounts
=================================
* shannon ok (expires 2026-03-07 14:30) [rc]
monet ok (access expired, will auto-refresh) [rc]
debussy ok (expires 2026-03-06 22:15) [no-rc]
3 accounts, 0 issues
Implementation: Iterate ~/.hats/claude/*/, skip base/, read each account's .credentials.json using _token_info().
Purpose: Get or set the default account
Set flow:
- Verify account exists
- Update
config.toml - Update
~/.claudesymlink:ln -sfn ~/.hats/claude/<name> ~/.claude
Purpose: Share a resource with base (replace local copy with symlink to base)
Flow:
- Verify account exists and resource exists in base
- Check resource is not already linked
- If resource is
.credentials.jsonor.claude.json, refuse (always isolated) - Remove account's local copy of resource
- Create relative symlink:
<account>/<resource> → ../base/<resource>
Purpose: Isolate a resource (copy from base, break symlink)
Flow:
- Verify account exists and resource is currently a symlink to base
- If resource is
.credentials.jsonor.claude.json, refuse (already isolated) - Resolve the symlink target and copy the actual file/directory
- Remove the symlink
- Place the copy in the account directory
Purpose: Show which resources are linked vs isolated for an account
Output format:
Account: shannon (default)
Directory: ~/.hats/claude/shannon/
ISOLATED (account-specific):
.credentials.json (always isolated)
.claude.json (always isolated)
LINKED (shared with base):
settings.json → ../base/settings.json
hooks.json → ../base/hooks.json
.mcp.json → ../base/.mcp.json
CLAUDE.md → ../base/CLAUDE.md → /home/tanwa/opt/scripts/CLAUDE.md
agents → ../base/agents → /home/tanwa/opt/scripts/claude-agents
skills → ../base/skills → /home/tanwa/opt/scripts/claude/skills
projects → ../base/projects
plugins → ../base/plugins
...
Purpose: Run Claude Code with a specific account's config directory
Implementation:
cmd_swap() {
local name="$1"; shift
[ "${1:-}" = "--" ] && shift
local account_dir="$HATS_DIR/claude/$name"
[ -d "$account_dir" ] || die "Account '$name' not found."
[ -f "$account_dir/.credentials.json" ] || die "Account '$name' has no credentials. Run: hats add $name"
CLAUDE_CONFIG_DIR="$account_dir" claude "$@"
}That's the entire swap implementation. No locking, no save-back, no profile restore.
Purpose: Output shell functions for each account
Output:
# Generated by hats shell-init
shannon() { CLAUDE_CONFIG_DIR="$HOME/.hats/claude/shannon" claude "$@"; }
monet() { CLAUDE_CONFIG_DIR="$HOME/.hats/claude/monet" claude "$@"; }
debussy() { CLAUDE_CONFIG_DIR="$HOME/.hats/claude/debussy" claude "$@"; }With --skip-permissions:
shannon() { CLAUDE_CONFIG_DIR="$HOME/.hats/claude/shannon" claude --dangerously-skip-permissions "$@"; }Purpose: Repair symlinks, verify auth status, detect issues
Flow:
- Verify
~/.hats/structure exists - For each account directory:
a. Check all expected symlinks point to valid targets in base
b. Repair broken symlinks (re-create pointing to base)
c. Verify
.credentials.jsonexists and is readable d. Check auth status via_token_info() - Verify
~/.claudesymlink points to default account's directory - Report issues found and repairs made
Purpose: Show version
Output: hats 1.0.0
| Command | Reason | Alternative in v1.0 |
|---|---|---|
stash |
Was needed to temporarily remove active creds for new login | hats add runs claude auth login in isolated dir |
unstash |
Counterpart to stash | Not needed |
backup |
Vault backup of credential files | Account dirs ARE the backup; use cp -r |
restore |
Restore from vault | Copy credentials back manually |
The vault system is removed because the per-account directory structure makes credentials self-contained and trivially backed up. A future version could add hats backup / hats restore as convenience wrappers around cp -r.
[hats]
version = "1.0.0"
default = "shannon"
[provider.claude]
path = "claude"
# Future: [provider.cursor], [provider.windsurf]Parsed with simple grep/sed — no TOML library needed for this minimal format.
Each account directory is a valid CLAUDE_CONFIG_DIR:
<account>/
├── .credentials.json # OAuth tokens (chmod 600, NEVER symlinked)
├── .claude.json # State/identity (NEVER symlinked)
├── settings.json → ../base/settings.json (symlink by default)
├── hooks.json → ../base/hooks.json (symlink by default)
├── .mcp.json → ../base/.mcp.json (symlink by default)
├── CLAUDE.md → ../base/CLAUDE.md (symlink by default)
├── agents → ../base/agents (symlink by default)
├── skills → ../base/skills (symlink by default)
├── projects → ../base/projects (symlink by default)
├── plugins → ../base/plugins (symlink by default)
├── debug → ../base/debug (symlink by default)
├── session-env → ../base/session-env (symlink by default)
├── shell-snapshots → ../base/shell-snapshots (symlink by default)
├── file-history → ../base/file-history (symlink by default)
├── downloads → ../base/downloads (symlink by default)
├── cache → ../base/cache (symlink by default)
├── hooks → ../base/hooks (symlink by default)
├── stats-cache.json → ../base/stats-cache.json (symlink by default)
└── (any other base files) → ../base/... (symlink by default)
Not applicable — hats is a CLI tool, not an API. The "API" is the command-line interface documented above.
Always Isolated (never symlinked to base):
.credentials.json— OAuth tokens unique per account.claude.json— runtime state, cached identity, unique per account
Always Shared (always symlinked to base by default, can be unlinked):
- Everything else in the directory
Cannot Be Linked/Unlinked (always isolated, hats link refuses):
.credentials.json.claude.json
-
⏳ 1.1 Define constants and directory structure
HATS_DIR="${HATS_DIR:-$HOME/.hats}"HATS_CLAUDE_DIR="$HATS_DIR/claude"HATS_BASE_DIR="$HATS_CLAUDE_DIR/base"HATS_CONFIG="$HATS_DIR/config.toml"ALWAYS_ISOLATED=(".credentials.json" ".claude.json")- Status: Not started
- Notes: Preserve
{ }guard from current script (line 8-11)
-
⏳ 1.2 Implement config.toml parser
_config_get <section> <key>— grep-based TOML value extraction_config_set <section> <key> <value>— sed-based TOML value update_default_account— read from config.toml- Status: Not started
- Notes: Keep it simple — no nested tables, no arrays, just
key = "value"under[section]
-
⏳ 1.3 Implement account enumeration
_accounts— list directories in$HATS_CLAUDE_DIR/excludingbase/_account_dir <name>— return path to account directory_account_exists <name>— check if account directory exists_validate_name <name>— check name format (alphanumeric, hyphens, underscores, dots)- Status: Not started
- Notes:
-
⏳ 1.4 Preserve
_token_info()function- Copy from current
hats:43-66with minimal changes - Update file path references for new directory structure
- Status: Not started
- Notes: This function is stable and well-tested by usage
- Copy from current
-
⏳ 1.5 Implement
_show_account_status()- Adapt from current
hats:133-179 - Remove vault marker (vault system removed)
- Add directory path in verbose mode
- Status: Not started
- Notes:
- Adapt from current
-
⏳ 2.1 Implement
_link_resource <account> <resource>- Refuse if resource is in ALWAYS_ISOLATED list
- Remove local copy, create relative symlink to
../base/<resource> - Handle both files and directories
- Status: Not started
- Notes: Use
ln -sfnfor atomic symlink creation
-
⏳ 2.2 Implement
_unlink_resource <account> <resource>- Refuse if resource is in ALWAYS_ISOLATED list
- Verify resource is currently a symlink to base
- Resolve symlink, copy target content to account dir
- Remove symlink, place copy
- Handle both files and directories (
cp -afor dirs) - Status: Not started
- Notes:
-
⏳ 2.3 Implement
_is_linked <account> <resource>- Check if resource in account dir is a symlink pointing to
../base/<resource> - Return 0 (true) or 1 (false)
- Status: Not started
- Notes:
- Check if resource in account dir is a symlink pointing to
-
⏳ 2.4 Implement
_setup_account_dir <name>- Create account directory
- Enumerate all files/dirs in base/ excluding ALWAYS_ISOLATED
- Create relative symlinks for each
- Create empty
.claude.json - Status: Not started
- Notes: This is used by both
hats addandhats initmigration
-
⏳ 3.1 Implement
cmd_swap- ~5 lines: validate account, set CLAUDE_CONFIG_DIR, exec claude
- No locking, no save-back, no profile management
- Status: Not started
- Notes: This is the simplest it can possibly be
-
⏳ 3.2 Implement
cmd_add- Create account dir via
_setup_account_dir - Run
CLAUDE_CONFIG_DIR=<account_dir> claude auth login - Verify credentials created
- Rollback on failure
- Status: Not started
- Notes:
- Create account dir via
-
⏳ 3.3 Implement
cmd_remove- Validate, warn if default, remove directory
- Status: Not started
- Notes: Consider
--forceflag to skip confirmation
-
⏳ 3.4 Implement
cmd_list- Iterate account directories, show status
- Status: Not started
- Notes:
-
⏳ 3.5 Implement
cmd_default- Get: read from config.toml
- Set: update config.toml, update
~/.claudesymlink - Status: Not started
- Notes:
-
⏳ 3.6 Implement
cmd_linkandcmd_unlink- Thin wrappers around
_link_resourceand_unlink_resource - Status: Not started
- Notes:
- Thin wrappers around
-
⏳ 3.7 Implement
cmd_status- Enumerate resources, show linked vs isolated
- Show symlink chain for external symlinks (e.g., CLAUDE.md → base → external path)
- Status: Not started
- Notes:
-
⏳ 4.1 Implement
cmd_init— fresh setup path- No existing
~/.hats/— create structure from scratch - If
~/.claude/exists, move contents to base - Status: Not started
- Notes:
- No existing
-
⏳ 4.2 Implement
cmd_init— v0.2.x migration path- Detect existing
.credentials.<name>.jsonfiles - Create account directories with credentials
- Move remaining files to base
- Handle external symlinks (agents, skills, CLAUDE.md)
- Create
~/.claude→ default account symlink - Migrate
~/.config/hats/defaultto config.toml - Status: Not started
- Notes: This is the most complex command — needs thorough testing
- Detect existing
-
⏳ 4.3 Implement migration safety
- Atomic directory rename (migrating → final)
- Verification step before removing old
~/.claude/ - Rollback on failure
- Handle
~/.claudealready being a symlink - Status: Not started
- Notes:
-
⏳ 5.1 Implement
cmd_shell_init- Generate
CLAUDE_CONFIG_DIR-based functions - Support
--skip-permissionsflag - Status: Not started
- Notes:
- Generate
-
⏳ 5.2 Implement
cmd_fix- Verify symlink integrity per account
- Repair broken symlinks
- Verify
~/.claudesymlink - Check credential file existence and permissions
- Status: Not started
- Notes:
-
⏳ 5.3 Implement
cmd_versionand help text- Update help text for new command set
- Remove references to stash/unstash/backup/restore/flock
- Status: Not started
- Notes:
-
⏳ 5.4 Update
install.sh- Same atomic install pattern
- No changes needed unless install path changes
- Status: Not started
- Notes:
-
⏳ 5.5 Update
README.md- New architecture description
- New command reference
- Remove flock dependency
- Add macOS compatibility note (no flock needed)
- Status: Not started
- Notes:
-
⏳ 6.1 Update test plan for v1.0 architecture
- Remove all flock-related tests
- Remove credential-swapping tests
- Add symlink management tests
- Add migration tests
- Add resource link/unlink tests
- Status: Not started
- Notes:
-
⏳ 6.2 Implement BATS test suite
- Follow structure from
/home/tanwa/hats/TEST_PLAN.md - Adapt for new architecture
- Status: Not started
- Notes:
- Follow structure from
- Completed Tasks: 0/20 (0%)
- In Progress: 0
- Blocked: 0
- Last Updated: 2026-03-06 by PRD-engineer
Based on the existing test plan at /home/tanwa/hats/TEST_PLAN.md:
- Framework: BATS-core (vendored as git submodules)
- Isolation:
HATS_DIRenv var per test (temp directory) - Mock Strategy: Mock
claudebinary for auth login simulation
_link_resourcecreates correct relative symlink_link_resourcerefuses ALWAYS_ISOLATED resources_unlink_resourcecopies content and removes symlink_unlink_resourcehandles directories correctly_is_linkeddetects symlinks to base vs local files_setup_account_dircreates all expected symlinks
_config_getreads values from TOML_config_setupdates values in TOML_default_accountfalls back correctly
hats addcreates directory, runs auth login, sets up symlinkshats removedeletes directory, warns on defaulthats swapsets CLAUDE_CONFIG_DIR correctlyhats defaultupdates config and symlinkhats link/hats unlinktoggle resource sharinghats statusshows correct linked/isolated statehats listshows all accounts with statushats shell-initgenerates correct functions
- v0.2.x directory structure migrated correctly
- External symlinks preserved
- Per-account credentials isolated
- Config migrated from
~/.config/hats/ - Idempotent — running init twice is safe
- Partial migration recovery
- Concurrent sessions don't interfere (each has own dir)
- Token refresh writes to correct account's credentials
- Default account's credentials unchanged when running non-default
- No file locking needed
- Target: All commands tested, all error paths tested
- Critical paths: migration, swap, add, link/unlink
- All success criteria must be tested
- All user stories must have test coverage
- Migration must be tested with real-world directory structures
- Concurrent session safety must be verified
| Risk | Evidence | Probability | Impact | Mitigation |
|---|---|---|---|---|
| Migration corrupts user's ~/.claude | Complex file operations during init | Medium | High | Atomic rename, backup before migration, rollback on failure |
| Claude Code doesn't respect CLAUDE_CONFIG_DIR for all files | Assumption based on env var name | Low | High | Test with actual Claude Code before release; verify .claude.json, credentials, and all config files use CLAUDE_CONFIG_DIR |
| External symlinks break during migration | User has CLAUDE.md, agents, skills symlinked externally (hats:ls -la ~/.claude shows this) |
Medium | Medium | Detect symlinks in base, preserve them as-is, test with real symlink chains |
| config.toml parsing edge cases | Grep/sed-based parsing is fragile | Low | Low | Keep TOML format minimal, add validation, consider python3 fallback |
| Shared directories cause issues (session-env, debug) | Multiple Claude instances writing to same dirs | Medium | Medium | These are already shared in v0.2.x; monitor for issues, can unlink per-account if needed |
| Users have customized ~/.config/hats/ | v0.2.x stores config there | Low | Low | Migrate during init, preserve old config dir |
Critical assumption: Claude Code uses CLAUDE_CONFIG_DIR for ALL file access including:
.credentials.json(OAuth tokens).claude.json(state, cached identity)settings.json(user settings)- All other config files
This MUST be verified before implementation begins. If Claude Code hardcodes ~/.claude/ for any file, the entire architecture needs adjustment.
Verification steps:
- Set
CLAUDE_CONFIG_DIR=/tmp/test-claude - Copy credentials to
/tmp/test-claude/.credentials.json - Run
claudeand verify it reads from the custom dir - Verify
.claude.jsonis created in the custom dir (not~/.claude.json) - Verify token refresh writes to the custom dir's credentials
| Debt Item | Location | Impact |
|---|---|---|
| flock-based locking | hats:126-131, 389-393 |
Eliminated — no locking needed |
| Credential save-back | hats:407-409 |
Eliminated — Claude writes directly to account dir |
| Profile save/restore | hats:68-119 |
Eliminated — each account has own .claude.json |
| Contamination detection | hats:541-598 |
Eliminated — contamination impossible with isolated dirs |
| Stash/unstash | hats:670-692 |
Eliminated — hats add runs auth login in isolated dir |
| Active credentials file | hats:14 |
Eliminated — no shared credentials file |
- Internal: None — single-file script
- External:
python3(JSON parsing),claudeCLI (auth login, running sessions)
flock/util-linux— no longer needed~/.config/hats/directory — migrated to~/.hats/config.toml
CLAUDE_CONFIG_DIRenvironment variable must be respected by Claude Code for ALL file operations (credentials, state, settings)- Filesystem must support symlinks (all Unix systems do)
- CRITICAL: Verify
CLAUDE_CONFIG_DIRbehavior with actual Claude Code before implementing migration. If Claude Code doesn't respect this env var for.claude.json(state file), the architecture needs adjustment.
- Verify CLAUDE_CONFIG_DIR — test manually before writing any code
- Phase 1-2 — build infrastructure and symlink engine
- Phase 3 — core commands (swap first, since it's the simplest and most important)
- Phase 4 — migration (most complex, needs thorough testing)
- Phase 5 — shell integration and utilities
- Phase 6 — testing
{ }script guard for atomic loading (hats:8-11)set -euo pipefail(hats:6)_token_info()python3 inline script (hats:43-66)- Atomic install via temp file + mv (
install.sh:11-13) chmod 600on credential files throughout
- Never read/write to
~/.claude/.credentials.jsondirectly — always use account directory - Never copy credentials between files — Claude writes directly to the right place
- No
flockusage — concurrency is handled by directory isolation - No python3 for anything other than JSON parsing — keep bash-native where possible
- Provider scoping (
~/.hats/cursor/,~/.hats/windsurf/) is built into the directory structure - Encrypted credentials could encrypt
.credentials.jsonat rest and decrypt into a tmpfs mount hats backup/hats restoreas convenience wrappers around directory copy
-
CLAUDE_CONFIG_DIR and .claude.json: Does Claude Code read/write
.claude.jsonfrom$CLAUDE_CONFIG_DIR/.claude.jsonor always from~/.claude.json? This is critical for state isolation.- Context: v0.2.x handles this at
hats:16— the state file is~/.claude.json(outside CLAUDE_DIR) - Options: If
.claude.jsondoesn't respect CLAUDE_CONFIG_DIR, we may need to also setHOMEor use a wrapper - This must be verified before implementation
- Context: v0.2.x handles this at
-
Shared mutable directories:
session-env/,debug/,shell-snapshots/,file-history/are large and mutable. Should they be symlinked to base (shared) or isolated per account?- Context: Currently shared in v0.2.x (single ~/.claude dir)
- Recommendation: Start shared (symlinked), allow users to unlink if needed
- Risk: Concurrent writes to shared dirs could cause issues (but haven't in v0.2.x)
-
history.jsonl: This large file (1.3MB) contains conversation history. Should it be shared or per-account?
- Options: Shared (current behavior), per-account (cleaner separation), or excluded from base
- Recommendation: Per-account — history should reflect that account's sessions
-
config.toml vs simpler format: Is TOML overkill for the minimal config needs? A simple
key=valuefile might be simpler to parse.- Context: Only need
defaultandprovider.pathcurrently - Recommendation: Use TOML for future-proofing (provider scoping), but keep parser minimal
- Context: Only need
-
hats addauth flow: Shouldhats addrunclaude auth loginor justclaude(letting the user run/logininteractively)?- Context:
claude auth loginis the direct auth command - Recommendation: Use
claude auth login— it's purpose-built for this
- Context:
| v0.2.x Location | v1.0 Location | Notes |
|---|---|---|
~/.claude/.credentials.json |
~/.hats/claude/<default>/.credentials.json (via symlink) |
Shared file eliminated |
~/.claude/.credentials.<name>.json |
~/.hats/claude/<name>/.credentials.json |
Per-account, isolated |
~/.claude/.profile.<name>.json |
(eliminated) | State in .claude.json per account |
~/.claude/.credentials.lock |
(eliminated) | No locking needed |
~/.claude/settings.json |
~/.hats/claude/base/settings.json |
Shared via symlinks |
~/.claude/CLAUDE.md |
~/.hats/claude/base/CLAUDE.md |
Shared via symlinks |
~/.config/hats/default |
~/.hats/config.toml |
Consolidated config |
~/.config/hats/vault/ |
(eliminated or ~/.hats/vault/) |
Vault system simplified |
~/.claude.json |
~/.hats/claude/<account>/.claude.json |
Per-account state (verify CLAUDE_CONFIG_DIR) |
~/.claude/ exists?
├── No → Create ~/.hats/claude/base/ (empty), prompt hats add
└── Yes → Has .credentials.*.json files?
├── No → Move all to base/, prompt hats add
└── Yes → For each .credentials.<name>.json:
1. Create ~/.hats/claude/<name>/
2. Move credentials to <name>/.credentials.json
3. Symlink everything else to base
Move remaining files to base/
Set default from ~/.config/hats/default or first account
Replace ~/.claude/ with symlink → ~/.hats/claude/<default>/
| v0.2.x Command | v1.0 Command | Changes |
|---|---|---|
hats init |
hats init |
Complete rewrite — migration engine |
hats add <name> |
hats add <name> |
Creates isolated dir + auth login (was: copy credentials) |
hats remove <name> |
hats remove <name> |
Removes directory (was: remove credential file) |
hats list |
hats list |
Same output, reads from account dirs |
hats default [name] |
hats default [name] |
Also updates ~/.claude symlink |
hats swap <name> |
hats swap <name> |
Sets CLAUDE_CONFIG_DIR (was: copy + flock + save-back) |
hats shell-init |
hats shell-init |
Uses CLAUDE_CONFIG_DIR (was: hats swap wrapper) |
hats fix |
hats fix |
Repairs symlinks (was: repair credentials + locks) |
hats backup |
(removed) | Directory-level backup is trivial |
hats restore |
(removed) | Directory-level restore is trivial |
hats stash |
(removed) | Not needed with isolated dirs |
hats unstash |
(removed) | Not needed with isolated dirs |
| (new) | hats link <account> <resource> |
Share resource with base |
| (new) | hats unlink <account> <resource> |
Isolate resource from base |
| (new) | hats status [account] |
Show resource sharing status |