If you’ve ever tried to manage two Anthropic accounts on one laptop by editing ANTHROPIC_API_KEY between terminal tabs, you’ve already met the problem Ringlet solves. The fix sounds easy until you actually try it.

This article walks through what’s inside a Ringlet profile, why isolating with environment variables alone breaks down, and what the agent itself sees when it launches.

What’s inside a profile

Every profile lives at ~/.ringlet/profiles/<name>/. The directory looks like this:

~/.ringlet/profiles/work/
├── HOME/                       ← this becomes the agent's $HOME
│   ├── .claude/                ← Claude Code's directory, this profile's copy
│   │   ├── conversations/
│   │   ├── settings.json
│   │   ├── memory/             ← project-level memory (CLAUDE.md cache, etc.)
│   │   └── usage.jsonl
│   └── .config/                ← if the agent uses XDG paths
├── ringlet.toml                ← profile manifest
├── usage.db                    ← Ringlet's own token + cost ledger
└── hooks/                      ← profile-specific hooks

The crucial bit is the HOME/ subdirectory. When you run ringlet profiles run work, Ringlet exports HOME=~/.ringlet/profiles/work/HOME before exec’ing claude. From the agent’s point of view, that is its home directory. It writes its .claude/conversations/ there. It reads its .claude/settings.json from there. It has no idea any other profile exists.

The ringlet.toml manifest defines the binding:

agent = "claude"
provider = "anthropic"

[provider-config]
endpoint = "https://api.anthropic.com"
key-alias = "work"          # keychain entry name
model = "claude-sonnet-4-5" # optional default

[history]
import-from = "~/.claude/usage.jsonl"  # one-time, on profile creation

Why environment variables alone don’t work

The obvious first attempt at isolation is a wrapper:

function claude-work() {
  ANTHROPIC_API_KEY=$(security find-generic-password -s 'anthropic-work') \
    claude "$@"
}

This sort of works. The API key is right. The token charges land on the right account. But agents cache much more than tokens, and none of it lives in environment variables. Specifically:

  • ~/.claude/conversations/ — every conversation Claude Code has ever had on this machine. Switching ANTHROPIC_API_KEY doesn’t switch which conversations Claude reads.
  • ~/.claude/settings.json — per-machine settings, including MCP server registrations. Pointing one profile at a Postgres MCP server pointed at staging means the other profile gets it too.
  • ~/.claude/memory/ — project-level memory. If the agent learned something specific to project A, it’ll surface that in project B.
  • ~/.claude/usage.jsonl — local usage log. Mixing two accounts here makes per-account cost accounting impossible.
  • Per-project trust prompts. When Claude asks “can I run this rm -rf?” and you say yes, the answer is cached. Across two profiles with the same HOME, that cache is shared.

Every one of these is a state leak that an env-var wrapper can’t fix, because the agent is reading from its own HOME path.

The only durable fix is to give each profile a separate HOME. That’s exactly what Ringlet does — and it’s why “profile” is the right unit, not “session” or “command.”

What Ringlet actually does at launch

When you run ringlet profiles run work, the call stack is:

  1. Read manifest. ~/.ringlet/profiles/work/ringlet.toml.
  2. Look up credentials. Pull anthropic/work from the system keychain.
  3. Build env block. Construct the environment the agent will see:
    HOME=/Users/you/.ringlet/profiles/work/HOME
    ANTHROPIC_API_KEY=<from keychain>
    ANTHROPIC_BASE_URL=https://api.anthropic.com
    PATH=/usr/local/bin:/usr/bin:/bin   # unchanged
  4. Attach token-counting filter. Insert a streaming proxy on the agent’s network calls so usage events are written to usage.db in real time.
  5. Exec. execve("/usr/local/bin/claude", ...) with the new environment.

The agent then runs, reads its HOME, finds its conversations, and Ringlet stays out of the way. You can ps aux | grep claude and see exactly what process is running with what HOME — nothing magic.

Two profiles on the same agent, in two terminals

This is the headline use case. Two Claude Code instances on different Anthropic accounts, simultaneously, no conflict.

# Terminal A
ringlet profiles run work
# claude launches with HOME=~/.ringlet/profiles/work/HOME
# ANTHROPIC_API_KEY=<work key>

# Terminal B (at the same time)
ringlet profiles run personal
# claude launches with HOME=~/.ringlet/profiles/personal/HOME
# ANTHROPIC_API_KEY=<personal key>

They don’t share .claude/conversations. They don’t share MCP registrations. They don’t share trust prompts. They don’t share usage logs. Two separate processes, two separate HOMEs, two separate everything. The agents have no idea the other exists.

This isn’t a contrived edge case — it’s the most common Ringlet usage pattern. You’re answering a client-A question in Terminal A and a client-B question in Terminal B and the agents don’t get confused about whose code memory belongs to whose.

What about MCP servers?

MCP server registrations are per-profile, because they live in .claude/settings.json inside the profile’s HOME. This is more useful than it sounds.

Concrete example: you have a Postgres MCP server. In your work profile, it’s pointed at the staging database. In your production-incident profile, it’s pointed at prod. There is zero chance of accidentally running a staging-debug query against prod, because the agents are loading two different MCP configs from two different HOMEs.

# work profile: staging
cat ~/.ringlet/profiles/work/HOME/.claude/settings.json
{
  "mcpServers": {
    "postgres": { "url": "postgres://staging.internal/app" }
  }
}

# production-incident profile: prod
cat ~/.ringlet/profiles/production-incident/HOME/.claude/settings.json
{
  "mcpServers": {
    "postgres": { "url": "postgres://prod.internal/app" }
  }
}

If you’ve ever fat-fingered a staging connection into a prod-shaped agent session, you’ll appreciate the value.

What gets shared across profiles

Not everything is isolated. By design:

  • The binary. All profiles run the same claude binary from $PATH. Upgrading Claude Code upgrades it for every profile.
  • Token storage backend. Keychain entries are stored once, referenced by alias. Two profiles can share a key by sharing an alias if you want.
  • The Ringlet daemon and the hooks defined globally. Per-profile hooks are isolated; daemon-level ones run across all profiles.

If you actually want a totally isolated binary too — e.g. test a Claude Code prerelease in one profile while another stays on stable — install the prerelease to a different path and override binary = "/opt/claude-canary/claude" in the profile’s ringlet.toml.

Importing an existing setup

If you’ve been using Claude Code without Ringlet, you already have a populated ~/.claude/. Ringlet won’t touch it unless you ask. ringlet import claude --to work copies the relevant bits into ~/.ringlet/profiles/work/HOME/.claude/, attributing the usage history to the work profile. Future runs of ringlet profiles run work use the imported state; your original ~/.claude stays as-is.

What this isn’t

Some things this specifically isn’t:

  • Container-level isolation. Profiles share the kernel, the filesystem (outside HOME), and the network stack. If you need stronger isolation, run the daemon inside a container or use the sandboxed remote session feature.
  • Multi-user isolation. A profile boundary is a HOME boundary. The OS user is the same. Two Ringlet users would each have their own ~/.ringlet.
  • Magic. Everything Ringlet does is files in a directory and env vars on exec. No kernel modules, no namespaces, no LD_PRELOAD. You can cat ringlet.toml, you can ls HOME/, you can debug it.

Try it

The two-profile pattern is the entry point. The install page walks through it. If you have two Anthropic accounts, that’s day-one Ringlet — make a work and a personal profile and stop worrying about which key is set.