Agent team iTerm2 panes not closed on teammate shutdown

Status Open
Reported on v2.1.37
Maintainer reply None cached
Activity 4 comments · opened Feb 9, 2026

Description

When using --teammate-mode tmux with the iTerm2 backend, teammate panes are not closed when agents shut down. The agent process exits, the shutdown is acknowledged (including the paneId), but the iTerm2 split pane remains open with a dead shell.

This also causes a cascading issue: if the orphaned pane is closed manually (or via AppleScript), Claude Code's internal pane tracking becomes stale. Subsequent team spawns fail with Session '<old-pane-id>' not found because it tries to split from the now-deleted pane. The only recovery is restarting Claude Code entirely.

Reproduction

  1. Launch Claude Code with --teammate-mode tmux in iTerm2 (Python API enabled)
  2. Create an agent team and spawn 2 teammates
  3. Panes appear correctly (backend: iterm2)
  4. Shut down both teammates via shutdown_request
  5. Both agents respond with shutdown_approved (including paneId and backendType: iterm2)
  6. Bug: Panes remain open — shell is alive at a prompt, agent process exited
  7. Close panes manually
  8. Try to spawn a new team
  9. Bug: Spawn fails with Failed to create iTerm2 split pane: Error: Session '<stale-pane-id>' not found

Expected behavior

When a teammate shuts down, Claude Code should call async_close() on the iTerm2 session/pane using the Python API. The pane ID is already available in the shutdown response.

Verified that iterm2.Session.async_close() works correctly for this:

import iterm2

async def main(connection):
    app = await iterm2.async_get_app(connection)
    window = app.current_terminal_window
    session = tab.current_session
    new_session = await session.async_split_pane(vertical=True)
    # ... later on shutdown:
    await new_session.async_close()  # <-- this works, pane closes cleanly

iterm2.run_until_complete(main)

Environment

  • Claude Code 2.1.37
  • macOS (Darwin 25.3.0, arm64)
  • iTerm2 with Python API enabled
  • --teammate-mode tmux (auto-detects iTerm2 backend)

Related issues

  • #24261 — Stale pane state causing spawn failures (same cascading consequence)
  • #23615 — Pane spawning layout issues

View original on GitHub ↗

4 Comments

phpmypython · 6 months ago

I think this might be happening because when it runs the it2 command to close the session it doesn't send the -f flag so it requires user approval to close.

Possible root cause: The ITermBackend.killPane() method runs it2 session close -s <paneId> without the -f (force) flag. The it2 session close command prompts for interactive confirmation (Close session <id>? [y/N]:), which defaults to "No" in the non-interactive context, causing a click.exceptions.Abort.

This would affect both the InboxPoller's shutdown_approved handler and the cleanup-on-exit path.

If this is the issue, the fix would be adding -f to the killPane call:

// Current:
async killPane(A, q) {
  return (await sTA(["session", "close", "-s", A])).code === 0
}

// With force flag:
async killPane(A, q) {
  return (await sTA(["session", "close", "-f", "-s", A])).code === 0
}

Possibly related secondary issue: The internal pane tracking array (PW1 in the minified source) only supports push() and full reset — there's no removal of individual entries when a pane closes. After a pane is closed externally, subsequent teammate spawns try to split from a dead pane (session split -s <stale-id>) and fail with Session '<id>' not found. The split logic may need to validate the target pane still exists before splitting, or fall back to the leader session.

Workaround that seems to work : A SessionEnd hook using it2 session close -f -s "$SESSION_UUID" (extracted from $ITERM_SESSION_ID) closes teammate panes on shutdown. However, spawning new teammates in the same session after closure fails due to the stale pane tracking mentioned above.

— Claude

trhinehart-attentive · 6 months ago

Running into this as well. My iTerm2 is set to "Close" after a session ends, but the teammate panes still hang around after shutdown.

The issue is that the Claude process exits, but the shell that launched it is still alive sitting at a prompt — so iTerm2 doesn't consider the session "ended" and the pane stays open.

Two fixes that would work well together:

  1. Add -f to it2 session close (primary fix) — As noted above, the interactive confirmation silently fails in a non-interactive context. Force-closing fixes the active cleanup path.
  1. exec the Claude command (fallback) — If the spawn used exec before the claude command, the Claude process would replace the shell instead of running as a child. When Claude exits, the session truly ends and iTerm2's close-on-exit setting handles cleanup naturally. This would also cover cases where the it2 cleanup fails for other reasons (stale pane IDs, it2 not available, etc.).
JKeeter · 5 months ago

Hitting both issues described here.

Repro for the stale pane tracking (secondary issue):

  1. TeamCreate → spawn agent via Agent with team_name → agent opens in iTerm2 split pane (works fine)
  2. shutdown_request → agent shuts down → TeamDelete succeeds
  3. Close the lingering pane manually (osascript tell aSession to close by unique ID)
  4. TeamCreate a second team → spawn a new agent → fails with Error: Session '<old-pane-id>' not found

The cached pane ID from step 1 persists in the Claude Code session even after TeamDelete. Only fix is restarting Claude Code.

Confirming the proposed fix direction: the split logic needs to either remove individual pane entries on close, or validate the target pane still exists and fall back to the leader session.

ahmadelafify · 5 months ago

I hit this bug during heavy agent team usage and debugged the iTerm2 backend source to understand the full scope. The missing -f flag is the surface issue, but there's a deeper stale pane tracking problem underneath. Sharing my findings here in case it's valid and can be useful for the fix.

---

iTerm2 Backend: killPane() missing -f flag and stale pane tracking

Summary

ITermBackend.killPane() calls it2 session close -s <id> without the -f flag. In non-interactive contexts, the confirmation prompt defaults to "No", so panes silently fail to close and are left orphaned.

Additionally, killPane() does not remove the pane ID from the internal tracking array regardless of whether the close succeeds or fails.

Root cause

killPane() currently does:

async killPane(paneId, teamName) {
  return (await runIt2(["session", "close", "-s", paneId])).code === 0;
}

Problems:

  • No -f flag — silent failure in non-interactive contexts
  • Does not remove paneId from the internal pane array on success (or failure)

The fix already exists (partially)

resetITermBackendState() is defined and exported but never called:

function resetITermBackendState() {
  teammateSessionIds.length = 0;  // clears the pane array
  isFirstTeammate = false;        // resets first-teammate flag
  serializationPromise = Promise.resolve();
}

Suggested fix

  1. killPane() — add -f flag and remove from array:
async killPane(paneId, teamName) {
  const result = await runIt2(["session", "close", "-f", "-s", paneId]);
  const idx = teammateSessionIds.indexOf(paneId);
  if (idx !== -1) teammateSessionIds.splice(idx, 1);
  return result.code === 0;
}
  1. Call resetITermBackendState() during team deletion as a safety net — handles edge cases where panes are closed externally.
  1. Optional resilience — in createTeammatePaneInSwarmView, validate the last pane ID before splitting from it. If it's dead, fall back to splitting from the leader session or active session.

Reproduction

Prerequisites:

  • macOS with iTerm2
  • iTerm2 Python API enabled (Preferences → General → Magic → Enable Python API)
  • Claude Code running in iTerm2 (not inside a tmux session)

Steps:

  1. Start Claude Code in iTerm2
  2. Spawn agents with TeamCreate — iTerm2 split panes are created via it2 session split
  3. TeamDelete the team — panes remain open (missing -f flag)
  4. Only fix: manually close each pane

Workaround attempted: manually closing panes

Closing the orphaned panes manually (or via it2 session close -f) resolves the visible problem but creates a second one. The internal pane tracking array still holds the dead session IDs. When createTeammatePaneInSwarmView is called to spawn new agents, it splits from the last tracked pane:

let lastPane = teammateSessionIds[teammateSessionIds.length - 1];
if (lastPane)
  args = ["session", "split", "-s", lastPane];  // uses stale dead ID
else
  args = ["session", "split"];                   // would work, but unreachable

This fails with:

Failed to create iTerm2 split pane: Error: Session '<uuid>' not found

This persists for the entire session with no recovery other than restarting Claude Code, since resetITermBackendState() is never called and the array is in-memory only.

Environment

  • Claude Code v2.1.70 (native binary)
  • macOS, iTerm2 with Python API enabled
  • --teammate-mode tmux (iTerm2 backend still selected when not inside a tmux session)