[BUG] shutdown_approved pane-close handler is non-deterministic — same setup produces orphan in some runs

Resolved 💬 1 comment Opened May 3, 2026 by haltsir Closed Jun 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When a teammate emits shutdown_approved (carrying paneId + backendType: tmux), the harness's pane-close handler is non-deterministic. Two identical clean-shutdown sequences produced different outcomes on a single test session: one closed the pane cleanly within 2 seconds, the other left an orphan bash holding the pane open after the teammate's claude had already exited.

When the close fails, the bash wrapper that launched claude is left at an interactive prompt. SIGTERM to that bash is ignored — only SIGKILL closes the pane. The teammate's claude process exits in both cases (so the harness thinks the teardown succeeded), but the tmux pane survives in the failing case.

What Should Happen?

shutdown_approved should reliably trigger pane closure via the paneId + backendType: tmux payload it carries. Pane count should drop within a few seconds of the response, every time.

If the underlying tmux operation can race / fail intermittently, the harness should verify the close actually landed and retry / escalate to a kill if it didn't.

Error Messages/Logs

Two runs of the identical reproduction had different polled timelines.

**Run A (clean — pane closed):**

[t+0s]  panes=2  probe_pid=ALIVE  team_dir=present
[t+1s]  panes=2  probe_pid=ALIVE  team_dir=present
[t+2s]  panes=1  probe_pid=GONE   team_dir=absent   ← shutdown_approved fired AND pane closed


**Run B (orphan — pane stayed open, same setup):**

[t+0s ]  panes=2  probe_pid=ALIVE  team_dir=present
[t+1s ]  panes=2  probe_pid=ALIVE  team_dir=present
[t+2s ]  panes=2  probe_pid=ALIVE  team_dir=present
[t+3s ]  panes=2  probe_pid=ALIVE  team_dir=present
[t+5s ]  panes=2  probe_pid=GONE   team_dir=absent   ← claude exited, team_dir wiped, but pane still open
[t+10s]  panes=2  bash_pid=ALIVE   team_dir=absent   ← bash stayed alive holding the pane


Pane state inspection on the orphan via `tmux list-panes -a -F '#{pane_pid} #{pane_current_command} #{pane_dead}'`:


8503  2.1.126  0     ← orchestrator's pane (alive, expected)
71988 bash     0     ← orphan: claude exited, bash dropped to interactive prompt


Note `pane_current_command` for the orphan is now `bash` (was previously the versioned claude binary path), confirming claude exited and bash inherited control.

The harness reports the shutdown handshake succeeded:


{"type": "shutdown_approved", "requestId": "shutdown-...", "from": "probe", "paneId": "%8", "backendType": "tmux"}


…so from the orchestrator's view nothing went wrong. The orphan is invisible without external inspection.

Steps to Reproduce

  1. Create a fresh team:

``
TeamCreate({ team_name: "close-test" })
``

  1. Spawn a teammate that idles immediately:

``
Agent({
team_name: "close-test",
name: "probe",
subagent_type: "general-purpose",
prompt: "FIRST ACTION: ToolSearch select:SendMessage. Then SendMessage to team-lead 'ack'. Then go idle. When you receive {type: shutdown_request}, reply with shutdown_response."
})
``

  1. Wait ~15 seconds for the teammate to boot and ack.
  1. Send a clean shutdown_request (proper-object form):

``
SendMessage({ to: "probe", message: { type: "shutdown_request" } })
``

  1. Poll tmux pane state every 1s for 10 seconds. Do NOT call TeamDelete. Watch:
  • tmux list-panes -a -F '#{pane_pid} #{pane_current_command}'
  • ps -ax -o pid,etime,command | grep <teammate-claude-pid>
  • ls ~/.claude/teams/close-test/ (auto-cleans either way)
  1. Observe outcome. Run the entire sequence (steps 1–5) multiple times. Some runs will produce a clean close (pane count 2 → 1 by t+2s); other runs will produce the orphan (pane count stays at 2 even after claude exits).
  1. Cleanup of the orphan: kill -9 <bash-pid> from the polling output. SIGTERM is ignored.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.126

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

This is one of three related bugs in the Agent Teams teardown path (filed separately per the preflight checklist):

  • Related Bug 1: TeamDelete returns success on a team with a shutdown-pending teammate, orphaning a pane via a different teardown race.
  • This issue (Bug 2): shutdown_approved's pane-close handler is non-deterministic.
  • Related Bug 3: Orchestrator lead context never auto-releases after teammate exits — sticky status bar + blocked TeamCreate.

Bug 2 + Bug 3 compound: even after a "clean" shutdown, the user often has both an orphan pane AND a sticky lead context displaying the dead teammate's name in the status bar.

I cannot 100% rule out a race between the harness's pane-close call and tmux's own pane-handling. If that's the underlying cause, the suggested fix (verify-then-escalate) still resolves the user-visible symptom.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗