Getting Claude Code's Multi-Agent Team Mode to Actually Work

Getting Claude Code's Multi-Agent Team Mode to Actually Work

February 6, 2026·urjit

The yak stack: a programmer at their desk with a tower of yaks, each representing a step in the yak-shave

I wanted to try out Claude Code’s multi-agent team mode - the one where you spin up a swarm of agents that coordinate on tasks. The docs describe a “split pane” display mode where each agent gets its own terminal pane and you can watch them all work simultaneously. That sounded cool. The docs say to pip install it2, a CLI for controlling iTerm2 through its Python API, and use that as the project for the agents to work on. Simple enough.

Except before I could get to the fancy multi-agent stuff, I tried running the tool itself:

❯ it2 window new
AttributeError: 'App' object has no attribute 'async_create_window'

So I forked it to fix the thing I was supposed to be testing with. The yak stack was getting tall.

If you just want the working setup and don’t care about the bug safari, skip to the split panes section .

Bug 1: The Method That Doesn’t Exist

The first error was clear enough. The code was calling app.async_create_window() but the iterm2 Python API (v2.13) doesn’t have that method on the App class. The actual API for creating a window is a static method on the Window class:

# What I had (wrong)
window = await app.async_create_window(profile=profile)

# What iterm2 v2.13 actually provides
window = await iterm2.Window.async_create(connection, profile=profile, command=command)

The static method also accepts command directly, which meant the manual workaround of sending keystrokes to the new window after creation could go away entirely:

# Before: create window, then manually type the command
window = await app.async_create_window(profile=profile)
if command:
    session = window.current_tab.current_session
    await session.async_send_text(command + "\r")

# After: just pass it to the API
window = await iterm2.Window.async_create(connection, profile=profile, command=command)

Cleaner, and actually works. Two improvements for the price of one bug.

Bug 2: Click’s Name Resolution Strikes Again

With windows fixed, I tried the shortcut commands:

❯ it2 send "hi"
TypeError: register_shortcuts.<locals>.send_shortcut() got an unexpected keyword argument 'session'. Did you mean 'session_id'?

This one is a classic Click gotcha. When you declare a Click option like @click.option("--session", "-s"), Click derives the Python kwarg name from the option string. --session becomes session. But the shortcut functions had the parameter named session_id:

@click.option("--session", "-s", help="Target session ID")
def send_shortcut(ctx, text, session_id, all_sessions):  # Click passes 'session', not 'session_id'
    ctx.invoke(session_module.send, text=text, session=session_id, ...)

Click sees --session, strips the dashes, and passes it as session. The function expects session_id. TypeError. This was broken in six different shortcuts: send, run, split, vsplit, clear, and newtab.

Fix was straightforward - rename the parameters to match what Click actually passes:

@click.option("--session", "-s", help="Target session ID")
def send_shortcut(ctx, text, session, all_sessions):
    ctx.invoke(session_module.send, text=text, session=session, ...)

Bug 3: The Same Bug, One More Time

Fixed the session parameter, tried again:

❯ it2 send "hi"
TypeError: register_shortcuts.<locals>.send_shortcut() got an unexpected keyword argument 'all'

Same exact problem, different parameter. --all resolves to all, but the function expected all_sessions. Unlike --session where renaming the param was enough, here I actually needed the Python name to differ from the option name (because all shadows Python’s builtin). The fix: tell Click explicitly what to name it.

# Before: Click derives 'all' from '--all'
@click.option("--all", "-a", is_flag=True, help="Send to all sessions")

# After: explicitly tell Click to use 'all_sessions'
@click.option("--all", "-a", "all_sessions", is_flag=True, help="Send to all sessions")

This is exactly what the target commands in session.py already did. The shortcuts just forgot to copy that part.

The Debugging Loop

What made this session interesting was the feedback loop. Claude would read the error, trace through the code, find the issue, fix it, update the tests, and run them. Each fix revealed the next bug because the first error was masking the others. Classic stack of failures where you can only see one at a time.

The test suite was also running against the installed package rather than the source tree. First round of test runs all failed because the installed copy still had the old broken code. A quick pip install -e . for editable mode fixed that - a good reminder to always develop with editable installs.

Three Commits, One Working CLI

Three bugs, three commits, all found and fixed in one session:

  1. Window.async_create() is a static method, not App.async_create_window() (patched against iterm2 v2.13)
  2. Shortcut parameter names must match Click’s --option name resolution
  3. Options like --all need explicit name arguments when the Python kwarg should differ

OK great. it2 works. Now back to the actual goal: split pane team mode.

Actually Getting Split Panes to Work

The docs describe two display modes for agent teams: “in-process” (everything in one terminal) and “split panes” (each agent gets its own pane). For split panes, they say:

Split-pane mode requires either tmux or iTerm2.

That “or” is doing a lot of heavy lifting. It reads like two independent options: you can use tmux, or you can use iTerm2 with the it2 CLI. I went down the iTerm2 path first - installed it2, enabled the Python API, fixed all the bugs above, set the config:

{
  "teammateMode": "tmux"
}

Spawned a team. Agents ran fine. No split panes. Everything stayed in-process in one terminal.

Turns out there’s a note buried further down the docs page:

tmux has known limitations on certain operating systems and traditionally works best on macOS. Using tmux -CC in iTerm2 is the suggested entrypoint into tmux.

“Suggested entrypoint” is underselling it. This is the actual way to do it. The setup that works is:

# 1. Install tmux (yes, even on macOS with iTerm2)
brew install tmux

# 2. Start tmux in iTerm2's control mode
tmux -CC

# 3. NOW run claude inside the tmux session
claude

iTerm2’s -CC flag is the magic. It runs tmux in “control mode” where iTerm2 takes over the window management. Instead of tmux’s usual terminal-within-a-terminal, you get native iTerm2 tabs and split panes that happen to be backed by tmux sessions. When Claude’s team mode spawns agents, they each get a real iTerm2 pane.

The docs present “tmux or iTerm2” as alternatives. In practice, the working setup is tmux through iTerm2. The it2 CLI might work too at some point, but tmux -CC is what actually got me to split panes.

I went looking to see if others hit the same wall. I found a few blog posts about Claude Code agent teams — Addy Osmani’s , Marco Patzelt’s setup guide , paddo.dev’s writeup — but I didn’t see anyone mention trying the it2 path specifically. Most recommend starting with in-process mode, which is fair. It’s possible everyone just went straight to tmux and never looked back. Either way, the docs could really use a “here’s what you actually need to do on macOS” section instead of presenting two options where only one currently works.

The Full Yak Stack

To summarize the afternoon:

  1. Wanted to test Claude Code’s multi-agent team mode with split panes
  2. Docs said install it2 - it was broken
  3. Forked it2, used Claude to fix three bugs
  4. it2 works now but split panes still didn’t appear
  5. Put teammateMode in the wrong place in settings (inside env instead of top-level)
  6. Fixed settings, still no panes
  7. Installed tmux, ran tmux -CC, ran claude inside that
  8. Split panes finally worked

I came here to test multi-agent coordination. Seven yaks later, I got there.

Header image courtesy of Nanobanana , Gemini’s image generation extension, which nailed the vibe of “programmer surrounded by yaks” on the first try.

Last updated on