You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
run-claude-cli: a process a leg detaches outlives the leg's timeout #799
When a leg times out or overflows, run-claude-cli kills the CLI, but a process the leg detached (for example setsid nohup ... & from a Bash tool call) keeps running until the job ends. --die-with-parent (legCommand, lane.cjs) sets PR_SET_PDEATHSIG on the one process bwrap execs. prctl(2) clears that setting in a forked child, so it never reaches the CLI's own children. Before #798 the CLI was spawned directly, and the same gap existed.
Reproduced locally (bwrap 0.11.1): a fake CLI that spawns a detached: true child and then times out under the leg's bwrap. The child wrote its marker 3 s after the leg was killed.
Impact: the survivor holds no credential. legEnv sets CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1, and the hosted probe on #798 recorded credential-in-subprocess= (empty). On a GitHub-hosted runner it dies when the job ends. It can still run past the leg's timeout inside the job.
Why --unshare-pid alone does not work
Adding --unshare-pid to the leg's bwrap argv kills the detached child (a test showed this). But with --dev-bind / /, /proc is still the host's procfs inside the new namespace, so a bwrap nested in the leg fails:
$ bwrap --die-with-parent --unshare-pid --dev-bind / / -- bwrap --die-with-parent --unshare-pid --unshare-net --ro-bind / / --proc /proc --dev /dev -- sh -c 'echo ok'
bwrap: open /proc/3/ns/ns failed: No such file or directory
The same nested call without the outer --unshare-pid works. The CLI's Bash sandbox is a nested bwrap like this one, so the flag as tried would break every Bash-enabled lane.
Next step
Try --unshare-pid together with --proc /proc on the outer bwrap, or another way to reap the leg's process tree, such as a cgroup or killing the process group. Prove it on a hosted runner with the run-claude-cli-ro-binds-probe workflow in pr-pipeline-sandbox (#173 shape), adding a detached-child check, before the change lands.
Design decision for this issue (GitHub Security & Cost with Lane Funding, 2026-10-20, under the owner's overnight procedure):
Run the API-key leg and the subscription leg as separate jobs, not as two legs of one step. Draft #801 (a post-leg process reap) is superseded and will be closed when the split PR opens.
Why. A same-job reap only covers processes. Within one job, leg 1 shares the runner user, the filesystem and the launcher's environment with leg 2, so isolation would also need a private pid namespace, a fresh HOME and workspace, a fixed root-owned PATH and absolute tool paths, and blocked schedulers. --unshare-pid on the outer bwrap already broke the CLI's nested bwrap in the v0.53.0 work. Separate jobs get that isolation from the platform: "each GitHub-hosted runner is a new virtual machine" (github-hosted-runners), executing "within ephemeral and clean isolated virtual machines" (secure-use).
Shape.
run-claude-cli runs one leg per invocation: either the key or the token, never both, and there is no in-step fallback.
Job B (token) runs only when job A's lane-class output is credit-stop. The output is set by the launcher after it truncates the runner's output files. Job B validates it against a closed enum and never interpolates it into run:. A forged credit stop costs only subscription usage; that residual is accepted.
Job B runs on a fixed GitHub-hosted label (ubuntu-24.04), never a caller-supplied runner. It does a fresh checkout, reads only trusted inputs, and does no cache restore and no artifact download from job A.
The split lives inside the reusable workflow where one exists (intake-triage), so callers change once. pr-verify gets the same two-job shape on its side.
Lane outcome reporting reads both jobs, so a credit stop followed by a leg-2 success is one lane record. Write lanes take the patch from whichever job ran.
verifyManagedSettings also requires allowManagedHooksOnly or disableAllHooks.
Not removed by this design: job B still reads the lane's untrusted text, so prompt injection under the token stays bounded by the lane's tool limits. That limit applies equally to the single-job design.
Activity
kyle-sexton commentedon Oct 20, 2026
Design decision for this issue (GitHub Security & Cost with Lane Funding, 2026-10-20, under the owner's overnight procedure):
Run the API-key leg and the subscription leg as separate jobs, not as two legs of one step. Draft #801 (a post-leg process reap) is superseded and will be closed when the split PR opens.
Why. A same-job reap only covers processes. Within one job, leg 1 shares the runner user, the filesystem and the launcher's environment with leg 2, so isolation would also need a private pid namespace, a fresh HOME and workspace, a fixed root-owned PATH and absolute tool paths, and blocked schedulers.
--unshare-pidon the outer bwrap already broke the CLI's nested bwrap in the v0.53.0 work. Separate jobs get that isolation from the platform: "each GitHub-hosted runner is a new virtual machine" (github-hosted-runners), executing "within ephemeral and clean isolated virtual machines" (secure-use).Shape.
run-claude-cliruns one leg per invocation: either the key or the token, never both, and there is no in-step fallback.lane-classoutput iscredit-stop. The output is set by the launcher after it truncates the runner's output files. Job B validates it against a closed enum and never interpolates it intorun:. A forged credit stop costs only subscription usage; that residual is accepted.ubuntu-24.04), never a caller-supplied runner. It does a fresh checkout, reads only trusted inputs, and does no cache restore and no artifact download from job A.verifyManagedSettingsalso requiresallowManagedHooksOnlyordisableAllHooks.Not removed by this design: job B still reads the lane's untrusted text, so prompt injection under the token stays bounded by the lane's tool limits. That limit applies equally to the single-job design.
fix(run-claude-cli): restore step files by content and pin the CLI ve…