I run Claude Code, Codex and pi in two contexts. A context has its own home directory per tool: its own skills, MCP servers, plugins, rules, trust decisions, login and conversation history. One shared profile would load tools a session has no use for, trust projects it should never see and pile every history together.
Identity stays local to each home. Policy is built elsewhere and copied in. Most of what follows comes from keeping those two apart.
What a context is
A context follows account and ownership lines: which logins a session uses, which repositories it works in and where its history belongs. Each of my two contexts has three homes, one per tool: a Claude home, a Codex home and a pi home. One context also had a second Claude home, ~/.claude-openai, which ran Claude Code on GPT models through a LiteLLM gateway. I retired it on 21 October 2026; its archived conversations keep their own namespace.
My everyday Claude home has agent-history, Firecrawl, Home Assistant and n8n as MCP servers, and the Codex home in the same context adds the Computer Use and Node REPL servers. The second context has its own three homes with their own logins, plugins and trust, and nothing from one context is ever restored into the other.
Native and named homes
A Claude or Codex home is either native, at the plain ~/.claude or ~/.codex the desktop apps use, or named, at a CLI-only ~/.claude-<context> or ~/.codex-<context>. pi homes are ~/.loop-pi-<context>. One small home map in the source repository says which context each home carries, and it fails closed: when its answer is partial or contradictory, the maintenance tools refuse to pick a destination.
Logins stay where they were made
Each home has its own local login. Authentication, Keychain contents, trust approvals and SQLite databases never get copied between homes or Macs.
Claude’s OAuth refresh tokens make that absolute. They rotate on use, so seeding one session from another’s saved credentials can invalidate both. A named Claude home keeps its credentials in an isolated file of its own, which the launcher restores before a session and snapshots when it ends, so it only ever restores what it saved.
Two things do move: conversations go to the archive under their own tool and context, and a classified set of preferences travels between the homes of one context through that context’s private repository. Neither crosses between contexts.
The launchers
Plain claude and codex start the native homes through launchers that don’t trust the calling shell. The claude launcher clears every inherited Codex, OpenAI, Anthropic, Claude, telemetry and XDG variable, sets CLAUDE_CONFIG_DIR to ~/.claude and AGENT_CONTEXT to the home’s context, labels telemetry with the context and machine, and runs the vendor binary under an admission wrapper that coordinates settings changes with other sessions on the same home.
Setting CLAUDE_CONFIG_DIR looks redundant and isn’t. Unset, Claude Code keeps its account and MCP metadata in ~/.claude.json in the home directory rather than in the config directory, so removing it as a tidy-up would move that metadata out from under the native login.
Named launchers are generated executables in ~/.local/bin, never aliases or symlinks. Each clears inherited credentials, home selections and telemetry settings, selects its own home and restores its credential copy.
The pi home
Loops run on the pi coding agent by default for OpenAI models, and on Claude Code or Codex when I name one. Why I don’t use Codex for long-running agent loops has the reasons for the default.
A pi home is built from two parts. loop-pi is a public package, and rob/agents pins it by full commit SHA. bin/install-loop-pi fetches that commit, assembles a private overlay (the model routes, a settings overlay, my policy header, the Codex policy set and its references, a lane policy profile, and the shared guard and transcript scripts) and passes it to loop-pi’s own installer with --overlay. The public package never sees anything private, and the private repository holds no pi code.
The installer stages a build under ~/.local/share/loop-pi/, labelled with the pi version and a hash of its inputs. It validates the build before moving it into place, and earlier builds stay for rollback. It writes only the home files it manages, never auth.json or sessions.
The package carries eight extensions. Between them they fence dangerous commands in the root and every lane, hold waits open on timers and process watchers instead of polling, re-prompt a root that stops early and resume it after a provider error, bound every model request by wall-clock time, record per-call timing, show the root’s live status line, and keep the loop’s state log and lane worktrees. The machinery around the loop covers how they work.
The same installer writes the loop-pi launcher. It selects the home, turns pi’s telemetry off and, inside a git repository, runs a preflight and takes an audit snapshot of remote state before a loop starts, refusing to start if either fails. loop-pi --plain skips both for an ordinary session. A fast variant home, the same build with every model at the priority service tier, is chosen by name at launch.
pi homes sit outside the rest of the publication machinery: install-loop-pi owns them, the Claude and Codex publishers ignore them, the hourly reconciler doesn’t cover them and they aren’t git repositories. The updater I run rebuilds them from the pinned commit, and install-loop-pi --check reports drift without writing anything. Inference goes through the model proxy using a key file the home’s models.json reads, and only the transcripts leave, to the conversation archive under their own pi-<context> namespace.
Routing through the model proxy
My everyday Codex home routes inference through my model proxy as a custom provider, and configure-codex-ui writes a retry budget of 7 request retries and 13 stream retries into that provider’s table. Backoff starts at 200 ms, doubles with no cap and nests the HTTP retries inside each stream retry, so a persistent 5xx can hold a session for about an hour.
The proxy also sees every frame of every session, which makes it the most useful telemetry in the setup and the most awkward to store. The archive article deals with that.
Config is build output
Separate homes per context and per tool multiply every rule, hook and reference. I don’t maintain those copies by hand. rob/agents is a private repository checked out on each Mac, holding the policy sources, reference documents, hook templates, home manifests, the loop-pi overlay and the publishers and installers that turn them into installed copies. It pins two public packages by full SHA, loop-pi and agent-history: public code lives upstream, and the private repository keeps only what names my own hosts, homes and policy. No agent home reads it at runtime, and no runtime code imports from it or from another home. My own skills ship separately as plugins from a private marketplace.
A manifest describes one named home: its backup repository, rules, on-demand references, MCP servers, plugins, observability target and the settings the renderer owns.
Each publisher owns a narrow slice:
render-profilesbuilds the named homes and writes a receipt with the source commit, a batch id and a SHA-256 for every generated file.publish-context-baselinesupdates policy and references in the native homes as independent real files. It can’t touch configuration, authentication, trust, hooks or runtime state.publish-automodeowns Claude’sautoModeclassifier policy, the matching permission keys and a few environment settings, among them the shell the Bash tool runs and the security-guidance review settings. It’s the only publisher that writes Claude settings, and its source is keyed by context, never by home path.- Smaller publishers and installers own the shared guards in native homes, the context marker in Codex shells, Firecrawl wiring, the launchers, the native backups, the notifier, the plugin marketplace, the preference exchange between one context’s homes and keeping the gopls MCP server out of Codex homes.
With receipts, a home either matches the files rendered from the recorded commit or it has drifted.
The classifier policy has two traps around $defaults. It has to be the first item in allow, soft_deny and hard_deny, because leaving it out replaces that band’s built-in rules. It has to be absent from environment, where values add up per slot and it leaves a stray None configured entry. The source writes all 21 environment slots explicitly. The permissions deny list is empty by a recorded decision, and the publisher leaves it that way.
The loop protocol is the one thing that isn’t copied. Each repository gets a short pointer marked as not authoritative. A loop’s root reads the canonical contract and its harness file from the source, plus the run’s charter and the repository’s LOOP.md, and a lane reads only its brief and the run’s LANE.md. A curated public export of those files is what the loop contract reference renders.
Drift heals itself, loudly
agents-reconcile is a launchd job that runs hourly on each Mac and treats origin/main of the shared rob/agents checkout as the desired state. It copies that ref into a private clone, checks it out detached and runs the publishers from there, each stamping the commit it published. A few rules keep it from doing harm:
- It never contacts the remote. Homebrew Python run from launchd has no macOS Local Network permission, so the session start hooks do the fetch.
- It never fetches into, checks out or publishes from the shared checkout. It only reports whether that checkout is behind, ahead or dirty.
- If a home is stamped ahead of the desired commit, the local ref is stale, and the run applies nothing rather than revert newer policy.
- Publishers replace files atomically, so they can run while sessions are live. A running session keeps what it loaded.
Drift that survives a run means a check or publication failed, and it raises one alert through the watchdog that looks after unattended loops. A chat backup repository left unsynced for over an hour raises its own.
At session start the backup hook in every home sends a systemMessage that Claude and Codex both show, naming how far behind the home is or that the reconciler has never run, with the command that fixes it. A shared checkout that’s behind or has unpushed commits gets a warning too, since a publisher run by hand from it would publish stale or uncommitted policy. After six hours without an established policy state the message says drift isn’t being checked - six hours because a laptop offline for an afternoon isn’t news.
Publication is ordered, not atomic
A policy change runs in a fixed order: validate the source, commit and push it, publish from the committed revision with the tool that owns each destination, let the home’s backup commit the result, reassert permissions, then verify the layer that changed with its own evidence. Stopping halfway leaves a partial publication, and calling it atomic would hide exactly the state I’d need to repair.
The permission step is there because rsync -a from a temporary tree with a trailing slash copies the temporary directory’s 0755 mode onto a home root that should stay 0700. The publisher copies contents without the source directory’s permissions and reasserts the destination mode afterwards.
A direct edit in a generated home doesn’t survive the next render, so a fix starts in the source repository or it isn’t a fix.
Rules, references and memory
Claude Code’s memory documentation distinguishes rules loaded unconditionally at launch from path-scoped rules loaded when a matching file is touched. I keep unconditional rules short and imperative because they occupy the session’s context. A standing constraint belongs there; the story of how it was found doesn’t. Task-specific material lives in reference/, outside rules/, and each rule names the exact condition that calls for it. A few references load only when a session touches matching paths, such as a workflow file or a justfile. A runbook for one kind of task becomes a skill, so only its one-line description is always loaded. Memory has the same shape: an always-loaded index pointing at files read only when relevant.
Hooks own the boundaries
Each home carries real copies of its hooks. backlog-guard blocks tracker flags that would replace existing notes instead of appending. staging-guard blocks blanket staging and bare commits that can sweep another session’s staged files into unrelated work. In Codex, which runs every command under zsh, the shared guard also refuses the common zsh traps in bash-style commands. On Claude, path-guard answers a read or edit of a missing path with a listing of the parent directory, so the agent corrects the path instead of guessing another. pi homes run backlog-guard and staging-guard through loop-pi’s command fence.
Backing up each home
Every Claude and Codex home is a git repository with its own private remote on a self-hosted Forgejo reachable only on my own network. Its .gitignore excludes everything, then re-includes what the backup owns: settings, the home’s credential copy, generated rules and references, hooks, native skills and the publication receipt. Codex homes leave out their SQLite databases, which get rebuilt from the session logs. The credentials go in on purpose: the remote is private, single-tenant and on hardware I own, and it isn’t a pattern I’d move to GitHub or anyone else’s host. A home’s repository only holds that home, and nothing in it is restored into another context. pi homes have no backup repository, since the installer rebuilds them.
A native home uses a push-only hook bound to one machine, one tool and one remote, so it never merges remote state into a live home. It refuses pre-staged changes, pending git operations, nested repositories and symlinked or hard-linked paths, commits an explicit allowlist, reads the remote head back after pushing, and stops and records why if the remote has moved on its own.
A named home uses a synchronising hook that finds its home from BASH_SOURCE, locks with an atomic mkdir, refuses incomplete git operations, records conflicts in a marker the next session surfaces and reasserts 0700 on the home and 0600 on sensitive files. History in every backup repository is append-only.
Portable preferences go separately. A launch agent runs every five minutes and exports only fields classified as safe to move between one context’s homes; credentials, trust approvals, endpoints, MCP headers and hook registrations never qualify. A conflict keeps both candidates and the last validated baseline. Claude writes wait for every session on the target home to release its admission lock, and Codex writes go through its versioned config API, which rejects a stale write.
Headless git clears any inherited credential helper before picking a file-backed store:
[credential "https://forgejo.example"]
helper =
helper = store --file ~/.config/git/<host>-credentials
Without the empty helper =, the macOS Keychain helper can swallow a non-interactive request when no GUI session is there to answer it, and a backup that only authenticates while someone’s logged in isn’t unattended.
Dotfiles are a bare repository at ~/.dotfiles.git over $HOME, committed every ten minutes by a launchd job that stages with git add -u, so it can’t sweep in new files.
Transcripts stay out of all of these, and the ignore rules exclude session JSONL explicitly even inside re-included directories. Append-only files written from more than one Mac conflict constantly in git, and merging two versions of a conversation is the wrong operation anyway, so they go to the hot and cold conversation archive.
Reassert what you care about
Config you care about has to be reasserted on a schedule, because setting it once only tells you what was true then. The ChatGPT desktop app will rewrite its own config to wrap the Codex notify command in its Computer Use helper. In my native Codex home that wrapped line is the config I want, so nothing there treats it as drift. A reassertion has to be retired when the intended state changes, or it ends up fighting a decision made on purpose. Permissions are the same: a runtime will happily loosen a mode the moment it touches a file, so named homes get theirs reasserted after every session and the native backup runs under a 077 umask.
The review before main
I commit finished work on my own repositories straight to main with no pull request, so CodeRabbit runs from the CLI before a commit whenever a change contains code, scripts, workflows or infrastructure with branching behaviour. The plugin and its rule are installed in every Claude and Codex home.
A branch with no upstream needs an explicit base, and severity names are lowercase. A run with no completion event is a failed review, and a diff of roughly 100 files or more gets split rather than retried.
Docs, comments, ordinary declarative config and dependency bumps skip the review, since there’s no executable behaviour to check. Declarative files that control access, redirects or permissions still get a human read, because simple syntax doesn’t mean a small blast radius. Backup repositories never go to CodeRabbit, because they hold private state. Claude’s security-guidance plugin sits alongside it with its own commit review off and its review models pinned.
What check-profiles answers
check-profiles is the final local check for the Claude and Codex homes, covering home ownership, inventories, modes, backup remotes, the classifier policy and context isolation. install-loop-pi --check does the same job for pi homes. The publication stamp is what makes config build output: I can name the source commit behind an installed rule instead of assuming the latest copy reached every home.