Run 4 Claude Code sessions in one iTerm2 window. Opus reviews. Sonnet builds. Each pane has a locked role, model, and permission set — the reviewer is physically incapable of writing files. Optional add-ons route work between panes automatically and give every session persistent memory.
Three tools to install before anything else. All three are Homebrew packages — if you already have them, each check-then-install is a single command.
Open iTerm2 once to complete initial setup, then continue in it for the rest of this guide.
Claude Code is Anthropic’s official CLI. Install it globally via npm, then authenticate. Browser OAuth is the recommended path — no API key required.
Pro or Max subscription required for browser OAuth. No API key needed with this method.
Run AI models privately on your Mac — no API calls, no data leaving your machine. Install Ollama, pull the models for your RAM tier, then add the shell aliases.
| Tier | Models to pull | Disk / Time |
|---|---|---|
| 🌺 16 GB+ | qwen3:8b qwen3-coder:7b nomic-embed-text | ~10 GB / 15 min |
| 🔵 32 GB+ | qwen3:14b qwen3-coder:14b nomic-embed-text deepseek-r1:8b | ~23 GB / 40 min |
| 🔴 64 GB+ | qwen3:14b qwen3-coder:32b nomic-embed-text deepseek-r1:8b gemma3:27b | ~55 GB / 90 min |
Example (16 GB tier):
The Ollama block in zshrc-snippet.sh adds these aliases. Append the relevant tier block to your ~/.zshrc (see Step 6 for full snippet instructions).
| Alias | Model | Use for |
|---|---|---|
| llm-fast | qwen3:8b / :14b | General queries, summaries |
| llm-code | qwen3-coder:7b / :14b / :32b | Code generation & review |
| llm-reason | deepseek-r1:8b | 🔵 Structured reasoning (32 GB+) |
| llm-embed | nomic-embed-text | Embeddings / RAG |
| llm-smart | router | Auto-picks by task type |
This workflow relies on features the default Terminal doesn't have:
| Feature | macOS Terminal | iTerm2 |
|---|---|---|
| Split panes | Tabs/windows only — no side-by-side splits | Unlimited independent panes in a single tab |
| Named profiles | Basic profiles, no $ITERM_PROFILE env var | Auto-sets $ITERM_PROFILE per pane — the key to role detection |
| Visual identity | Basic themes and transparency | Per-profile backgrounds, tab colours, badges, and 24-bit colour |
| Window arrangements | No saved layouts | Save & auto-restore multi-pane layouts on launch |
| Productivity | Standard find and copy | Paste history, Instant Replay, triggers, shell integration |
The $ITERM_PROFILE environment variable is especially critical — it's what lets the cc alias automatically launch the right model and permissions per pane, and it survives window arrangement restores.
Create 4 named profiles, each with a distinct background tint, tab colour, and badge so you can instantly tell which pane you're in.
Each pane runs a separate Claude Code session with a dedicated role. The AUDIT pane uses Opus for deep review. Everything else uses Sonnet to keep costs down. A cc alias launches the right configuration automatically.
Open iTerm2 → Settings → Profiles (⌘,). Click the + button four times. Double-click each "New Profile" name to rename it:
| Profile Name | Background | Tab Colour | Role |
|---|---|---|---|
| CC-AUDIT | #0d0b18 | #a855f7 | Code review |
| CC-IMPL | #080f0b | #22c55e | Implementation |
| CC-PROMPT | #080e10 | #06b6d4 | Prompt engineering |
| CC-PLAN | #0d0b00 | #f59e0b | Architecture & planning |
For each profile, click the Colors sub-tab:
Click the General sub-tab for each profile:
Then click the Text sub-tab and set font to JetBrains Mono 13pt (or Menlo 13pt).
Each profile needs a startup command (to cd into the project directory and open an interactive shell) and an initial directory setting (as a fallback for window arrangement restores).
In each profile → General sub-tab → change the Command dropdown from "Login Shell" to "Custom Shell" and paste the corresponding command:
Replace your-project with your actual project directory name.
exec zsh at the end? It replaces the subshell with an interactive zsh session. Without it, the pane would close the moment you exit Claude Code or any other command.Still in the General sub-tab, find Initial directory below the Command field. Change from "Home directory" to "Directory:" and enter the full path:
Add a block to your ~/.zshrc that auto-detects which profile you're in and configures coloured prompts, locked tab titles, and a cc alias to launch Claude Code with the right flags.
Open your shell config file and paste this block at the very bottom:
Then run source ~/.zshrc in each pane to apply.
Create a 2×2 pane layout, save it, and configure iTerm2 to restore it automatically on launch.
Type cc in any pane. The alias you set up in Step 4 launches Claude Code with the correct model, effort level, and permissions for that pane.
| Pane | cc expands to | What you see |
|---|---|---|
| AUDIT | claude --model opus --effort high --permission-mode plan | Opus · plan mode on |
| IMPL | claude --model sonnet --effort high --permission-mode acceptEdits | Sonnet · accept edits on |
| PROMPT | claude --model sonnet --effort medium | Sonnet · medium effort |
| PLAN | claude --model sonnet --effort low | Sonnet · low effort |
| Flag | Purpose |
|---|---|
| --model opus|sonnet|haiku | Model selection (or full IDs like claude-opus-4-6) |
| --effort low|medium|high|max | Thinking budget: low = fast, minimal reasoning; medium = balanced; high = extended reasoning chains, more tokens; max = maximum depth (slowest, most expensive). PLAN uses low — architectural discussion doesn't need deep reasoning chains. |
| --permission-mode plan | Read-only — Claude cannot write files |
| --permission-mode acceptEdits | Auto-accept file edits without prompting |
| --append-system-prompt "..." | Add custom instructions on top of defaults |
| --continue | Resume most recent conversation in this directory |
| --resume | Resume a specific session by ID |
iTerm2 navigation
Claude Code (inside a session)
Auto-highlight keywords in terminal output.
Set up in Profile → Advanced → Triggers → +
| Regex | Action | Colour |
|---|---|---|
| \b(CRITICAL|ERROR|FAIL(ED)?)\b | Highlight Text | Red background |
| \b(PASS(ED)?|SUCCESS)\b | Highlight Text | Green background |
| \b(WARNING|WARN|TODO)\b | Highlight Text | Yellow background |
Every code change follows this pattern across panes:
When AUDIT finds an issue, copy its output and paste into IMPL:
This setup is project-agnostic. To use it with a different codebase:
Use a project-specific prefix: MYPROJECT-AUDIT, MYPROJECT-IMPL, etc. Or keep DEV-* and just change the directory.
Point to the new project path in each profile's General tab.
Add matching entries in both case blocks for the new names:
Name it after the project (Window → Arrangements → Save Window Arrangement). To switch between projects later, use Window → Arrangements → [arrangement name] — iTerm2 opens the saved 4-pane layout for that project in a new window.
Hook scripts enforce safety invariants that prompt instructions alone cannot guarantee — blocking dangerous tool calls before they execute and tripping a circuit-breaker on repeated failures.
Open (or create) ~/.claude/settings.json. Copy the "hooks" block from hooks/settings.json.example into the top level of the JSON object. All "command" values use $HOME — Claude Code does not expand ~.
$HOME instead of ~? Claude Code does not expand ~ in hook command strings. Use $HOME which the shell expands at execution time, or replace with your full absolute path.The example also includes "env": {"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50"}. This tells Claude Code to auto-compact the conversation context when it reaches 50% of the context window. Without this, Claude waits until near the limit before compacting, which can cause abrupt context loss mid-session. Set to "0" to disable, or remove the "env" block entirely to use the default threshold.
| Hook | Tier | Blocks when |
|---|---|---|
| protect-env.py | PreToolUse | Edit/Write/MultiEdit targets any .env file |
| protect-git-push.py | PreToolUse | Bash command matches git … push |
| circuit-breaker.py | PostToolUse | 3 consecutive tool failures |
| session-start-reset.py | SessionStart | (resets counter — never blocks) |
| version-check.py | SessionStart | (never blocks — prints update checklist when Claude Code version changes) |
Two aliases in the IMPL pane create a hard gate between implementation and review.
Both aliases are only defined when $ITERM_PROFILE == CC-IMPL. They are no-ops in other panes.
Never send work to AUDIT until gate passes.
Without a session log, each IMPL session starts cold — Claude asks for a project overview instead of resuming from where work stopped.
Append a new entry at the end of every session. Never overwrite old entries. Minimum viable format:
Real example:
In your ~/.claude/CLAUDE.md (see Step 14), add a Session Continuity instruction: "At the start of every session, read the last 60 lines of SESSION_LOG.md and surface the most recent 'Next:' items before doing anything else."
Separating global rules from project-specific rules reduces noise and prevents cross-project contamination in every Claude session.
| File | Location | Committed? | Contains |
|---|---|---|---|
| CLAUDE.md | ~/.claude/ | No — personal | Coding conventions, error-handling policy, tool preferences |
| CLAUDE.md | .claude/ (repo) | Yes | Project architecture, active constraints, session continuity |
Claude Code merges both automatically — global first, project-specific second.
Phase 6 additions — GitHub MCP integration, the /reflect command, and contextual skills.
Claude Code registers MCP servers via claude mcp add, not by reading a config file from disk. The included .mcp.json.example is reference JSON if you need claude mcp add-json instead.
Run /reflect at the end of any IMPL or AUDIT session. Claude reads the recent git log and diff, then outputs a formatted table of suggested CLAUDE.md additions — ready to copy and paste.
Three skills that load relevant instructions automatically when Claude is doing review or test work:
| Skill | Pane | Adds |
|---|---|---|
| code-review | AUDIT | Project-specific review conventions |
| security-audit | AUDIT | Security checklist and vulnerability patterns |
| testing | IMPL | pytest conventions matching the project |
Skills activate automatically — no manual invocation needed.
The cross-pane workflow (Step 11) has one manual chore left: copying AUDIT’s findings into the IMPL pane, and IMPL’s hand-back into AUDIT. This optional add-on removes it — one pane writes a file, a background watcher delivers it into the other pane as a single paste.
The problem it solves. Every AUDIT → IMPL round-trip means selecting a multi-line directive, switching panes, and pasting it — and multi-line pastes into a terminal normally submit on the first newline, mangling code blocks and blank lines. Do that ten times a day and it’s the slowest, most error-prone link in the loop.
How it works. A tiny file-based message bus. Each pane binds itself to a role (IMPL or AUDIT) and a project scope. To send work, Claude writes a file into ~/.claude/handoff/; an fswatch-based watcher (a launchd daemon, running 24/7) picks it up, wraps the content in bracketed-paste escape codes, and types it into the bound iTerm pane — where the receiving Claude sees it as one fresh user message, every line preserved. On success the watcher truncates the file to 0 bytes as its delivered-marker.
Scoped filenames are the routing table. Every file is keyed by a per-project scope: to-impl.<scope>.txt routes to that scope’s IMPL pane, to-audit.<scope>.txt to its AUDIT pane, and HALT.<scope> pauses that scope only. Two projects run side by side with zero cross-talk. Unscoped writes (to-impl.txt) are refused on purpose.
The installer verifies fswatch, osascript, and iTerm2 are present; copies pane-handoff.sh (the watcher) and enforce-handback.py (a Stop hook) to ~/.claude/hooks/; copies the /start-impl and /start-audit commands to ~/.claude/commands/; installs a launchd agent at ~/Library/LaunchAgents/com.user.handoff.plist (auto-starts at login, restarts on crash); and arms routing.
To uninstall: ./handoff/install.sh --uninstall
This defines the user-facing API, available in every pane:
| Command | What it does |
|---|---|
| handoff-use <scope> | Set this pane’s HANDOFF_SCOPE — once per pane open, before any scoped command |
| handoff-claim-impl / handoff-claim-audit | Bind this pane as the IMPL or AUDIT target for the current scope |
| handoff-on / handoff-off / handoff-status | Arm / disarm / check routing (global flag file ~/.claude/handoff/active) |
| handoff-halt / handoff-resume | Pause / resume the loop for this scope only (writes / removes HALT.<scope>) |
| handoff-targets | List all current pane bindings |
| send-impl / send-audit | Pipe stdin to the scoped inbox (one-liners) |
| handoff-scope | Print this pane’s current scope |
Pick a scope name per project (lowercase, no spaces — e.g. myapp). In the IMPL pane, inside Claude:
In the AUDIT pane, inside Claude: handoff-use myapp, then the /start-audit slash command, then handoff-claim-audit. Confirm both bindings and smoke-test:
AUDIT → IMPL — the AUDIT pane writes a directive (via its Bash tool) and ends it with the confirmation line → handed off:
IMPL → AUDIT — after gate passes, IMPL hands back, ending with → handed back:
The confirmation lines are load-bearing: the enforce-handback.py Stop hook (a script Claude Code runs at the end of each turn — see Step 14) blocks IMPL from claiming “→ handed back” in chat without actually writing the file — the failure mode that otherwise leaves AUDIT waiting forever on a loop that silently broke.
Either pane can pause its own project’s loop by writing the scoped sentinel. Contents must name the trigger and the human action required:
This writes ~/.claude/handoff/HALT.myapp. The watcher logs ⛔ HALT scope=myapp, fires a macOS notification, and both panes in that scope stop before doing anything else. Other projects keep flowing — HALT is per-scope, there is no global pause. Only a human resumes it:
If a paste doesn’t arrive, ask three questions in order — then read /tmp/handoff.log:
| Log pattern | Meaning |
|---|---|
| -> IMPL scope=X then delivered to IMPL/X — file truncated to 0 bytes | Success |
| -> IMPL scope=X then WARN: delivery failed — file NOT truncated | Stale binding (re-claim), no binding for that scope, or macOS Automation permission denied for iTerm/osascript |
| No -> IMPL scope=X line at all | Watcher not running, routing disarmed, or wrote to the wrong path |
| WARN: refusing unscoped to-impl.txt | You forgot the scope in the filename — use to-impl.<scope>.txt |
Edited the watcher script? It does not hot-reload — pkill -f pane-handoff.sh and launchd respawns it within ~1s.
That’s the full daily loop. For multi-project isolation probes, the complete command and path reference, and the under-the-hood details, see the Pane Handoff operator guide.
Every pane in this setup is a separate Claude Code session — and every session starts with amnesia. Close a pane and the decisions, preferences, and project facts you spent an hour establishing are gone. Sigil — an open-source, local-first memory system by Anmol Srivastava — fixes this: one shared, persistent memory that all four panes read from and write to automatically, across sessions and across projects.
The SESSION_LOG pattern (Step 16) carries context forward in time within one project. It does not carry context sideways — AUDIT never learns what you told IMPL, and nothing crosses project boundaries. In practice that means re-explaining “we use pnpm, not npm” or “never run db:reset against staging” in every pane, every session.
Sigil closes that gap with hooks that run inside every Claude Code session, regardless of pane, model, or permission mode:
| Hook | Fires | What it does |
|---|---|---|
| UserPromptSubmit | Every prompt you type | Searches memory and injects the most relevant facts before Claude sees your prompt |
| Stop | End of each turn | Classifies what you said and auto-captures memorable facts (preferences, decisions, corrections) |
| SessionEnd | Session close | Summarizes the session into durable memory |
| Hot context | Session start | A snapshot of your most-used facts, loaded via an @import in ~/.claude/CLAUDE.md |
Because memory lives in a local database rather than in any one session, all four panes share it — and it is MCP-native, so the same memory is available to other MCP clients (Cursor, Codex CLI) if you use them alongside this setup.
Installation is a one-liner, then an interactive wizard. Follow the upstream README at github.com/Anmol-Srv/sigil for current instructions; as of this writing:
The sigil init wizard walks you through four choices: a database (Postgres — local or managed), an LLM provider for fact classification (OpenRouter, OpenAI, Anthropic, Ollama, or your Claude subscription), an embedding provider (OpenAI, Voyage, or Ollama), and hook wiring — it auto-detects Claude Code, installs its hooks into ~/.claude/settings.json, and registers MCP servers for other clients. It merges alongside the hooks from Step 14 without touching them.
Verify the loop end-to-end: open any pane with cc, submit a prompt, and confirm memory search fires without a timeout warning. Then check claude mcp list shows Sigil connected.
The hooks do the routine work unprompted. Manual interaction comes down to three commands, runnable from any pane (or inside Claude Code with ! shell-exec):
| Command | What it does |
|---|---|
| sigil facts | List everything stored, with IDs. Your spot-check that memory contains what you think it does. |
| sigil remember "..." | Save a fact explicitly. Sigil classifies it, extracts atomic facts, and embeds them — write natural language. |
| sigil search "..." | Semantic search over memory. This is the same path the prompt hook uses — run it to debug what Claude is actually seeing. |
The hooks fire identically everywhere, but there’s a natural division of labor:
| Pane | What shared memory changes |
|---|---|
| AUDIT | Recalls past findings and ratified decisions — stops re-flagging issues you already resolved. Best pane for saving durable facts after a review: Opus synthesis is worth keeping. |
| IMPL | Stack choices, conventions, and constraints inject automatically — no more re-explaining the package manager or test layout every session. Add tactical facts as you discover them. |
| PLAN | Prior architectural decisions surface during planning, so new plans build on old ones instead of contradicting them. |
| PROMPT | Natural home for memory housekeeping — reviewing sigil facts, pruning stale entries, bulk ingestion. |
New project — spend two minutes on day 1 telling Sigil what the project is. Every later prompt, in every pane, has it on tap:
Existing project — no migration needed; the hooks fire regardless. Teach it project specifics as you hit them, and optionally bulk-seed from the docs you already maintain:
Sigil extracts atomic facts and de-duplicates against existing memory. Run sigil facts afterward to spot-check what landed. Your SESSION_LOG (Step 16) and CLAUDE.md files (Step 17) keep working exactly as before — Sigil owns the cross-session, cross-project layer; it doesn’t replace either.
Free local image generation on macOS. Runs entirely on-device via Core ML — no API calls, no sign-up. Completely independent from Claude Code and Ollama.
Recommended starting model: SDXL Turbo (fast) or Flux Schnell (higher quality). Models are downloaded in-app — no terminal commands needed.
Common failure modes and how to fix them.
Symptom: echo $ITERM_PROFILE returns nothing. The cc alias falls through with no model flags.
Cause: Old iTerm2 version (< 3.3) or the pane was opened before the profile was applied.
Fix:
Symptom: which cc shows /usr/bin/cc. Typing cc outputs compiler error messages.
Fix: Rename the alias in ~/.zshrc. Replace all 4 alias cc= occurrences with alias cl= (or any name you prefer), then reload:
Check 1 — Hook files exist:
Check 2 — Python 3 is available:
Check 3 — settings.json is valid JSON:
Symptom: Claude is blocked and won't proceed. The circuit-breaker tripped after 3 consecutive tool failures.
Fix A — Start a new session: Press Ctrl+C, then type cc. The session-start-reset.py hook fires automatically on session start and resets the counter.
Fix B — Reset the state file directly:
Symptom: gate exits non-zero immediately or pytest reports collected 0 items.
The protect-git-push.py hook blocks Claude from running git push autonomously. You can always push directly from a regular terminal prompt — hooks only intercept tool calls inside a Claude Code session.
Symptom: After npm update -g @anthropic-ai/claude-code, typing cc errors with an unknown flag message.
Run claude --version to check your installed version. This guide was verified against v2.1.81 (March 2026).
Everything on one screen — copy, paste, go.
| Pane | Model | Effort | Permission | Purpose |
|---|---|---|---|---|
| AUDIT | Opus | high | plan | Code review (read-only) |
| IMPL | Sonnet | high | acceptEdits | Write code + run tests |
| PROMPT | Sonnet | medium | default | Prompt engineering |
| PLAN | Sonnet | low | default | Architecture + planning |
| Alias | Pane | What it does |
|---|---|---|
| cc | All | Launch Claude Code with role-correct flags |
| gate | IMPL | Run full test suite — must pass before AUDIT handoff |
| ship | IMPL | gate + git add -p + git commit |
| Shortcut | Action |
|---|---|
| ⌘⌥ ←/→/↑/↓ | Switch panes |
| ⌘⇧↵ | Zoom/unzoom pane |
| ⌘D | Split right |
| ⌘⇧D | Split down |
| Esc | Stop Claude generation |
| /clear | Reset conversation context |
| /compact | Compress context (keep history) |
| Hook | Event | Blocks |
|---|---|---|
| protect-env.py | PreToolUse | Any edit to .env files |
| protect-git-push.py | PreToolUse | git push commands |
| circuit-breaker.py | PostToolUse | 3 consecutive tool failures |
| session-start-reset.py | SessionStart | (resets counter) |
| version-check.py | SessionStart | (never blocks — prints update checklist on version change) |