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:
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.
Summary
The
Install subprocess isolation dependenciesstep inaction.ymlis declared best-effort (continue-on-error: true, "Best-effort: skips on non-Linux or when sudo/apt unavailable"), but a hangingapt-get— as opposed to a failing one — still kills the entire job.continue-on-erroronly tolerates a non-zero exit; it does nothing about a step that never exits. The step silently consumes the caller's wholetimeout-minutesbudget and Claude never runs.claude-code-action/action.yml
Lines 225 to 247 in 459ad35
The retry loop does not help here for the same reason:
If a mirror accepts the connection and then stalls,
apt-get updatenever 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 ownClaude Issue Triageworkflow, which usesanthropics/claude-code-action@v1withallowed_non_write_users: "*"(so the step is enabled) andtimeout-minutes: 5on 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_3durationLog excerpt from 32088203619:
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.ymlrunson: 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-errorare already designed to absorb: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 — withcontinue-on-error: truealready 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.