fix(claude_code): scope the pinned claude bin dir to the env -i blocks that spawn claude

The npm install's node_modules/.bin was prepended to PATH at script
scope, so every later step (git, gh with the agent token, curl, uv
sync, the docs publish) ran with package-controlled binaries first on
PATH; a compromised dependency shipping a bin named git or gh would
have executed with the full systemd-injected token environment. The
bin dir is now captured once as CLAUDE_CLI_BIN and prepended only
inside the two env -i allowlists that need claude: the version probe
and the pytest invocation (cli_driver re-derives its allowlist from
the pytest env). A structural test pins the exactly-two prepends and
rejects any script-scope PATH reassignment. Anchor lookups in the
touched test files now fail via pytest.fail naming the missing anchor
instead of raising a bare ValueError.
This commit is contained in:
mateo-berri 2026-07-15 17:35:58 -07:00
parent 8220c57468
commit d23f919e2c
3 changed files with 66 additions and 13 deletions

View file

@ -17,6 +17,9 @@ flow in run_daily.sh:
the supply-chain vector the 3-day buffer exists for.
3. The `claude --version` probe verifies the binary now on PATH
reports exactly the resolved version and dies on a mismatch.
4. The install's bin dir reaches PATH only inside the two `env -i`
blocks that spawn `claude` (probe and pytest), never at script
scope where later git/gh/curl/uv steps run with the token env.
The resolve/install/probe block is extracted out of run_daily.sh and
executed with a stub resolver and a fake `npm`, mirroring the fake-curl
@ -32,6 +35,8 @@ import textwrap
from dataclasses import dataclass
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[4]
RUN_DAILY = REPO_ROOT / "tests" / "e2e" / "claude_code" / "cron_vm" / "run_daily.sh"
@ -52,11 +57,25 @@ _SECRET_ENV = {
}
def _anchor(text: str, needle: str, start: int = 0) -> int:
found = text.find(needle, start)
if found == -1:
pytest.fail(f"run_daily.sh anchor not found: {needle!r}")
return found
def _extract_block(text: str, start_marker: str, end_marker: str) -> str:
start = _anchor(text, start_marker)
end = _anchor(text, end_marker, start)
return text[start:end]
def _extract_pin_snippet() -> str:
body = RUN_DAILY.read_text()
start = body.index('CLAUDE_PROBE_HOME="${WORKDIR}/claude-probe-home"')
end = body.index('log "pinned claude code:')
return body[start:end]
return _extract_block(
RUN_DAILY.read_text(),
'CLAUDE_PROBE_HOME="${WORKDIR}/claude-probe-home"',
'log "pinned claude code:',
)
def _executable_lines(block: str) -> str:
@ -233,11 +252,11 @@ def test_probe_dies_when_installed_version_mismatches_resolved(
def test_static_resolver_and_install_are_env_i_wrapped() -> None:
body = RUN_DAILY.read_text()
lines = _executable_lines(_extract_pin_snippet())
resolver_call = lines.index("pr_gate_version_resolver.py")
resolver_call = _anchor(lines, "pr_gate_version_resolver.py")
assert "env -i" in lines[:resolver_call], (
"run_daily.sh must run pr_gate_version_resolver.py under `env -i`"
)
install_call = lines.index('@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}')
install_call = _anchor(lines, "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}")
assert "env -i" in lines[resolver_call:install_call], (
"run_daily.sh must run the npm install under its own `env -i`"
)
@ -249,8 +268,33 @@ def test_static_resolver_and_install_are_env_i_wrapped() -> None:
assert required in _required_commands(body)
def test_static_npm_bin_dir_scoped_to_probe_and_pytest_env_blocks() -> None:
body = RUN_DAILY.read_text()
_anchor(body, 'CLAUDE_CLI_BIN="${CLAUDE_CLI_PREFIX}/node_modules/.bin"')
prepend = 'PATH="${CLAUDE_CLI_BIN}:${PATH}"'
assert body.count(prepend) == 2, (
"run_daily.sh must prepend the npm install's bin dir in exactly "
"the two `env -i` blocks that spawn claude (version probe and "
"pytest); every other step (git, gh, curl, uv, docs publish) "
"must not see package-controlled binaries on PATH."
)
probe_block = _extract_block(
body, "PROBED_CLAUDE_VERSION=", '[[ -n "${PROBED_CLAUDE_VERSION}" ]]'
)
pytest_block = _extract_block(body, 'log "running pytest"', "PYTEST_EXIT=$?")
assert prepend in probe_block
assert prepend in pytest_block
for line in body.splitlines():
assert not line.startswith(("PATH=", "export PATH=")), (
f"run_daily.sh reassigns PATH at script scope: {line!r}. A "
f"compromised npm dependency shipping a bin named `git` or "
f"`gh` would then execute in later steps with the full "
f"systemd-injected token environment."
)
def _required_commands(body: str) -> tuple[str, ...]:
marker = "for cmd in "
start = body.index(marker) + len(marker)
end = body.index(";", start)
start = _anchor(body, marker) + len(marker)
end = _anchor(body, ";", start)
return tuple(body[start:end].split())

View file

@ -17,14 +17,23 @@ from __future__ import annotations
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[4]
RUN_DAILY = REPO_ROOT / "tests" / "e2e" / "claude_code" / "cron_vm" / "run_daily.sh"
def _anchor(text: str, needle: str, start: int = 0) -> int:
found = text.find(needle, start)
if found == -1:
pytest.fail(f"run_daily.sh anchor not found: {needle!r}")
return found
def _version_probe_block() -> str:
body = RUN_DAILY.read_text()
start = body.index("PROBED_CLAUDE_VERSION=")
end = body.index('[[ -n "${PROBED_CLAUDE_VERSION}" ]]', start)
start = _anchor(body, "PROBED_CLAUDE_VERSION=")
end = _anchor(body, '[[ -n "${PROBED_CLAUDE_VERSION}" ]]', start)
return body[start:end]

View file

@ -208,10 +208,10 @@ env -i \
TMPDIR="${TMPDIR:-/tmp}" \
npm install --prefix "${CLAUDE_CLI_PREFIX}" "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|| die "npm install of @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} failed"
PATH="${CLAUDE_CLI_PREFIX}/node_modules/.bin:${PATH}"
CLAUDE_CLI_BIN="${CLAUDE_CLI_PREFIX}/node_modules/.bin"
PROBED_CLAUDE_VERSION="$(env -i \
PATH="${PATH}" \
PATH="${CLAUDE_CLI_BIN}:${PATH}" \
HOME="${CLAUDE_PROBE_HOME}" \
USER="${USER:-mateo}" \
TERM="${TERM:-dumb}" \
@ -422,7 +422,7 @@ set +e
(
cd "${WORKTREE}" \
&& env -i \
PATH="${PATH}" \
PATH="${CLAUDE_CLI_BIN}:${PATH}" \
HOME="${HOME}" \
USER="${USER:-mateo}" \
TERM="${TERM:-dumb}" \