Agent team iTerm2 panes not closed on teammate shutdown
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
- Launch Claude Code with
--teammate-mode tmuxin iTerm2 (Python API enabled) - Create an agent team and spawn 2 teammates
- Panes appear correctly (backend:
iterm2) - Shut down both teammates via
shutdown_request - Both agents respond with
shutdown_approved(includingpaneIdandbackendType: iterm2) - Bug: Panes remain open — shell is alive at a prompt, agent process exited
- Close panes manually
- Try to spawn a new team
- 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
4 Comments
I think this might be happening because when it runs the
it2command 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 runsit2 session close -s <paneId>without the-f(force) flag. Theit2 session closecommand prompts for interactive confirmation (Close session <id>? [y/N]:), which defaults to "No" in the non-interactive context, causing aclick.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
-fto thekillPanecall:Possibly related secondary issue: The internal pane tracking array (
PW1in the minified source) only supportspush()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 withSession '<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
SessionEndhook usingit2 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
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:
-ftoit2 session close(primary fix) — As noted above, the interactive confirmation silently fails in a non-interactive context. Force-closing fixes the active cleanup path.execthe Claude command (fallback) — If the spawn usedexecbefore 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 theit2cleanup fails for other reasons (stale pane IDs,it2not available, etc.).Hitting both issues described here.
Repro for the stale pane tracking (secondary issue):
TeamCreate→ spawn agent viaAgentwithteam_name→ agent opens in iTerm2 split pane (works fine)shutdown_request→ agent shuts down →TeamDeletesucceedstell aSession to closeby unique ID)TeamCreatea second team → spawn a new agent → fails withError: Session '<old-pane-id>' not foundThe 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.
I hit this bug during heavy agent team usage and debugged the iTerm2 backend source to understand the full scope. The missing
-fflag 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-fflag and stale pane trackingSummary
ITermBackend.killPane()callsit2 session close -s <id>without the-fflag. 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:Problems:
-fflag — silent failure in non-interactive contextspaneIdfrom the internal pane array on success (or failure)The fix already exists (partially)
resetITermBackendState()is defined and exported but never called:Suggested fix
killPane()— add-fflag and remove from array:resetITermBackendState()during team deletion as a safety net — handles edge cases where panes are closed externally.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:
Steps:
TeamCreate— iTerm2 split panes are created viait2 session splitTeamDeletethe team — panes remain open (missing-fflag)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. WhencreateTeammatePaneInSwarmViewis called to spawn new agents, it splits from the last tracked pane:This fails with:
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
--teammate-mode tmux(iTerm2 backend still selected when not inside a tmux session)