Attaching to a stopped/idle background agent crashes the worker with contradictory "currently running as a background agent" error

Status Fixed / completed
Reported on v2.1.199
Maintainer reply None cached
Activity 9 comments · opened Jul 3, 2026 · closed Aug 17, 2026

Summary

Attaching to a stopped/idle background agent session from the claude agents (agent view / FleetView) list crashes the worker with a self-contradictory error: the stopped session reports that it is "currently running as a background agent." Sessions with a running icon attach fine; every session with a stopped/idle icon fails the same way.

Environment

  • Claude Code version: 2.1.199
  • OS: macOS (Darwin 25.5.0)
  • Install: native (~/.local/share/claude/versions/2.1.199)

Steps to reproduce

  1. Open the agent list with claude agents.
  2. Select a session showing the running icon (animated star) and press Enter — attaches fine.
  3. Select any session showing the stopped/idle icon and press Enter to attach.

Actual behavior

Attaching to the stopped/idle session fails with:

Session <session-id> is currently running as a background agent (bg). Use `claude agents` to find and attach to it, or add --fork-session to branch off a copy.

[worker crashed (exit 1 — exit_with_message) — respawning…]

This is contradictory — the session is displayed as stopped/idle, yet the message claims it is "currently running as a background agent." It reproduces for every stopped/idle session, consistently. Running sessions (star icon) are unaffected.

The same message appears when trying claude --resume <session-id> on such a session.

Expected behavior

Pressing Enter on a stopped/idle background session should wake/restart its process and attach, rather than emitting a "currently running" error and crashing the worker.

Workaround

claude --resume <session-id> --fork-session successfully branches a copy and opens it. However, if that forked session is then stopped (Ctrl+X), re-attaching to it fails again with the same error under a new session ID — so each stop produces another session that cannot be re-attached from the list.

View original on GitHub ↗

7 Comments

jmsundar · 1 month ago

Additional triage signal: this started appearing right after upgrading to 2.1.199 (installed the same day). Prior versions on this machine were 2.1.198 (day before) and 2.1.197.

At the same time, this version also began emitting startup warnings for malformed permission deny rules (rules written as Read ~/.netrc instead of Read(~/.netrc)) that had been silently inert in earlier versions — i.e. 2.1.199 clearly added/changed some startup validation and error surfacing. That makes me suspect the stopped-session attach failure may have been occurring silently before as well, and a recent change just started surfacing it as a hard worker crash. Flagging in case the regression window is the 2.1.1972.1.199 range.

bugron · 1 month ago

I experience a similar issue; I'm on the latest Claude Code. Just reported via /feedback.

UPD: Downgrading to 2.1.197 fixed the issue for me; I haven't tried 2.1.198, though.

leolower · 1 month ago

Hit the same crash-loop on 2.1.201 (macOS, native install) and did some binary/state spelunking that may help pinpoint the root cause. Everything below reproduced today with ~140 completed bg jobs on disk.

Symptom recap

Attaching (or --resume) to any settled (done/stopped/failed) background session fails; live workers attach fine. The daemon log shows the loop — every revival attempt settles crashed ~22s after spawn:

[2026-07-05T06:39:16.924Z] [bg] bg claimed-spare 2f24e9ba (fleet)
[2026-07-05T06:39:39.584Z] [bg] bg settled 2f24e9ba (crashed)
[2026-07-05T06:39:49.900Z] [bg] bg claimed-spare 2f24e9ba (fleet)
[2026-07-05T06:40:12.567Z] [bg] bg settled 2f24e9ba (crashed)
... (6 consecutive attempts, then same for 4 other sessions)

The worker's stderr is the resume guard message itself, so the "contradictory" error in the OP is the revived worker refusing to resume its own session.

What the guard does (from the 2.1.201 bundle)

The resume path runs a resume_live_check:

let u=performance.now(), d=await Ffe(i.sessionId);
if (uu("resume_live_check_ms", ...), d)
  return process.stderr.write(`Error: Session ${i.sessionId} is currently running as a background agent ...`)

Ffe lists live sessions from the per-PID registry (~/.claude/sessions/*.json), keeping entries whose PID is alive (and proc start time matches), and returns a hit when sessionId matches with kind !== "interactive":

async function Ffe(e){
  let t = await ...listAllLiveSessions()...;
  for (let n of t)
    if (n.sessionId===e && n.kind && n.kind!=="interactive") return {kind:n.kind};
  return null
}

There is no self-exclusion (own PID / own CLAUDE_CODE_SESSION_ID). The revived worker registers itself in ~/.claude/sessions/<pid>.json with kind: "bg" and the target sessionId during startup, then the resume path runs the check, finds its own registration, prints the guard error and exits 1 → daemon respawns → loop. Verified the registry entry exists for live workers (kind: "bg", sessionId = the resumed session) and that no stale entry exists for the crashing sessions between attempts — the only candidate the guard can be seeing is the worker itself.

Consistent with that mechanism:

  • Fresh dispatches (new session, no --resume) spawn fine — the guard never runs.
  • Worker revival at daemon boot (roster recovery) also worked here — presumably a different code path/ordering.
  • Job metadata is irrelevant: I rewrote a failing job's state.json (template claudebg, dropped --agent, bumped cliVersion) and it crashed identically.

Workarounds that work today

  • Per session, one-time: remove/move the job dir under ~/.claude/jobs/<short-id>/ — the session stops being claimed as bg and plain claude --resume <id> works again (at the cost of losing the fleet entry).
  • Downgrade: not independently verified here yet, but the 2.1.197 datapoint above matches the regression window from the OP's triage (2.1.198/2.1.199).

Happy to provide the full daemon.log or run instrumented builds if useful.

wiklob · 1 month ago

Reproducing this on 2.1.201 (macOS arm64, on-demand origin=transient daemon). Adding a code-level root cause and a second, independent defect that also produces this refusal, in case it helps the fix.

Root cause: agents-view attach is dispatched as a plain --resume, so it runs the resume-time liveness guard against the very session being attached

When attaching from the agents view, the daemon spawns the worker as a plain resume — claude --resume /…/projects/…/<sessionId>.jsonl --agent claude (confirmed against the live crash-looping process). That resume path runs a liveness guard before attaching (minified Ffe in the 2.1.201 bundle):

let sid = basenameWithoutExt(resumePath)
if (sid && !forkSession) {
  let live = await Ffe(sid)          // → listAllLiveSessions()
  if (live) return await sN(cn,      // sN → KVf → sets bg exit cause "exit_with_message" → process.exit(1)
    `Session ${sid} is currently running as a background agent (${live.kind}).
     Use \`claude agents\` to find and attach to it, or add --fork-session to branch off a copy.`)
}

FfelistAllLiveSessions (Bze) reports a session live purely from ~/.claude/sessions/<pid>.json being present with pid alive + matching start-time. So attaching to a bg session — which is legitimately live as a bg agent, or whose host <pid>.json is still present mid-handoff — makes the guard fire against the very session being attached, refuse, and exit with cause exit_with_message. The daemon reads that exit as a crash and respawns another plain --resume worker → same guard → loop.

The correct path already exists but isn't taken here: CLAUDE_BRIDGE_REATTACH_SESSION (bridge-reattach) connects to the running host and bypasses Ffe. The defect is that agents-view attach is dispatched as a plain --resume instead of a bridge-reattach.

This lines up with the self-lock #74198 described before it was closed as a dup of this issue: the dispatched resume worker's guard finds its own job record.

Secondary, independent defect: the PID-reuse check fails open

listAllLiveSessions (Bze) cleans a record only when its pid is dead, and the pid-reuse guard (AO) is lenient:

async function AO(pid, recordedStart) {
  if (recordedStart === void 0) return true          // no recorded start → assume same process
  let n = await lstartOf(pid)                          // `ps -o lstart=`
  return n === void 0 || n === recordedStart          // ps failed/timed out → assume same process
}

So a reused pid or a flaky/slow ps yields a false "live" → false refusal, independent of any handoff race. Under load this alone crash-loops attach.

Suggested fixes

  1. Dispatch agents-view attach as bridge-reattach (CLAUDE_BRIDGE_REATTACH_SESSION), not a plain --resume, so it never routes through the resume-time Ffe guard.
  2. Make AO fail closed: on ps error/timeout or missing procStart, treat the record as not matching (i.e. not live) rather than assuming same-process.
  3. Proactively unlink <pid>.json when the pid is reused (alive but different process / procStart mismatch), not only when dead.
  4. Treat a resume-time "session already live" as attach-to-existing (bridge) rather than a fatal exit_with_message the daemon reads as a crash.

Workarounds (consistent with the OP's)

  • claude --resume <id> --fork-session bypasses the guard.
  • Wipe stale liveness records: quit all clients, then rm ~/.claude/sessions/*.json, rm -rf /tmp/cc-daemon-*, relaunch.
cheekychops · 1 month ago

Reproduced on Linux (Ubuntu, kernel 6.8), Claude Code 2.1.201, native install.

Same contradictory refusal on attach, but I want to flag an aggravating behavior that may deserve its own fix: the daemon's pty-host supervisor treats the exit_with_message exit as a worker crash and respawns it indefinitely. Each respawned worker hits the same "Session <id> is currently running as a background agent" refusal, exits 1, and gets respawned again:

Session <session-id> is currently running as a background agent (bg). Use `claude agents` to find and attach to it, or add --fork-session to branch off a copy.

[worker crashed (exit 1 — exit_with_message) — respawning…]

This loops forever inside the pty-host, so the attach viewer terminal becomes impossible to exit cleanly — the only way out is killing the pty-host/viewer processes from another terminal. The respawn also leaves focus-reporting escapes enabled, so the terminal prints literal ^[[I afterwards (same symptom as #74508).

Suggested fix independent of the state-contradiction root cause: exit_with_message is a deliberate, user-facing refusal — the supervisor should treat it as terminal and not respawn, or at minimum cap retries with backoff.

cj81499 · 1 month ago

I reproduced this using claude version 2.1.201 on Ubuntu 24.04.4 LTS (installed via mise, although I doubt that matters).
Notably, it _does_ seem to eventually respawn the worker/resume the agent, but it takes a few seconds for it to retry.

I tried downgrading to determine when this regression was introduced. It seems to work as expected on 2.1.198, but not 2.1.199. I'll be sticking with 2.1.198 until I hear this has been addressed.

hyperpolymath · 1 month ago

Hit the same failure with one detail worth capturing here: it isn't always a single crash — it can become an indefinite crash-loop. Attempting to resume/attach produces a repeating

[worker crashed (exit 1 — exit_with_message) — respawning…]

that never resolves into an attach, alongside the contradictory:

Session <id> is currently running as a background agent (bg). Use `claude agents` to
find and attach to it, or add --fork-session to branch off a copy.

There's no path forward from the attach attempt itself — the only escape is to stop the session from the separate claude agents list view. Same-spirit fix as this issue: attaching to a session in this state should either succeed, or fail once with a clear, actionable error — never loop respawning… indefinitely.

_(Originally filed as a combined report in #74249; splitting the detail here since this is the canonical attach-crash issue.)_

Showing cached comments. Read the full discussion on GitHub ↗