Skip to content

Install subprocess isolation dependencies: a hanging apt-get consumes the whole step timeout despite continue-on-error #1694

Description

@NubeBuster

Summary

The Install subprocess isolation dependencies step in action.yml is declared best-effort (continue-on-error: true, "Best-effort: skips on non-Linux or when sudo/apt unavailable"), but a hanging apt-get — as opposed to a failing one — still kills the entire job. continue-on-error only tolerates a non-zero exit; it does nothing about a step that never exits. The step silently consumes the caller's whole timeout-minutes budget and Claude never runs.

- name: Install subprocess isolation dependencies
# Install subprocess isolation dependencies when processing content from non-write users.
# Best-effort: skips on non-Linux or when sudo/apt unavailable (self-hosted runners).
if: ${{ inputs.allowed_non_write_users != '' && runner.os == 'Linux' }}
continue-on-error: true
shell: bash
run: |
if [ "${CLAUDE_CODE_SUBPROCESS_ENV_SCRUB:-}" = "0" ]; then
echo "Subprocess isolation opted out via CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0"
exit 0
fi
if command -v apt-get >/dev/null && command -v sudo >/dev/null; then
for i in 1 2 3; do
sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break
echo "apt-get attempt $i failed, retrying..."
sleep 5
done
fi
# Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
# The sysctl doesn't exist on older kernels — that's fine.
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ] && command -v sudo >/dev/null; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi

The retry loop does not help here for the same reason:

for i in 1 2 3; do
  sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break
  echo "apt-get attempt $i failed, retrying..."
  sleep 5
done

If a mirror accepts the connection and then stalls, apt-get update never returns, the && never resolves, and the loop never reaches iteration 2. The retry covers the fast-failure case only, which is the less likely one on GitHub-hosted runners.

Evidence

This is currently firing in anthropics/claude-code's own Claude Issue Triage workflow, which uses anthropics/claude-code-action@v1 with allowed_non_write_users: "*" (so the step is enabled) and timeout-minutes: 5 on the action step.

Of the last 100 runs of that workflow, 10 failed, and all 10 are this exact signature — the step burning the full 5-minute budget with zero output, not even the apt-get attempt 1 failed, retrying... line:

run __run_3 duration
32091441430 309652 ms
32090700246 309812 ms
32089934518 309963 ms
32089473766 309488 ms
32088750662 309338 ms
32088382552 309644 ms
32088203619 310028 ms
32087190931 310391 ms
32086961767 309849 ms
32084523998 310061 ms

Log excerpt from 32088203619:

01:26:19.5357Z start-action display=Install subprocess isolation dependencies;id=__anthropics_claude-code-action.__run_3
01:26:19.5432Z ##[endgroup]
                (no output for 310 seconds)
01:31:29.5634Z ##[error]The action has timed out.
01:31:29.5640Z end-action id=...__run_3;outcome=failure;conclusion=success;duration_ms=310028

Every subsequent step in the composite action is then reported as The action has timed out., which makes the run look like seven distinct failures when there is one cause. The bun install of the agent SDK completed normally 1 s earlier, so general network egress on the runner was fine — it is specific to the apt mirror.

Impact

Beyond the lost run, the failure notification lands on whoever triggered it. claude-issue-triage.yml runs on: issue_comment, so an external commenter with no visibility into or control over the workflow gets mailed a "workflow failed" notice for infrastructure they cannot touch. At a 10% flake rate on a busy public issue tracker, that is a fair amount of noise pushed onto third parties. (I hit this by commenting on anthropics/claude-code#87487.)

Suggested fix

Wrap the apt-get calls so a hang degrades into the failure the retry loop and continue-on-error are already designed to absorb:

if command -v apt-get >/dev/null && command -v sudo >/dev/null; then
  for i in 1 2 3; do
    timeout 60 sudo apt-get update -qq \
      && timeout 120 sudo apt-get install -y --no-install-recommends bubblewrap socat \
      && break
    echo "apt-get attempt $i failed or timed out, retrying..."
    sleep 5
  done
fi

Worst case then becomes ~9 minutes of retries, so pairing it with an explicit timeout-minutes: on the step itself (e.g. timeout-minutes: 4) would bound it regardless — with continue-on-error: true already set, a step-level timeout is non-fatal and the job proceeds to run Claude unsandboxed, which is the documented best-effort behaviour.

Related but distinct: #1420 covers when this step runs (gating on allowed_non_write_users), not its robustness once it does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:installationbugSomething isn't workingp2Non-showstopper bug or popular feature request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions