[BUG] Bash tool LD_PRELOAD recursion can abort Claude Code session instead of returning a failed tool_result

Status Open
Reported on v2.1.143
Maintainer reply None cached
Activity 1 comment · opened Aug 10, 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?

A Bash tool can launch a workload whose subprocess tree exhausts the same container cgroup used by the Claude Code host process. When that happens, the kernel may select the claude process itself as the OOM victim.

In my reproduction, Claude Code emitted a Bash tool_use / task_started, but never emitted the corresponding tool_result, and never emitted a final type: "result" event.

The specific workload used an LD_PRELOAD shared library whose constructor calls system("/bin/true") without clearing LD_PRELOAD. This recursively re-enters the constructor through child /bin/sh / /bin/true processes, creating a fork-bomb-like process tree.

The payload is intentionally unsafe, but the issue is the failure boundary: the Bash tool process tree shares enough resource scope with the Claude Code host that the host process can be killed before it can report a failed tool result.

What Should Happen?

Even if a Bash tool launches a broken workload that recursively spawns subprocesses, exhausts memory, aborts, or dumps core, Claude Code should ideally isolate that failure to the Bash tool.

Expected behavior:

  1. Run Bash tools in a resource boundary that cannot kill the Claude Code host process.
  2. Terminate the Bash tool and its descendants when the boundary is exceeded.
  3. Return a failed tool_result containing timeout/signal/resource-limit information.
  4. Keep the Claude Code main process and stream-json protocol alive.
  5. Eventually emit a final type: "result" event.

Error Messages/Logs

Claude Code emitted a Bash tool_use / task_started.

No matching tool_result was emitted.
No final type="result" event was emitted.

The outer container exited with:

OUTER_RC=137

Host kernel log showed:

Memory cgroup out of memory: Killed process ... (claude)

This reproduced with both:
- Claude Code 2.1.143
- Claude Code 2.1.226

Steps to Reproduce

Run this only inside a disposable container or VM. The unbounded version can create a fork-bomb-like process tree.

  1. Create recur.c:

```c
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void init() {
const char msg[] = "ctor-enter\n";
write(2, msg, sizeof(msg) - 1);
system("/bin/true");
}
```

  1. Start a disposable Linux container with limited memory but no PID cgroup cap:

``bash
podman run --rm --pids-limit=-1 --memory=1g --cpus=1 ...
``

  1. Confirm inside the container:

``bash
cat /sys/fs/cgroup/pids.max
``

Expected:

``text
max
``

  1. Install Claude Code:

``bash
npm install -g @anthropic-ai/claude-code@2.1.226
claude --version
``

Expected:

``text
2.1.226 (Claude Code)
``

  1. Run Claude Code in stream-json mode and ask it to call Bash exactly once with no command-level timeout:

``bash
claude -p 'Immediately call the Bash tool exactly once with this command and no other command: cc -shared -fPIC -o recur.so recur.c; env LD_PRELOAD=$PWD/recur.so /bin/true; echo RC=$?' \
--model <model> \
--output-format stream-json \
--verbose \
--allowedTools Bash
``

  1. Observe that Claude Code emits a Bash tool_use / task_started, for example:

``json
{
"type": "assistant",
"message": {
"content": [
{
"type": "tool_use",
"name": "Bash",
"input": {
"command": "cc -shared -fPIC -o recur.so recur.c; env LD_PRELOAD=$PWD/recur.so /bin/true; echo RC=$?"
}
}
]
}
}
``

``json
{
"type": "system",
"subtype": "task_started",
"task_type": "local_bash"
}
``

  1. Observe that no matching tool_result is emitted.
  1. Observe that no final type: "result" event is emitted.
  1. Observe that the outer container exits with code 137.
  1. Check the host kernel log:

``bash
dmesg -T | grep -iE 'oom|killed process|out of memory|memory cgroup' | tail
``

In my reproduction, the killed process was claude:

``text
Memory cgroup out of memory: Killed process ... (claude)
``

  1. As a bounded control case, wrap the dangerous command in a short command-level timeout:

``bash
cc -shared -fPIC -o recur.so recur.c
timeout -k 2s 1s env LD_PRELOAD=$PWD/recur.so /bin/true
echo RC=$?
``

Or ask Claude Code to run:

``bash
cc -shared -fPIC -o recur.so recur.c; timeout -k 2s 1s env LD_PRELOAD=$PWD/recur.so /bin/true; echo RC=$?
``

  1. In the bounded control case, Claude Code returns a normal tool_result and emits a final type: "result" event. I confirmed this with both:

``text
Claude Code 2.1.143
Claude Code 2.1.226
``

  1. In the unbounded Bash-tool case, I confirmed the failure with both:

``text
Claude Code 2.1.143
Claude Code 2.1.226
``

Summary of the important distinction:

With command-level timeout:
  Bash workload fails in a bounded way
  Claude Code returns tool_result
  Claude Code emits final type=result

Without command-level timeout:
  Bash subprocess tree exhausts the shared container memory cgroup
  kernel OOM-kills claude
  no tool_result
  no final type=result

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.226

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

This is not only an LD_PRELOAD correctness issue. The unsafe payload is just a compact reproducer for a broader isolation problem:

A Bash tool's subprocess tree can consume the same container-level memory cgroup as the Claude Code host process. If the kernel chooses claude as the OOM victim, Claude Code cannot emit a failed tool_result or final type: "result".

The bounded control case is important:

  • With command-level timeout, Claude Code 2.1.143 and 2.1.226 both returned tool_result successfully.
  • Without command-level timeout, both versions could be killed by the container memory cgroup before producing tool_result.

A robust fix may require running Bash tools in a separate resource boundary from the Claude Code host process, for example a child cgroup with its own pids.max / memory limit, and converting resource-limit failures into failed tool_result events.

View original on GitHub ↗

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