anthropics / anthropics/claude-code

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

Open
#94,820 1 comment 0 reactions 0 assignees View on GitHub
area:bash bug has repro platform:linux regression
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

### Preflight Checklist

- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) 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:

```mermaid
%%{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:

```mermaid
%%{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.
2. 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.
3. Keep the session working: dispatch further background tasks in the same session.
4. 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

```shell

```

### 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.

Contributor guide

No contributing guide indexed for this repository

Research direction

No repository file or test is named in the report. Begin with the run_in_background process-supervision path on Linux and reproduce the signalfd-attributed SIGTERM; done means background tasks remain tracked and complete without the supervising CLI terminating them, while preserving normal session behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash, go, linux
Domain
cli, operating-systems
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.