2026-10-11 16:37 UTC

Corral's author CG144 claims his released Linux runner verifies that every process an AI-agent command starts is dead before it returns β€” cgroup v2 group kill in enforced mode, subreaper plus /proc sweep and pidfd signals in fallback β€” closing documented Claude Code background-process-leak and SIGTERM failure modes; adoption by coding-agent harnesses or CI runners would establish verified process-tree reaping as a standard harness component.

state: watchingheat: lowuncertainty: mediumconvergesscott: highagent-harnesses process-lifecycle coding-agentsCG144Cardinal44

What is this?

Corral is a released Linux command runner by GitHub author CG144, presented via Show HN, that runs a command under a time limit and then verifies β€” exiting with status 120 if it cannot prove it β€” that no process the command started is still alive. Its two-tier design matches the kernel's reliability hierarchy: an enforced mode using cgroup v2's atomic cgroup.kill (available since kernel 5.14, which runc's maintainers confirm does 'the same thing' as their manual signal-all-processes loop), and a fallback using subreaper + /proc sweep + pidfd signals β€” the same pidfd-from-/proc mechanism discussed by runc's maintainers in the process-tree-killing threads. The snippets corroborate the mechanisms and the surrounding pain point (agent loops, hangs, and runaway processes surviving manual kills), but they do not themselves document Corral's benchmarks or the specific Claude Code background-process-leak and SIGTERM failure modes claimed β€” those rest on the case's artifact and hypothesis, not the supplied web material. Adjacent 2026 coverage shows the agent-isolation/harness territory is visibly active (Docker cloud sandboxes, VS Code agents outliving the chat window), giving the adoption-vector claim some ambient context.

Why it matters to Scott

An unrelated author independently arrives on Scott's ground: a released runner that proves process-tree death (exit 120 when it can't) closes the same Claude Code background-process-leak failure mode his own dev wiki documents, making this a dated receipt for his deterministic-verification-before-assertion doctrine and an immediately testable component for his bubblewrap/systemd/incus agent harnesses. The snippets corroborate the mechanism and the surrounding pain point but not the benchmark or Claude-Code-specific claims β€” those rest on the artifact, so treat adoption as an open test, not a settled fact.
dev:technology.claude-codeip:concept.deterministic-verification-before-assertionip:concept.runtime-containmentdev:concept.deterministic-agent-control-planeradar:concept.agent-harnessesradar:concept.agent-runtimeradar:jobbox-command-backgroundingradar:supervice-agent-process-supervisorradar:hardstop-kernel-agent-preemptionradar:aipass-false-success-fixes
queries asked of Scott's wikis
  • Claude Code background process leak SIGTERM failure mode
  • agent harness subprocess cleanup timeout process tree
  • cgroup namespace sandbox isolation coding agent
  • verified guarantee exit code contract harness component
  • dev project runner spawn kill subprocess management
  • CI runner process reaping agent isolation standard

Measured heat

now 0 pts/hpeak 1 pts/hcomments 0/hpeers p14momentum: steady2 platformsage 303h
points/hour across evidence Β· reading as of 2026-10-12 02:59:37.977291+11:00 Β· deterministic, not a model opinion

How the heat travelled

09-29 02:32 (minted)⭐ origin echo-reconstructedCorral runs a command with a time limit and verifies β€” exiting 120 if it cannot prove it β€” that no process the command started is still aliv
CG144 (GitHub: Cardinal44) on github (echo) Β· attributed from hn.story.49886422 Β· published time unknown
β€”
09-29 00:35first on hacker news Β· published Β· lag ?Show HN: Corral kill every command your agent starts
CG144
β€”
09-29 00:35amplified on hacker news πŸ‘‘hn.story.49886422
CG144
peak 19 Β· 6 comments Β· 100% of case engagement
09-29 02:21our radar first saw it Β· lag ?discovery anchor: hn.story.49886422β€”
pace: p54 vs 1188 stories at the 168h mark (now 303h old) β€” ahead of agent-screenshot-data-leaks (1.0x), behind 37signals-agent-driven-default (1.0x)

Evidence (2) β€” ⭐ canonical anchor

sourceobjectauthorscorecomments
🟧 hnShow HN: Corral kill every command your agent starts
Retrieved article excerpt

Open article Β· Retrieved 2026-09-29T02:30:43.643940+00:00

# corral

corral runs a command with a time limit. When corral returns, no process that
the command started is still alive. corral verifies this before it returns. If
it cannot prove it, corral exits with code 120.

AI coding agents and CI jobs run shell commands that they did not write. A
typical runner signals its child process or the child's process group, and then
waits until the output pipes close. This model fails in three common cases:

- A daemon forks twice and calls `setsid()`. It is then in a different process
  group and session, and its parent is init, so the signal does not reach it.
- A background process keeps stdout or stderr open. The runner never reads
  end-of-file, so it hangs after the command exits.
- A process ignores SIGTERM and continues to run.

Leftover processes keep ports, file locks, and CPU, and the next run can fail
because of them.

corral controls the full process tree, not only its direct child. In enforced
mode, the command runs in its own cgroup v2 group, and one write to
`cgroup.kill` stops every process in the group. Without a cgroup, corral is a
child subreaper, so orphaned processes become its children. It finds the
remaining processes through `/proc` and signals them through pidfds. In both
modes, the run ends when the command exits, not when the pipes close.

corral is not a security sandbox. It does not limit file access, network
access, or privileges.

## Install

corral needs Linux 5.11 or later, and 5.14 or later for enforced mode.

The prebuilt binary is for x86-64 with glibc 2.36 or later (for example Ubuntu
22.10, Debian 12, or Fedora 37, or a later release):

```
curl -LO https://github.com/Cardinal44/corral/releases/latest/download/corral-linux-x86_64.tar.gz
tar -xzf corral-linux-x86_64.tar.gz
sudo cp corral-linux-x86_64/corral corral-linux-x86_64/corral-enforced /usr/local/bin/
```

To build from source, you need glibc 2.36, CMake 3.22, Ninja, and GCC 11 or
Clang 14. The tests need Python 3.10.

```
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
```

## Use

```
corral --wall 30s -- ./build.sh
corral --wall 5m --max-output 1M --json run.json -- make test
corral-enforced --wall 30s --mem 512M --pids 64 -- ./build.sh
```

corral has two modes:

- **Enforced mode** puts the command in its own cgroup v2 group. The kernel
  keeps every descendant in that group, and one write to `cgroup.kill` stops
  all of them. This mode also applies `--mem` and `--pids`. It needs a
  delegated cgroup. `scripts/corral-enforced` starts one with `systemd-run`.
- **Fallback mode** needs no cgroup. corral finds the processes by parent,
  process group, and session, and scans `/proc` until nothing is left.

The default, `--cgroup-mode=auto`, uses enforced mode when it can.

| Option | Default | Meaning |
| --- | --- | --- |
| `--wall DURATION` | none | Time limit for the run. |
| `--grace DURATION` | `2s` | Time between SIGTERM and SIGKILL. |
| `--verify-timeout DURATION` | `2s` | Time limit for the final check. |
| `--mem SIZE` | none | Memory limit (enforced mode). |
| `--pids COUNT` | none | Process limit (enforced mode). |
| `--max-output SIZE` | none | Limit for stdout and stderr together. |
| `--cgroup-mode MODE` | `auto` | `auto`, `enforced`, or `fallback`. |
| `--cgroup-parent PATH` | none | Delegated cgroup for the groups that corral makes. |
| `--stdin POLICY` | `null` | `null` or `inherit`. |
| `--json PATH` | none | Write the audit record to PATH. |
| `--quiet` | off | Do not print the summary line. |

A DURATION is a number with `ms`, `s`, `m`, or `h`. A SIZE is a number of bytes
with an optional `K`, `M`, or `G`.

With `--json`, corral writes one JSON record for each run. The record gives the
mode, the limits, and why the run ended. It also gives the time of each step
and the pids of any process that did not stop.

## Exit codes

| Code | Meaning |
| --- | --- |
| command's code | The command exited. |
| 128 + N | Signal N stopped the command, or corral received signal N (130 for Ctrl-C). |
| 124 | The time limit expired. |
| 121 | The memory limit was reached. |
| 122 | The output limit was reached. |
| 126, 127 | corral could not start the command (127: not found). |
| 125 | Setup failed or an option is not correct. The command did not start. |
| 120 | corral could not prove that all processes stopped. This code overrides all other codes. |

## How it works

- The command starts in a new session, so a signal to its group never reaches
  corral.
- corral is a child subreaper. Orphaned processes become children of corral,
  so corral can stop them.
- corral signals a process that is not its child only through a pidfd, after
  it checks the process again. A reused pid never gets the signal.
- The run ends when the command exits, not when its output pipes close.
- After each run, corral stops and reaps processes until none are left. In
  enforced mode, the kernel must also report the group empty, and `rmdir` of
  the group must succeed.

## Results

`bench/compare.py` runs ten test programs from `faults/` with four runners:

- CE: corral in enforced mode
- CF: corral in fallback mode
- TO: `timeout -k 1s 2s`
- NV: a Python runner that kills only its child

Each run has a 2 s time limit. Each cell shows the exit status and the largest number of processes
that stayed alive, over 5 runs.

| Test program | CE | CF | TO | NV |
| --- | --- | --- | --- | --- |
| F1 prints a line every 100 ms | 124 / 0 | 124 / 0 | 124 / 0 | 137 / 0 |
| F2 waits for stdin | 0 / 0 | 0 / 0 | 124 / 0 | 137 / 0 |
| F3 starts a background job | 0 / 0 | 0 / 0 | 0 / 1 | hung / 1 |
| F4 starts a daemon and exits | 0 / 0 | 0 / 0 | 0 / 1 | hung / 1 |
| F5 starts a daemon and stays | 124 / 0 | 124 / 0 | 124 / 1 | hung / 1 |
| F6 ignores SIGTERM | 124 / 0 | 124 / 0 | 137 / 0 | 137 / 0 |
| F7 forks 200 children | 124 / 0 | 124 / 0 | 124 / 1 | hung / 192 |
| F8 child holds stdout | 0 / 0 | 0 / 0 | 0 / 1 | hung / 1 |
| F9 uses 256 MiB | 121 / 0 | 124 / 0 | 124 / 0 | 137 / 0 |
| F10 cleans up on SIGTERM | 124 / 0 | 124 / 0 | 124 / 0 | 137 / 0 |

corral left no process alive in any test. CE also stopped F9 at its 64 MiB
limit in 0.12 s. TO left a process alive in F7 in 1 of 5 runs. On this machine,
`timeout` is from uutils coreutils 0.8.0, not GNU coreutils.

`bench/bench.py` measured these times (P50 / P95):

| Measurement | Enforced | Fallback |
| --- | --- | --- |
| Stop F5: time limit to verified | 9 / 12 ms | 10 / 12 ms |
| Run `/bin/true` (direct: 0.83 / 1.88 ms) | 11.33 / 19.47 ms | 6.64 / 10.01 ms |

Test machine: Hetzner cloud server, 2 shared vCPUs (Intel Xeon, Skylake),
Ubuntu 26.04, kernel 7.0. The full data and a terminal recording of
`scripts/demo.sh` are in `bench/results/`.

## Limits

- corral cannot see work that the command starts outside its process tree,
  for example with `systemd-run`, D-Bus, or `at`.
- In enforced mode, a process can leave the group if it writes to cgroupfs
  itself.
- In fallback mode, corral cannot signal a setuid child. It then exits with 120.
- If you stop corral with SIGKILL, only the direct child is sure to stop.
- corral has no PTY support and no CPU time limit, and it runs only on Linux.

## License

MIT. See [LICENSE](https://github.com/Cardinal44/corral/blob/main/LICENSE).
CG144196
🟧 echo.github ⭐Corral runs a command with a time limit and verifies β€” exiting 120 if it cannot prove it β€” that no process the command started is still alivCG144 (GitHub: Cardinal44)β€”β€”

Interpretation history

Decision trace