Skip to content

fix(pr-review): install sandbox-runtime's seccomp helper for the lanes - #1029

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/install-seccomp-helper
Nov 8, 2026
Merged

kyle-sexton merged 2 commits into
mainfrom
fix/install-seccomp-helper

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Refs: #938

Summary

The Claude review lanes on the melodic-review Docker workers have no working shell. Every Bash call fails with bwrap: cannot open /proc/sys/user/max_user_namespaces: Read-only file system. This PR installs sandbox-runtime's apply-seccomp helper in both lanes. With the helper present, Claude Code no longer asks bubblewrap for --disable-userns.

The CI Runner session found the cause in ci-runner#528 (comment). I re-read each cited source line myself, at the versions concerned:

  • bubblewrap's --disable-userns opens the sysctl for writing and dies when it cannot. bubblewrap.c:3395-3402 at 1fb0aa2:
    if (opt_disable_userns)
      {
        cleanup_fd int sysctl_fd = -1;
        sysctl_fd = TEMP_FAILURE_RETRY (openat (proc_fd, "sys/user/max_user_namespaces", O_WRONLY));
        if (sysctl_fd < 0)
          die_with_error ("cannot open /proc/sys/user/max_user_namespaces");
  • sandbox-runtime passes that flag only when no helper is in use. linux-sandbox-utils.ts:3847-3859 at 27fce0d:
    // With no helper to refuse user namespaces, bubblewrap is asked to. Never
    // beside the helper, which makes one of its own, and not under
    // enableWeakerNestedSandbox: bubblewrap does it by writing a sysctl, and
    // dies on the read-only /proc/sys of an unprivileged container.
    if (
      applySeccompPrefix === undefined &&
      allowNestedUserNamespaces !== true
    ) {
    ...
        bwrapArgs.push('--disable-userns')
    When the helper is missing, it logs (L3624-L3629): apply-seccomp binary not available - unix socket blocking disabled. Install @anthropic-ai/sandbox-runtime globally for full protection.
  • A native Claude Code build finds the helper only under a global npm root. It first checks the bundle's own vendor directory. It then calls findGlobalApplySeccompPath(getGlobalNpmPaths()) (generate-seccomp-filter.ts:202-206). That joins <root>/vendor/seccomp/<arch>/apply-seccomp (L283-L299). The roots are npm root -g plus static paths that include join('/usr', 'local', 'lib', 'node_modules', '@anthropic-ai', 'sandbox-runtime') (L76-L97). Claude Code's sandboxing docs say the same: "Install it with npm install -g @anthropic-ai/sandbox-runtime if it is missing."
  • The helper's own sysctl write is not fatal. apply-seccomp.c:884-896: "Not fatal: where /proc could not be mounted above, /proc/sys is commonly read-only, and the filter below refuses the same calls."
  • The first failing release is claude-code-action v1.0.254. The v1.0.253...v1.0.254 compare changes src/entrypoints/run.ts from const claudeCodeVersion = "2.1.303"; to "2.1.304". I counted the string --disable-userns in each @anthropic-ai/claude-code-linux-x64 binary from the npm registry: 2.1.303 has 0, 2.1.304 has 3, and 2.1.319 has 3. 2.1.319 is the version the latest release, v1.0.269, installs (run.ts:80 at 61a9c27).
  • The Linux sandbox code is the same in v0.0.81 and in the cited commit. The v0.0.81...27fce0d compare touches only Windows, proxy and CI files, not linux-sandbox-utils.ts, generate-seccomp-filter.ts or apply-seccomp.c. The behavior came in with sandbox-runtime#588, merged 2026-10-14, before v0.0.80. The v0.0.81 helper binary carries SRT_ALLOW_NESTED_USERNS, the variable Claude Code's bundled runtime sets in front of it.

Fix

The Install subprocess isolation step in pr-review.yml and pr-review-security.yml now does the following. The two copies stay byte-identical, and the existing test holds them equal.

  1. It downloads https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.81.tgz, the latest stable release (npm view dist-tags latest: 0.0.81, published 2026-10-20).
  2. It checks the tarball against a pinned SHA-256, 0e0b8249…953f. Integrity follows the repo's pinned-download pattern (.github/actions/_shared/install-release.sh). I checked the pinned artifact three ways:
    • Its SHA-512 equals the registry's dist.integrity (sha512-bjHDu87g…W/w==).
    • npm audit signatures on an install of 0.0.81 reports "5 packages have verified registry signatures" and "2 packages have verified attestations".
    • sandbox-runtime itself carries SLSA provenance.
  3. It extracts only package/vendor/seccomp/<arch>/apply-seccomp. The binary is statically linked. It installs the binary root-owned, mode 0755, at /usr/local/lib/node_modules/@anthropic-ai/sandbox-runtime/vendor/seccomp/<arch>/apply-seccomp.

Why not npm install -g: the worker image has no npm. ci-runner's docs/worker-image.md says "node, npm, npx) are absent from PATH so workflows must install them with their official setup actions". A global npm install would need a setup-node step, which would put Node in the model's Bash environment, and it would pull four caret-ranged dependencies the helper does not use. The static /usr/local/lib root is searched whether or not npm is on PATH.

The step timeout goes from 1 to 2 minutes. The download is bounded at 3 attempts of 15 s, so the apt deadline (50 s) plus the download still fits. The helper install is best-effort. A failed download, a SHA-256 mismatch, an unsupported architecture or a failed extract installs nothing and emits a ::warning:: that names the cause. The step then continues, so a registry outage cannot stop a lane. Without the helper, Claude Code behaves as it does today. A binary that failed its hash is never installed.

Left out: pr-review-cli.yml. Its model has Bash disallowed (header, lines 120-123: "Bash and the write and web tools are disallowed"), so no sandboxed command runs there.

Verification

  • After the best-effort change, node --test on .github/scripts/claude-*.test.cjs, lane-outcome-emitters.test.cjs and pr-review-cli.test.cjs: 333 pass, 0 fail.

  • Two new tests in claude-lane-install-isolation.test.cjs run the step's own script with stub curl and sudo:

    • The helper lands under one of sandbox-runtime's searched roots, through sudo install -D -o root -g root -m 0755, from the 0.0.81 registry URL.
    • A tarball that does not match the pinned SHA-256 installs nothing, warns, and the step still exits 0.

    Each test failed when I broke the code on purpose: the helper path moved to /opt/tools, and the SHA-256 check turned off.

  • I ran the step's script against the live registry with the real pin, with only sudo stubbed. It exited 0. The installed helper's SHA-256 (7effeb0f…ca24a) equals the one in a plain npm pack of 0.0.81.

  • actionlint (only the known $/ noise), zizmor --offline (no findings) and biome ci on the test: clean.

Not yet verified: a review lane on a melodic-review worker with SRT_DEBUG=1 on the latest claude-code-action. That run would show bwrap starting without --disable-userns, Bash calls succeeding, and whether the helper's nested unshare passes ci-runner's userns seccomp profile. pr-pipeline-sandbox cannot run it: the repository is public, and every org runner group has allows_public_repositories: false (runner groups 4 and 5 list only private repositories). Until that run passes, the claude-code-action v1.0.253 pin and the Dependabot ignore at .github/dependabot.yml:34 stay, and the issue stays open. When the pin is lifted, note that v1.0.269 is inside the 3-day Dependabot cooldown until 2026-11-09.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_015XWwmbdW8moDiQ5Rtgt7d5

Claude Code 2.1.304 and later pass bubblewrap --disable-userns when no
apply-seccomp helper is found. That flag writes
/proc/sys/user/max_user_namespaces, which Docker mounts read-only on the
melodic-review workers, so every Bash call in a review lane fails.

The isolation step in both lanes now downloads the pinned
@anthropic-ai/sandbox-runtime 0.0.81 tarball from the npm registry,
checks its SHA-256, and installs only the helper, root-owned, under
/usr/local/lib/node_modules, a root sandbox-runtime searches. The
worker image has no npm, so this needs no Node toolchain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWwmbdW8moDiQ5Rtgt7d5
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-08T06:01:49.204083Z
We couldn’t complete this review. Comment @codex review to try again.
eeb4684 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

claude-security-review reviewed d8087e5ceba23a2a7b2d940f33f3714d6c1bed67: 0 finding(s), 0 as inline comments.

Security review: incremental, d8087e5

I found no security issues in this change. I reviewed only the diff since eeb4684 in pr-review.yml, pr-review-security.yml and claude-lane-install-isolation.test.cjs.

What changed: the "Install subprocess isolation" step used to fail the lane if the seccomp helper (apply-seccomp) could not be installed. It now installs the helper on a best-effort basis. These cases now warn and skip: an unsupported runner architecture, a failed download, a SHA-256 mismatch, and a failed extract or install. The test was updated to match.

Checks:

  • Tarball integrity: the SHA-256 check still runs before any tar or sudo install. A tampered or mismatched tarball is never extracted or installed. The change only downgrades the failure from an error to a warning, and the warning still names the cause. Nothing attacker-controlled is installed.
  • Environment scrubbing: the hard failure for missing bwrap/socat is unchanged (pr-review-security.yml around line 713), and CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 stays set on the model step. The credential scrub for Bash subprocesses therefore still fails closed.
  • Namespace probe: check-subprocess-sandbox still runs after the install step. It still gates the model step on whether bwrap can create a namespace.
  • Helper absence: a missing helper puts Claude Code back in the state it was in before this helper was introduced. The workflow comment says that without it Claude Code passes --disable-userns. That either works with less seccomp hardening or fails every Bash call, and the probe catches a failure. It adds no new reachable attacker path, token scope, secret exposure or charge. It is a deliberate availability-over-hardening tradeoff for the lane. This is not a trust-control widening (not the trusted-trigger check, token scope, kill switch, merge hold or untrusted text reaching a credentialed lane).
  • Shell variables: url, helper, member and skip are built from RUNNER_ARCH, the pinned version and fixed strings. No PR-controlled input reaches them, and all are quoted. When arch is empty the later branches are skipped, so the empty-arch helper path is never used.
  • Test file: it only changes assertions to match the new warn-and-continue behavior, with no security surface.

manifest fields: none touched.
critical-findings: 0

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

claude-review reviewed d8087e5ceba23a2a7b2d940f33f3714d6c1bed67: 0 finding(s), 0 as inline comments.

Review (incremental, since eeb4684)

No findings. I read the incremental diff and the surrounding step and test at head. I did not run the tests myself.

What changed. The seccomp-helper install in pr-review.yml and pr-review-security.yml is now best-effort. An unsupported runner architecture, a failed download, a SHA-256 mismatch, or a failed extract or install each emit ::warning:: and leave the step green with nothing installed. The test for the SHA-256 mismatch case now expects exit 0 and a ::warning:: line.

What I checked

  • Chain and set -e. The if/elif chain is correct. Conditions inside if/elif are exempt from set -e, so a failing curl, sha256sum, tar, or sudo install falls through to its warning instead of aborting. The rm -rf -- "${work:?}" cleanup runs on every path.
  • Empty $arch. When $arch is empty, helper and member are built with an empty segment. They are never used, because the first branch only warns.
  • Workflow copies. The two workflow copies are identical in the diff, and the existing equality test still covers them.
  • Test fragment. The test slices the step at case "$RUNNER_ARCH" in. url, helper, member and skip are all assigned after that point, so the fragment is self-contained. The ^::warning::… regex with the mu flags matches the new message. The /^sudo /mu negative assertion still holds, because sudo is only reached after the SHA-256 check passes.
  • Failure classification. The isolate step still fails when bwrap or socat is missing, so install-failed classification is still reachable. The later namespace probe still catches the case where the helper is missing and bwrap then dies on --disable-userns.

Advisory, not counted as findings

  • PR description is stale. The ## Fix section still says "A failed download or a SHA-256 mismatch fails the step with a named ::error::", and the ## Verification bullet says a mismatch "installs nothing". The code now warns and exits 0. Update the description so the record matches the behavior.
  • Untested warning paths. Only the SHA-256 mismatch warning has a test. The unsupported-architecture, download-failure and extract/install-failure branches have none. They are simple and low risk.

Closing issues. The PR uses Refs: #938, so there are no closing-issue criteria to check.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

PR body contract — issue linkage

This PR body conforms to the issue-linkage contract. Nothing to do.

@github-actions github-actions Bot added needs-issue-linkage Pull request body has no issue linkage line; set by the PR-contract check, advisory. and removed needs-issue-linkage Pull request body has no issue linkage line; set by the PR-contract check, advisory. labels Nov 8, 2026
A failed download, a hash mismatch, an unsupported architecture or a
failed extract now installs nothing, warns with the cause, and lets the
isolation step continue. Without the helper Claude Code behaves as it
did before, so a registry outage no longer stops every review lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWwmbdW8moDiQ5Rtgt7d5
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 7dd9720 Nov 8, 2026
54 checks passed
@kyle-sexton
kyle-sexton deleted the fix/install-seccomp-helper branch November 8, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant