[Bug] statusLine command: zombie subprocess accumulation exhausts process table

Resolved 💬 4 comments Opened Mar 13, 2026 by christianromney Closed Apr 10, 2026
This report supersedes #31961, which I filed on 2026-Mar-07. That report incorrectly attributed the symptoms (RSS runaway, CLI crash) to the context-management-2025-06-27 beta. Controlled experiments revealed the actual root cause is described here; #31961 will be closed.

Affected versions: First observed in ≥2.1.71; reproduced on 2.1.75 (current).

Summary

When a statusLine command script is configured in ~/.claude/settings.json, Claude spawns the script's subprocesses at an extremely high rate without reaping them. The resulting zombie accumulation exhausts the process table and drives RSS into the gigabytes, rendering Claude — and eventually the entire system — unresponsive.

---

Experimental methodology

I wrote a probe script (statusline-probe.sh) that attaches to the Claude process by PID and samples RSS and direct child process count every second for 30 seconds, writing output to a timestamped file. Children are enumerated via pgrep -P <pid> '.' (the pattern argument is required on macOS for the PPID filter to apply) and retrieved in a single batched ps call. RSS is captured at start and end of the window.

All runs used:

  • macOS 15, Apple Silicon
  • Claude v2.1.75 (probe data collected under 2.1.73–2.1.75; session transcript recorded under 2.1.72)
  • The same resumed session transcript throughout (2c8474a0-2964-428f-8280-28c31bc5bee0): 560,906 bytes, 187 JSONL records, 82 assistant turns, 38k/200k tokens, ~4.5 hours old

Controlled variable: the presence or absence of statusLine in ~/.claude/settings.json. All other configuration was held constant.

---

Evidence

Claims are classified by epistemic status:

  • Directly observed — seen or measured directly, no inference required
  • Deduced with certainty — logically necessary from observed facts
  • Inferred from strong evidence — high-confidence but not logically necessary
  • Conjecture — plausible but not independently confirmed

---

Directly observed
  1. Controlled experiment: statusLine present + --resume of a large transcript → RSS grows continuously from launch; Claude becomes unresponsive within seconds to minutes. statusLine removed → --resume of the same session completes without incident. No other variable changed between runs.
  1. Fresh session stability: A fresh (non-resumed) session with statusLine configured remained stable for the full 30-second observation window: 2 persistent children, RSS flat at 367M, delta +0M.
  1. Onset captured: A resumed session without any process throttling showed the following sequence:

| Time | RSS | Kids | Notes |
|------|-----|------|-------|
| 00:00–00:11 | 371–383M | 2 | Stable — python (basic-memory MCP) + bash only |
| 00:12 | 424M (+41M) | 4 | First invocationgh and bash appear as live children |
| 00:13 | 500M (+76M) | 1,149 | Explosion — those PIDs now <defunct>, 1,147 more added |
| 00:15 | 597M (+97M) | 2,122 | Continued accumulation |
| End | 1,038M | — | +667M delta over 30s |

  1. Zombie accumulation during live runaway (not a SIGKILL artifact): All 1,147+ accumulated children were <defunct> while Claude was still running and had not been killed. This confirms Claude is not calling wait()/waitpid() for its statusline children. (Earlier runs where <defunct> was observed after Activity Monitor sent SIGKILL cannot distinguish this; this run can.)
  1. Monotonic child growth confirmed: In a throttled run (taskpolicy -b -d throttle), the full 30-second time series was captured. KIDS grew without interruption from 558 → 5,904, an average of ~185 new zombie children per second.
  1. System-wide fork failures: In earlier runs, unrelated processes — Slack, the macOS screenshot tool, the probe script itself — all emitted:

``
fork: retry: Resource temporarily unavailable
``

  1. Memory progression in a single unthrottled run: Activity Monitor screenshots at progressive points during one runaway session: ~250 MB → 1.12 GB → 3.77 GB → 17.13 GB.
  1. CPU and syscall rate: Activity Monitor during the runaway: 99.44% CPU, approximately 20 million Unix syscalls in 61 seconds.
  1. Babashka statusline without transcript parsing — fresh session stable (PID 89516):

2 children throughout (basic-memory python + bash), RSS 1628M → 1627M (-1M delta),
full 30-second window. No zombie accumulation.

  1. Babashka statusline without transcript parsing — known-repro session stable

(PID 90280): The same 82-turn, 560 KB session (2c8474a0) that reliably triggered
the runaway with the .sh script was resumed with the current .clj script (which
does not read the transcript). Result: 2 children throughout, RSS flat at 426M
(+0M delta), 30 seconds. No zombie accumulation.

  1. Babashka statusline without transcript parsing — second resumed session stable

(PID 91028): 2 children throughout, RSS flat at 475M (+0M delta), 30 seconds.
No zombie accumulation.

---

Deduced with certainty
  1. statusLine is a necessary condition. The controlled experiment changed exactly one variable. Removing it eliminated the behavior; restoring it restored it.
  1. Claude is not reaping its statusline children. Zombie (<defunct>) processes accumulated continuously while Claude was alive and had not been killed. A process becomes a zombie when its parent has not called wait()/waitpid() after it exits.
  1. The process table was exhausted. fork: retry: Resource temporarily unavailable is the kernel's EAGAIN response to fork(2) when the process limit is reached.
  1. The exhaustion was system-wide. Unrelated processes failed to fork, not just Claude.

---

Inferred from strong evidence
  1. The invocation rate is extremely high. The throttled run showed ~185 new zombie children per second. The statusline script spawns approximately 17–18 subprocesses per invocation (8× jq, 4× git, 1× grep, 2× awk, 1× bash, 1× whoami, plus gh appearing as a likely git credential helper). At 17 subprocesses per invocation, 185 new zombies/second implies approximately 10–11 statusline invocations per second — far beyond any reasonable polling interval.
  1. Large transcript amplifies the behavior. Fresh sessions (small or empty transcript) remain stable; the resumed session (560 KB, 82 turns) runs away. Transcript size may affect per-invocation cost, invocation rate, or both.
  1. gh at onset suggests additional subprocess spawning. The statusline script does not call gh directly. Its appearance as a live child at the moment of first invocation suggests git is spawning gh as a credential helper for GitHub operations, adding to the per-invocation subprocess count.
  1. Transcript file I/O is the operative variable. Two different runtimes (bash and

babashka) both triggered the runaway when configured to read and grep the full
transcript file on every invocation. The current babashka script — identical in
purpose except that it does not read the transcript — is stable even when resumed
with the known-repro session (2c8474a0, 560 KB, 82 turns). Session size alone does
not explain the behavior; the per-invocation transcript read is the common factor
in all runaway cases. The previous babashka version with transcript parsing is
preserved at: https://gist.github.com/christianromney/4ded1206e3533b6a66434844b1c164b6/9bcae64fd1d0a6965a6c72af2851b625feb17ade

---

Conjecture
  1. Invocation mechanism: Claude may be invoking the statusline command on every internal event or render cycle rather than on a fixed timer, or with a timer interval far shorter than intended.
  1. Mechanism of transcript I/O harm: Reading and grepping a large, growing file at

high invocation rates may cause the runaway through one or more of: blocking the event
loop with sequential I/O, forcing repeated page faults as the transcript grows, or
exhausting file descriptors when the file is opened faster than it is closed.

  1. Scope: Whether this affects all statusLine users or only those with large transcripts, certain script patterns, or specific configurations is unknown.

---

Prior art search

| Issue | Title | Similarity | Why distinct |
|-------|-------|------------|--------------|
| #8073 (closed) | statusline.js causes infinite loop and EBADF error | Same mechanism: statusLine script, FD exhaustion, transcript reads, high subprocess spawn rate | Different runtime (Node.js v24, not Bun); older version (1.0.85); crashes Claude rather than accumulating zombies system-wide; reporter did not isolate transcript I/O as the operative variable |
| #33453 | Memory leak: Bun/WebKit Malloc unbounded growth reaching 14GB+ | macOS + Bun + Apple Silicon; rapid RSS growth (~1GB/30s) | Attributed to JSC GC not reclaiming WebKit Malloc chunks; reporter did not check child process count; no fork: retry errors reported; GC recovery observed mid-burst argues against pure zombie accumulation |
| #33342 | Native memory leak ~30GB/hour crashes server (v2.1.73) | High RSS growth rate; server OOM crashes | Linux (Ubuntu 24.04), Node.js SEA executable (not Bun); attributed to native addon (node-pty); no fork failure messages; different growth profile |
| #32660 | statusLine configuration ignored since v2.1.50 (macOS) | statusLine | Configuration silently not applied — different bug |
| #29411 | Custom statusLine command not rendered on resumed sessions | statusLine, resumed sessions | Rendering failure, not subprocess accumulation |

No existing open issue describes the specific combination: statusLine configured →
zombie subprocess accumulation → process table exhaustion → system-wide fork: retry:
Resource temporarily unavailable
.

---

Statusline command script

<details>
<summary>~/.claude/statusline-command.sh</summary>

#!/usr/bin/env bash
# Claude Code status line — complements Starship / Catppuccin Frappe prompt
# Receives Claude Code session JSON on stdin

input=$(cat)

reset="\e[0m"
dim="\e[2m"
peach="\e[38;2;239;159;118m"
yellow="\e[38;2;229;200;144m"
green="\e[38;2;166;209;137m"
teal="\e[38;2;129;200;190m"
lavender="\e[38;2;186;187;241m"
sapphire="\e[38;2;133;193;220m"
mauve="\e[38;2;202;158;230m"
overlay1="\e[38;2;131;139;167m"

sep="${dim}${overlay1} ${reset}"

cwd=$(echo "$input"             | jq -r '.workspace.current_dir // .cwd // ""')
model=$(echo "$input"           | jq -r '.model.display_name // ""')
remaining=$(echo "$input"       | jq -r '.context_window.remaining_percentage // empty')
total_in=$(echo "$input"        | jq -r '.context_window.total_input_tokens // empty')
total_out=$(echo "$input"       | jq -r '.context_window.total_output_tokens // empty')
cost_usd=$(echo "$input"        | jq -r '.cost.total_cost_usd // empty')
transcript=$(echo "$input"      | jq -r '.transcript_path // empty')
vim_mode=$(echo "$input"        | jq -r '.vim.mode // empty')
session_name=$(echo "$input"    | jq -r '.session_name // empty')

home="${HOME}"
cwd_display="${cwd/#$home/\~}"
IFS='/' read -ra parts <<< "$cwd_display"
num=${#parts[@]}
if [ "$num" -gt 3 ]; then
    truncated="…/${parts[$num-3]}/${parts[$num-2]}/${parts[$num-1]}"
else
    truncated="$cwd_display"
fi

git_info=""
if git --no-optional-locks -C "$cwd" rev-parse --git-dir &>/dev/null; then
    branch=$(git --no-optional-locks -C "$cwd" symbolic-ref --short HEAD 2>/dev/null \
             || git --no-optional-locks -C "$cwd" rev-parse --short HEAD 2>/dev/null)
    if [ -n "$branch" ]; then
        dirty=""
        if ! git --no-optional-locks -C "$cwd" diff --quiet 2>/dev/null \
          || ! git --no-optional-locks -C "$cwd" diff --cached --quiet 2>/dev/null; then
            dirty="*"
        fi
        untracked=""
        if [ -n "$(git --no-optional-locks -C "$cwd" ls-files --others --exclude-standard 2>/dev/null | head -1)" ]; then
            untracked="?"
        fi
        git_info="${yellow} ${branch}${dirty}${untracked}${reset}"
    fi
fi

ctx_info=""
if [ -n "$remaining" ]; then
    remaining_int=${remaining%.*}
    used_int=$((100 - remaining_int))
    if   [ "$used_int" -ge 90 ]; then color="\e[38;2;231;130;132m"
    elif [ "$used_int" -ge 75 ]; then color="\e[38;2;239;159;118m"
    else                              color="$green"
    fi
    ctx_info="${color}Context: ${used_int}% Used${reset}"
fi

token_info=""
if [ -n "$total_in" ] && [ -n "$total_out" ]; then
    in_k=$(echo "$total_in"  | awk '{printf "%.1fk", $1/1000}')
    out_k=$(echo "$total_out" | awk '{printf "%.1fk", $1/1000}')
    token_info="${overlay1}Tokens: ${in_k}↑ ${out_k}↓${reset}"
fi

cost_info=""
if [ -n "$cost_usd" ]; then
    cost_fmt=$(echo "$cost_usd" | awk '{printf "$%.4f", $1}')
    cost_info="${mauve}Cost: ${cost_fmt}${reset}"
fi

turn_info=""
if [ -n "$transcript" ] && [ -f "$transcript" ]; then
    turns=$(grep -c '"role":"assistant"\|"role": "assistant"' "$transcript" 2>/dev/null || true)
    [ -n "$turns" ] && [ "$turns" -gt 0 ] && turn_info="${overlay1}Turns: ${turns}${reset}"
fi

vim_info=""
if [ -n "$vim_mode" ]; then
    case "$vim_mode" in
        NORMAL)  vim_info="${green}NORMAL${reset}" ;;
        INSERT)  vim_info="${sapphire}INSERT${reset}" ;;
        *)       vim_info="${overlay1}${vim_mode}${reset}" ;;
    esac
fi

name_info=""
if [ -n "$session_name" ]; then
    name_info="${dim}${overlay1}[${session_name}]${reset}"
fi

user=$(whoami)
parts_out=()
parts_out+=("${overlay1}${user}${reset}")
parts_out+=("${peach}${truncated}${reset}")
[ -n "$git_info"    ] && parts_out+=("$git_info")
[ -n "$name_info"   ] && parts_out+=("$name_info")
parts_out+=("${lavender}${model}${reset}")
[ -n "$ctx_info"    ] && parts_out+=("$ctx_info")
[ -n "$token_info"  ] && parts_out+=("$token_info")
[ -n "$cost_info"   ] && parts_out+=("$cost_info")
[ -n "$turn_info"   ] && parts_out+=("$turn_info")
[ -n "$vim_info"    ] && parts_out+=("$vim_info")

result=""
for i in "${!parts_out[@]}"; do
    if [ "$i" -eq 0 ]; then
        result="${parts_out[$i]}"
    else
        result="${result}${sep}${parts_out[$i]}"
    fi
done

printf "%b" "$result"

</details>

<details>
<summary>~/.claude/statusline-command.clj (current — no transcript parsing)</summary>

#!/usr/bin/env bb

(require '[cheshire.core :as json]
         '[clojure.string :as str])

;; ── Catppuccin Frappe palette ────────────────────────────────────────────────

(def colors
  "Catppuccin Frappe ANSI 24-bit color escape codes, keyed by role.
   :reset and :dim are SGR attributes; all others are foreground colors."
  {:reset    "\033[0m"
   :dim      "\033[2m"
   :green    "\033[38;2;166;209;137m"   ; #a6d189
   :lavender "\033[38;2;186;187;241m"   ; #babbf1
   :mauve    "\033[38;2;202;158;230m"   ; #ca9ee6
   :overlay1 "\033[38;2;131;139;167m"   ; #838ba7
   :red      "\033[38;2;231;130;132m"   ; #e78284
   :peach    "\033[38;2;239;159;118m"}) ; #ef9f76

;; ── Data extraction ──────────────────────────────────────────────────────────

(defn status-data
  "Given `input`, a keyword-keyed map parsed from Claude Code's statusline
   JSON payload, returns a map with keys:
   - :model        string display name; empty string if absent
   - :used         number, context window used percentage (optional)
   - :total-in     number, cumulative input tokens (optional)
   - :total-out    number, cumulative output tokens (optional)
   - :cost-usd     number, total session cost in USD (optional)
   - :session-name string, human-readable session name (optional)
   - :session-id   string, unique session identifier (optional)
   - :agent-name   string, agent name when running with --agent flag (optional)"
  [input]
  {:model        (or (get-in input [:model :display_name]) "")
   :used         (get-in input [:context_window :used_percentage])
   :total-in     (get-in input [:context_window :total_input_tokens])
   :total-out    (get-in input [:context_window :total_output_tokens])
   :cost-usd     (get-in input [:cost :total_cost_usd])
   :session-name (:session_name input)
   :session-id   (:session_id input)
   :agent-name   (get-in input [:agent :name])})

;; ── Segment renderers ────────────────────────────────────────────────────────

(defn user-segment
  "Given `user` (string, system username) and `colors` (map of ANSI escape
   codes), returns an overlay1-colored username string.
   Returns nil if `user` is blank or nil."
  [user colors]
  (when-not (str/blank? user)
    (str (:overlay1 colors) user (:reset colors))))

(defn model-segment
  "Given `model` (string display name) and `colors` (map of ANSI escape
   codes), returns a lavender-colored model name string.
   Returns nil if `model` is blank or nil."
  [model colors]
  (when-not (str/blank? model)
    (str (:lavender colors) model (:reset colors))))

(defn context-segment
  "Given `used` (number, pre-calculated context window used percentage) and
   `colors` (map of ANSI escape codes), returns a colored context-usage
   string. Color scales from green to peach to red as usage increases past
   75% and 90%. Returns nil if `used` is nil."
  [used colors]
  (when used
    (let [pct   (int used)
          color (cond (>= pct 90) (:red colors)
                      (>= pct 75) (:peach colors)
                      :else       (:green colors))]
      (str color "Context: " pct "% Used" (:reset colors)))))

(defn token-segment
  "Given `total-in` and `total-out` (numbers, cumulative token counts) and
   `colors` (map of ANSI escape codes), returns a formatted token-count
   string with counts expressed in thousands (e.g. \"3.2k↑ 1.1k↓\").
   Returns nil if either count is nil."
  [total-in total-out colors]
  (when (and total-in total-out)
    (str (:overlay1 colors) "Tokens: "
         (format "%.1fk" (/ (double total-in)  1000)) "↑ "
         (format "%.1fk" (/ (double total-out) 1000)) "↓"
         (:reset colors))))

(defn cost-segment
  "Given `cost-usd` (number, session cost in USD) and `colors` (map of ANSI
   escape codes), returns a formatted cost string (e.g. \"Cost: $0.0142\").
   Returns nil if `cost-usd` is nil."
  [cost-usd colors]
  (when cost-usd
    (str (:mauve colors) "Cost: " (format "$%.4f" (double cost-usd)) (:reset colors))))


(defn session-segment
  "Given `session-name` and `session-id` (strings or nil) and `colors` (map
   of ANSI escape codes), returns a dimmed bracketed string combining the
   non-blank values joined by \" · \" (e.g. \"[my-session · abc123]\").
   Returns nil if both are blank or nil."
  [session-name session-id colors]
  (let [parts (remove str/blank? [session-name session-id])]
    (when (seq parts)
      (str (:dim colors) (:overlay1 colors)
           "[" (str/join " · " parts) "]"
           (:reset colors)))))

(defn agent-segment
  "Given `agent-name` (string or nil) and `colors` (map of ANSI escape
   codes), returns a formatted agent-name string. Present only when Claude
   Code is running with the --agent flag. Returns nil if `agent-name` is
   blank or nil."
  [agent-name colors]
  (when-not (str/blank? agent-name)
    (str (:lavender colors) "Agent: " agent-name (:reset colors))))

;; ── Assembly ─────────────────────────────────────────────────────────────────

(defn status-line
  "Given keyword arguments :data (map as returned by `status-data`), :user
   (string, username), and :colors (map of ANSI escape codes), returns the
   assembled statusline string with non-nil segments joined by a dimmed
   separator."
  [& {:keys [data user colors]}]
  (let [sep   (str (:dim colors) (:overlay1 colors) " " (:reset colors))
        parts (remove nil?
                [(user-segment    user colors)
                 (session-segment (:session-name data) (:session-id data) colors)
                 (model-segment   (:model data) colors)
                 (context-segment (:used data) colors)
                 (token-segment   (:total-in data) (:total-out data) colors)
                 (cost-segment    (:cost-usd data) colors)])]
    (str/join sep parts)))

;; ── Entry point ──────────────────────────────────────────────────────────────

(let [input    (json/parse-string (slurp *in*) true)
      username (System/getProperty "user.name")]
  (print (status-line :data (status-data input) :colors colors :user username)))

</details>

<details>
<summary>statusline-probe.sh</summary>

#!/usr/bin/env bash
# statusline-probe.sh
#
# Diagnoses Claude CLI statusline subprocess accumulation.
#
# WHAT TO LOOK FOR:
#   H1 confirmed  -> KIDS column grows monotonically over time
#   H1 ruled out  -> KIDS stays near 0; problem is internal to Claude runtime
#
# Usage:
#   statusline-probe.sh <PID>         # print to stdout
#   statusline-probe.sh <PID> <label> # tee to ~/Desktop/probes/<label>-<timestamp>.txt

set -euo pipefail

INTERVAL=1     # seconds between samples
TIMEOUT=30     # stop after this many seconds
OUTDIR="$HOME/Desktop/probes"
SFMT="%-10s  %-7s  %8s  %7s  %6s  %s\n"

if [[ $# -lt 1 ]] || ! [[ "$1" =~ ^[0-9]+$ ]]; then
    echo "Usage: $0 <PID> [label]" >&2
    exit 1
fi

CLAUDE_PID="$1"
LABEL="${2:-}"

if ! kill -0 "$CLAUDE_PID" 2>/dev/null; then
    echo "ERROR: PID $CLAUDE_PID does not exist." >&2
    exit 1
fi

if [[ -n "$LABEL" ]]; then
    mkdir -p "$OUTDIR"
    OUTFILE="$OUTDIR/${LABEL}-$(date +%Y%m%dT%H%M%S).txt"
    exec > >(tee "$OUTFILE")
    echo "Output: $OUTFILE"
fi

START_RSS_KB=$(ps -o rss= -p "$CLAUDE_PID" 2>/dev/null | tr -d ' ' || echo 0)
START_RSS_MB=$(( START_RSS_KB / 1024 ))

echo "=================================================================="
echo "  Claude Statusline Probe"
echo "  PID $CLAUDE_PID   interval ${INTERVAL}s   timeout ${TIMEOUT}s"
echo "  Start RSS: ${START_RSS_MB}M"
echo "=================================================================="
printf "\n"
printf "$SFMT" "[SAMPLER]" "ELAPSED" "RSS" "DRSS" "KIDS" "CHILD COMMANDS"
printf "$SFMT" "----------" "-------" "--------" "-------" "------" "--------------"

PREV_RSS_MB=0
START=$(date +%s)

while kill -0 "$CLAUDE_PID" 2>/dev/null; do
    NOW=$(date +%s)
    ELAPSED=$(( NOW - START ))
    [[ $ELAPSED -ge $TIMEOUT ]] && { echo ""; echo "Probe stopped after ${TIMEOUT}s."; break; }
    ELAPSED_FMT=$(printf "%02d:%02d" $(( ELAPSED / 60 )) $(( ELAPSED % 60 )))

    RSS_KB=$(ps -o rss= -p "$CLAUDE_PID" 2>/dev/null | tr -d ' ' || echo 0)
    RSS_MB=$(( RSS_KB / 1024 ))
    RSS_STR="${RSS_MB}M"

    if [[ $PREV_RSS_MB -eq 0 ]]; then DRSS_STR="--"
    else
        DELTA=$(( RSS_MB - PREV_RSS_MB ))
        if   [[ $DELTA -gt 0 ]]; then DRSS_STR="+${DELTA}M"
        elif [[ $DELTA -lt 0 ]]; then DRSS_STR="${DELTA}M"
        else                          DRSS_STR="0"
        fi
    fi
    PREV_RSS_MB=$RSS_MB

    # macOS pgrep -P requires a pattern argument to apply the PPID filter;
    # without one it ignores -P and returns all processes system-wide.
    CHILD_PIDS=$(pgrep -P "$CLAUDE_PID" '.' 2>/dev/null || true)
    CHILD_COUNT=0
    CHILD_CMDS="--"

    if [[ -n "$CHILD_PIDS" ]]; then
        PID_ARGS=$(echo "$CHILD_PIDS" | tr '\n' ',' | sed 's/,$//')
        CHILD_DATA=$(ps -o pid=,comm= -p "$PID_ARGS" 2>/dev/null || true)
        if [[ -n "$CHILD_DATA" ]]; then
            CHILD_COUNT=$(echo "$CHILD_DATA" | wc -l | tr -d ' ')
            CHILD_CMDS=$(echo "$CHILD_DATA" | awk '{printf "%s(%s) ", $2, $1}')
        fi
    fi

    printf "$SFMT" "[SAMPLER]" "$ELAPSED_FMT" "$RSS_STR" "$DRSS_STR" \
        "$CHILD_COUNT" "$CHILD_CMDS"

    sleep "$INTERVAL"
done

END_RSS_KB=$(ps -o rss= -p "$CLAUDE_PID" 2>/dev/null | tr -d ' ' || echo 0)
END_RSS_MB=$(( END_RSS_KB / 1024 ))
RSS_DELTA=$(( END_RSS_MB - START_RSS_MB ))
[[ $RSS_DELTA -ge 0 ]] && RSS_DELTA_STR="+${RSS_DELTA}M" || RSS_DELTA_STR="${RSS_DELTA}M"

echo ""
echo "  End RSS: ${END_RSS_MB}M  (start: ${START_RSS_MB}M  delta: ${RSS_DELTA_STR})"
echo ""
echo "Claude PID $CLAUDE_PID is no longer being watched (timeout or exit)."

</details>

View original on GitHub ↗

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