Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: The lock pins the agent runtime, and the image stops installing a second CLI
status: implemented
date: 2026-11-02
scope: hookprobe
---

## Decision

The investigator's image no longer installs the Claude Code CLI from npm, and
the `CLAUDE_CODE_VERSION` build argument is gone. The CLI that runs an
investigation is the one inside the claude-agent-sdk wheel, so the
`claude-agent-sdk==` line in `hookprobe/requirements.lock` is the agent-runtime
pin, and Dependabot proposes it now that its strategy for this service raises
floors. Node stays, for MCP servers started with npx.

## Why

The SDK runs its bundled binary before it looks at PATH, and nothing here sets
`cli_path`. Read inside the images: the published 0.9.2 investigator ran CLI
2.1.235, bundled with SDK 0.2.140, while the npm CLI pinned at 2.1.259 sat on
PATH and never ran. After the relock to SDK 0.2.176 the engine runs 2.1.316.
The pin this Dockerfile documented had decided nothing since the SDK began
bundling the CLI, and it was still read as the answer: the 2026-09-03 fix for a
gateway's model ids raised it and changed nothing that ran.

The other way out was to point the engine at the pinned npm CLI with
`cli_path`. That keeps a version Dependabot cannot see, runs a CLI older than
the one each SDK release was built and tested with, and keeps two pins that
have to agree. The operator chose the lock.

## Consequences

- A relock of claude-agent-sdk is a CLI bump. The SDK's changelog names the
CLI each release bundles, and `claude_agent_sdk/_cli_version.py` names it
inside an image.
- `claude` is no longer on PATH in the image, so `claude --version` there
fails; read the version file instead.
- The image lost the npm CLI: 1.69 GB before and 1.17 GB after, in a local
build of the same lock.
- An operator who needs a CLI release the SDK has not shipped yet has no build
argument any more. `ClaudeAgentOptions.cli_path` is where that would go, and
it stays unwired until someone needs it.
- Check every SDK bump against a gateway's own model ids: a CLI release once
refused them with `[claude-code:unrecognized_model]`. 2.1.235 and 2.1.316
both accept them.
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ commands.
`CLAUDE_CODE_VERSION`. The SDK looks for its bundled binary first. The 0.9.2
image ran 2.1.235 with 2.1.259 unused on PATH, and this relock moves the
engine to 2.1.316. So the lock, not the build argument, has been pinning the
agent runtime since the SDK began bundling it. Whether the image should drop
the npm CLI or point the engine at it is the operator's call.
agent runtime since the SDK began bundling it. The operator chose the lock
the same day: see 2026-11-02-the-lock-pins-the-agent-runtime.
6 changes: 4 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ updates:
# The base images were the one thing nothing nagged: python:3.14-slim /
# python:3.12-slim / node:22-alpine drift silently, and a base with a fixed
# CVE never arrives without someone bumping the tag. One entry per Dockerfile,
# same one-PR-per-service reasoning as the pip groups above. The pinned tool
# versions inside (CLAUDE_CODE_VERSION, nodesource) stay manual by design.
# same one-PR-per-service reasoning as the pip groups above. The nodesource
# version inside stays manual by design. The agent's CLI is not installed
# there any more: it ships inside claude-agent-sdk, which the investigator's
# pip entry proposes.
- package-ecosystem: docker
directory: "/hookrelay"
schedule:
Expand Down
27 changes: 14 additions & 13 deletions hookprobe/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
# hookprobe: Python runtime + Node (the Claude Agent SDK drives the Claude
# Code CLI) + a lean read-only diagnostic core. Domain CLIs (kubectl, db
# clients) stay opt-in below — extend with exactly what your alerts need.
# hookprobe: Python runtime + Node (for MCP servers started with npx, see
# deploy/mcp.example.json) + a lean read-only diagnostic core. Domain CLIs
# (kubectl, db clients) stay opt-in below — extend with exactly what your
# alerts need.
FROM python:3.14-slim

ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
NPM_CONFIG_UPDATE_NOTIFIER=false

# The CLI is the agent runtime itself. Unpinned, every image rebuild
# silently adopts whatever shipped upstream that day — the one dependency
# where drift changes behaviour most, pinned last. Bump deliberately:
# docker compose build --build-arg CLAUDE_CODE_VERSION=x.y.z
# 2.1.259: 2.1.232 rejected third-party model names its catalog did not know
# (`[claude-code:unrecognized_model]` on e.g. gpt-5.6-luna behind a custom
# ANTHROPIC_BASE_URL), which broke the investigator against gateways that serve
# only their own model ids. 2.1.259 accepts them.
ARG CLAUDE_CODE_VERSION=2.1.259
# The agent's CLI is not installed here. The Claude Agent SDK wheel carries its
# own build of the Claude Code CLI and runs that one before anything on PATH,
# so the CLI that runs an investigation is the one requirements.lock pins
# through claude-agent-sdk (claude_agent_sdk/_cli_version.py names it), and
# Dependabot proposes it. A second CLI installed here was dead weight that read
# as the pin: the npm build this file pinned at 2.1.259 sat unused on PATH
# while 0.9.2 ran the SDK's 2.1.235. Bump the agent runtime by bumping the SDK,
# and check the new one still takes a gateway's own model ids: a CLI release
# once refused them (`[claude-code:unrecognized_model]` behind a custom
# ANTHROPIC_BASE_URL). 2.1.235 and 2.1.316 both accept them.

# The diagnostic core: what any investigation reaches for first, and what
# marketplace runbooks assume exists (a patrol skill mis-read this container
Expand All @@ -26,7 +28,6 @@ RUN apt-get update \
procps jq iproute2 dnsutils netcat-openbsd lsof \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
&& apt-get purge -y gnupg \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
Expand Down
5 changes: 3 additions & 2 deletions hookprobe/deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ services:
# analysis must not be able to take the host with it, and a compromised
# run must find no privilege ladder. Measured before this existed: no
# memory cap, no pid cap, full default capabilities.
# - mem_limit: the SDK CLI is a node process; 2g is roomy for one run
# plus subagents (a real investigation peaks well under 1g).
# - mem_limit: the agent is one native CLI process the SDK ships; 2g is
# roomy for one run plus subagents (a real investigation peaks well
# under 1g).
# - pids_limit: a fork bomb hits a wall at 512 instead of the host limit.
# - cap_drop ALL: an investigator only opens sockets and reads files;
# it never chowns, mounts, raw-sockets or ptraces. Costs `ping`
Expand Down
Loading