[BUG] Bash tool LD_PRELOAD recursion can abort Claude Code session instead of returning a failed tool_result
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:
- Run Bash tools in a resource boundary that cannot kill the Claude Code host process.
- Terminate the Bash tool and its descendants when the boundary is exceeded.
- Return a failed
tool_resultcontaining timeout/signal/resource-limit information. - Keep the Claude Code main process and stream-json protocol alive.
- 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.
- 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");
}
```
- Start a disposable Linux container with limited memory but no PID cgroup cap:
``bash``
podman run --rm --pids-limit=-1 --memory=1g --cpus=1 ...
- Confirm inside the container:
``bash``
cat /sys/fs/cgroup/pids.max
Expected:
``text``
max
- Install Claude Code:
``bash``
npm install -g @anthropic-ai/claude-code@2.1.226
claude --version
Expected:
``text``
2.1.226 (Claude Code)
- 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
- 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"
}
- Observe that no matching
tool_resultis emitted.
- Observe that no final
type: "result"event is emitted.
- Observe that the outer container exits with code
137.
- 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)
- 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=$?
- In the bounded control case, Claude Code returns a normal
tool_resultand emits a finaltype: "result"event. I confirmed this with both:
``text``
Claude Code 2.1.143
Claude Code 2.1.226
- 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 returnedtool_resultsuccessfully. - Without command-level
timeout, both versions could be killed by the container memory cgroup before producingtool_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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗