Background Bash task SIGTERMed by its own supervising CLI process on Linux — introduced in 2.1.246 (2.1.245 clean)

Status Fixed / completed
Reported on v2.1.220
Maintainer reply None cached
Activity 2 comments · opened Sep 16, 2026 · closed Oct 3, 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?

Summary

On Linux, a long-running command started with run_in_background from a long-lived CLI
session is SIGTERMed seconds to minutes after it starts. The parent CLI survives and the
session keeps going; only the tracked child dies. A signalfd handler in the child names the
sender: it is the supervising claude process itself, same PID as the session that
dispatched the task.

The rate, measured against task starts on one host:

  • 0.8% under 2.1.220 through 2.1.246
  • 19.7% under 2.1.247 / 2.1.250 / 2.1.251 over four heavy days
  • 1.4% after rolling back to 2.1.245, over the following 17 days and 434 task starts
  • 1 kill in 3 dispatches on a second host running 2.1.246, and again on 2.1.247 and 2.1.270

So this is a rate regression, not an on/off switch. 2.1.245 is the best version we have
measured and it is not clean either — six attributed kills in 17 days. Stating it that way
because the obvious reading ("introduced in 2.1.246") is falsified by our own logs.

At every attributed kill under 2.1.245 where the child sampled memory during the task, the
low-water mark of MemFree was well below this host's normal 2 GB+ (356 MB, 729 MB,
1.47 GB, 1.77 GB, 200 MB), while an operator pkill in the same window sampled 6.1 GB.
That makes the memory-pressure reaper in #78674 the leading mechanism, with the version
step affecting how readily it fires rather than whether it exists.

Environment

  • OS: Debian 13, Linux 6.12, KVM guest under Proxmox (virtio_balloon active)
  • CLI invocation: claude --dangerously-skip-permissions --remote-control NAME
  • Versions exercised: 2.1.245, 2.1.246, 2.1.247, 2.1.270, installed side by side under

~/.local/share/claude/versions/

  • Victim: a Go CLI dispatched as a background Bash task; normal runtime 2–40 minutes
  • No container and no systemd unit involved — the child is a plain descendant of the CLI

Observability we added, and why the dates below look the way they do

Every count here comes from instrumentation we added to the child, so the dates say as much
about the instrument as about the bug. Stated up front so no one reads a zero as quiet:

| date | what became visible | consequence |
|---|---|---|
| before 2026-08-04 | nothing | a killed task left no trace at all. 40 log days and 1,968 task starts before this date carry zero kills because they are unobservable, not because they were clean. No onset date can be derived from our data. |
| 2026-08-04 | the child logs a marker when it releases its workspace on any signal | kills become countable, sender unknown |
| 2026-08-30 (host B), 2026-09-01 (host A) | the child installs a signalfd and logs ssi_pid / ssi_uid / sender comm | kills become attributable. This is what separates "the CLI killed it" from "an operator pressed stop" — and it immediately exonerated one signal we had counted as a kill, which turned out to be pkill |
| 2026-09-04 | the child samples /proc/meminfo during the task and logs the low-water mark | memory at the kill becomes visible. Every earlier memory check was made after the fact and saw nothing |

Timeline of the instrument against the versions:

%%{init: {"themeVariables": {"cScale0": "#9e9e9e", "cScaleLabel0": "#ffffff", "cScale1": "#c62828", "cScaleLabel1": "#ffffff", "cScale2": "#2e7d32", "cScaleLabel2": "#ffffff", "cScale3": "#c62828", "cScaleLabel3": "#ffffff", "cScale4": "#2e7d32", "cScaleLabel4": "#ffffff"}}}%%
timeline
    title Instrument and version timeline, 2026 - grey is unmeasurable, red is the regression, green is the rollback
    section Baseline, 0.8 percent
      08-04 : kill marker added, first countable kill
      08-04 to 08-26 : 2.1.220 to 2.1.246 in service : 11 kills in 1368 task starts
    section Regression, 19.7 percent
      08-27 to 08-30 : 2.1.247 and 2.1.250 and 2.1.251 : 35 kills in 178 task starts
      08-30 : sender attribution added : sender named as the CLI itself
    section Rollback, 1.4 percent
      08-31 : rollback to 2.1.245 on two hosts
      09-04 : in-task memory sampling added
      08-31 to 09-16 : 2.1.245 in service : 6 attributed kills in 434 task starts
    section Bisect on a third host, unpinned
      09-15 : 2.1.270 reproduces : 2 attributed kills
      09-16 : 2.1.247 reproduces : 1 attributed kill
      09-16 : 2.1.246 reproduces : 1 kill in 3 dispatches : interval closed
    section Rollback on the third host, applied
      09-16 1533 CEST : drain paused at the killed unit : branch clean, no repair owed
      09-16 1730 CEST : 2.1.245 fetched and placed : all three hosts now on the same version
      next : resume the drain : expect the 1.4 percent floor, not zero

The four sections are colored by the init directive above — grey for the window where a
kill left no trace, red for the versions that carry the regression, green for the rollback,
red again for the bisect probes, which are bad-version results and do not belong in the
rollback block.

Two syntax constraints if this diagram is edited: a mermaid timeline splits a line
on every colon, so clock times are written 1533 CEST rather than 15:33,
and the init directive must stay on one line.

The fifth section is the same rollback applied to the third host, done on 2026-09-16 within
a couple of hours of the bisect closing — owner-reported, not verified from this box, which
cannot reach that host. All three hosts now run 2.1.245. It is drawn green as a version
state, but it carries no outcome data yet: the drain had not resumed under it at the
time of writing, so there is no kill count for this arm.

One honesty note both green blocks have to carry: 1.4 percent is not zero. Six attributed
kills landed under 2.1.245 on the rolled-back host, so green here means "back to the
pre-regression floor", not "fixed" — which is also why the pending section predicts a floor
rather than a clean run.
If a renderer ignores the directive it falls back to mermaid's default section palette and
the reading is unchanged.

Kills per day across the step and the rollback — the burst is the middle block:

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#c62828"}}}%%
xychart-beta
    title "SIGTERMs per day, host A - red bars are kills"
    x-axis ["08-26", "08-27", "08-28", "08-29", "08-30", "08-31", "09-01", "09-03", "09-04", "09-05", "09-10", "09-11", "09-14", "09-16"]
    y-axis "kills per day" 0 --> 15
    bar [1, 4, 4, 13, 14, 0, 2, 0, 1, 1, 0, 1, 0, 1]

What happens

The task is dispatched, runs normally, and is then terminated with SIGTERM with no user
interrupt and no stop gesture. The CLI reports the task as killed. The work is lost; a
relaunch is needed. Latency from task start to signal ranges from 1 s to 958 s, median 77 s,
so this is not a fixed-interval watchdog.

Attribution

The child installs a signalfd and logs ssi_pid / ssi_uid on every signal:

{"message":"signal received",
"fields":{"sender_command":"claude",
"sender_pid":3306786,
"sender_uid":2000,
"signal":"SIGTERM"}}

Resolving that PID while it is still alive:

$ ps -o pid,ppid,uid,lstart,cmd -p 3306786
3306786 3306778 2000 Wed Sep 16 13:07:55 2026 claude --dangerously-skip-permissions --remote-control zinc-2

That is the supervising session itself — the same process that dispatched the task, still
running and still holding the conversation. Not a sibling session, not a stale process, not
an external supervisor. The harness reaps its own background child.

Versions, with the rate at each

Every version we have run since the marker existed, and what it produced:

| version | how it was exercised | kills / task starts | verdict |
|---|---|---|---|
| 2.1.220 .. 2.1.245 | host A, normal load, 23 days | 11 / 1368 (0.8%) | non-zero floor, sender not yet attributable |
| 2.1.246 | host C, one batch of 3 dispatches | 1 / 3 | reproduces, attributed (sender pid 3306786) |
| 2.1.247 | host A burst days; host C, one batch | part of 35 / 178; 1 attributed | reproduces, attributed (sender pid 1632496) |
| 2.1.250 / 2.1.251 | host A and host B, four heavy days | 27 / ~100 on those days | worst observed; 14 attributed on the one day the instrument existed |
| 2.1.270 | host C, one batch | 2 attributed | reproduces (sender pid 49928) |
| 2.1.245, after rollback | host A and host B, 17 days | 6 / 434 (1.4%) | best measured, still not clean |

Two things follow, and they pull in opposite directions, so both are stated:

  1. The step is real and it is narrow. 2.1.245 to 2.1.246 is a single release step with

nothing between them, and the rate across that step goes from ~1% to ~30% under
comparable dispatch patterns.

  1. The step is not the whole story. Rolling back to 2.1.245 did not stop the kills, it

returned them to the earlier ~1% floor. Whatever ships in 2.1.246 makes an existing
mechanism fire far more often; it does not appear to create it.

Frequency

A 30-day process-attributed census on a second host found 17 confirmed kills over 1,127
task starts (1.5%). The rollback window on host A is 6 over 434 (1.4%). The burst days were
19.7%. Under 2.1.246 on a third host, 1 kill in 3 dispatches. Latency from task start to
signal is 1 s to 958 s, median 77 s.

Ruled out, with the measurement

  • Kernel OOM killer: no kernel OOM records at any kill, across the whole history.
  • External senders (cron, a supervisor process, docker, the inner executor): excluded by

sender_pid / sender_uid — the sender is the CLI.

  • Foreground control: roughly 15 foreground runs of the identical command across two hosts,

zero killed. Only backgrounded, harness-tracked children die.

  • Session scope: a bare sleep 120 backgrounded inside an affected session was killed 3 of

3; the same command in a fresh session on the same boot survived.

  • Context size and compaction: 138 dispatches above 350k tokens with zero kills, and no

compaction within 10 minutes either side of any censused kill.

Memory: the leading mechanism, on our own measurement

This started as "ruled out" and reversed when we measured it properly, so the sequence is
worth giving.

Every check up to 2026-09-04 was made after the kill: /proc/pressure/memory read
avg10/avg60/avg300 = 0.00 and MemAvailable was 8.6-13 GB, with no kernel OOM records ever.
That is a real measurement of the wrong moment. A 20-second sampler run on the same host
later caught MemFree collapsing to 227 MB for a single sample and recovering — a transient
no after-the-fact check could see.

Since 2026-09-04 the child samples /proc/meminfo while it runs and logs the low-water
mark. Every attributed kill under 2.1.245 with a sampled floor:

| kill | low-water MemFree during the task |
|---|---|
| 09-01 15:59 | 1.47 GB |
| 09-04 20:38 | 356 MB |
| 09-05 03:25 | 729 MB |
| 09-11 18:53 | 1.77 GB |
| 09-16 06:49 | 200 MB |
| operator pkill, 09-14 11:31 (control) | 6.1 GB |

This host normally holds 2 GB+ free. The control shows the sampler is not simply reporting
a low box. Caveat kept: a floor somewhere during the task is still not pressure at the
instant of the signal, and the hosts run under a hypervisor balloon whose MemTotal moves
(14 -> 10.5 -> 15 GB inside eight hours on one box), so absolute figures are soft. But the
direction is consistent and it fits #78674.

Workarounds tried

| workaround | result |
|---|---|
| Detach the child with setsid so it leaves the session's process group | The kills stop. But a detached child is no longer harness-tracked, so its completion never re-invokes the supervising session — the supervisor waits forever on work that already finished. Unusable for our pattern, and we have written it into our own house rules as forbidden. It is worth reporting as a diagnostic result rather than a fix: leaving the session's tracked-task set is sufficient to escape the signal, which is what a session-scoped reaper predicts and what an external killer does not. |
| Run the identical command in the foreground instead | Never killed, ~15 invocations across two hosts. Foreground calls are not tracked tasks. |
| Roll the CLI back to 2.1.245 | Rate drops from 19.7% to 1.4%. Does not eliminate. |
| CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1 | Not yet tried. We found this name in the discussion on #78674 and would like confirmation that it exists and disarms this path — unlike setsid it would keep the child tracked, which is the property we need. |

Reproduction

  1. On Linux, start claude --remote-control NAME on 2.1.246 or later.
  2. From that session, start a command that lives for minutes, backgrounded

(run_in_background) — sleep 600 is enough.

  1. Keep the session working: dispatch further background tasks in the same session.
  2. Within seconds to minutes, a task is marked killed. Capture the sender from the child

with signalfd, or run the child under
strace -f -e trace=none -e signal=SIGTERM.

Not deterministic — roughly 1 in 3 dispatches on 2.1.246 here.

What would help

  • Whether this is the task_local_shell_pressure_reap listener described in #78674 or a

separate reap path. Our in-task memory sampling says the kills land at low MemFree,
which fits that listener.

  • Whether CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1 is a real switch and what it turns

off.

  • Any environment variable or debug flag that logs the reap decision and its reason at the

moment the CLI sends the signal.

  • What changed in 2.1.246 around background task lifecycle.

Possibly related

#78674, #79801, #90490, #25188, #84981

What Should Happen?

Be resilient against memory pressure as in Version <= 2.1.245. works well in the same environment for two weeks now.
Log reason for SIGTERM in session log.

Error Messages/Logs

Steps to Reproduce

This is a statistical topic. Objective evidence attached in bug description. Tried latest version yesterday and today, just to go back to 2.1.245.

Proxmox ballooning driver is most likely trigger.

Claude Model

Not sure / Multiple models

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.245

Claude Code Version

2.1.270 (multiple see text)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Windows Terminal

Additional Information

See Attribution section.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗