| name | update_container_re |
| description | Update the Claude Code web container reverse engineering effort. Detects changed binaries, captures new references, runs parallel RE subagents for bindiff/decompilation, updates container snapshot/diff, and refreshes all documentation. |
| argument-hint | [--detect-only] [--skip-re] [--skip-diff] |
| allowed-tools | Bash, Read, Grep, Glob, Edit, Write, Agent, WebSearch, WebFetch |
Claude Code Web Container RE Update
Complete procedure for updating the reverse engineering effort when the live
container has changed. This covers binary updates, full disassembly-based
reverse engineering of deltas, container reconstruction diffing, and
documentation refresh.
Argument: $ARGUMENTS — optional flags to limit scope.
Directory: devinfra/claude/web_env/ in the repo root.
Prerequisite: Phases 1-3 (detection, binary capture, metadata) require
running inside a Claude Code web container with access to the live binaries.
Phase 5 (container build) works from any machine with Docker and network
access — it fetches packages from pinned Ubuntu snapshot archives via
fetch_debs.py, not dpkg-repack.
Phase 1: Detection — What Changed?
Compare live binaries against stored references to determine scope.
LIVE_EM_HASH=$(md5sum /opt/env-runner/environment-manager | awk '{print $1}')
REF_EM_HASH=$(zcat devinfra/claude/web_env/reference/environment-manager.gz | md5sum | awk '{print $1}')
echo "environment-manager: live=$LIVE_EM_HASH ref=$REF_EM_HASH"
LIVE_PA_HASH=$(md5sum /proc/1/exe | awk '{print $1}')
REF_PA_HASH=$(zcat devinfra/claude/web_env/reference/process_api.gz | md5sum | awk '{print $1}')
echo "process_api: live=$LIVE_PA_HASH ref=$REF_PA_HASH"
/usr/local/bin/environment-manager --version 2>&1
If hashes match, the binaries haven't changed — skip binary RE and go to
Phase 5 (container diff) to check for package/config changes only.
Extract key properties of changed binaries:
file /opt/env-runner/environment-manager
readelf -n /opt/env-runner/environment-manager | grep "Build ID"
go version -m /opt/env-runner/environment-manager | head -5
file /proc/1/exe
readelf -n /proc/1/exe | grep "Build ID"
wc -c < /proc/1/exe
strings /proc/1/exe | grep 'process_api_20'
Phase 2: Capture New References
2a. Binaries
cp /proc/1/exe /tmp/process_api_new
gzip -c /tmp/process_api_new > devinfra/claude/web_env/reference/process_api.gz
cp /opt/env-runner/environment-manager /tmp/env-manager-new
gzip -c /tmp/env-manager-new > devinfra/claude/web_env/reference/environment-manager.gz
2b. Version Snapshot
bazel run //devinfra/claude/web_env/tools:capture_versions -- \
> devinfra/claude/web_env/reference/versions-$(date +%Y-%m-%d).yaml
bazel run //devinfra/claude/web_env/tools:capture_versions -- \
--diff devinfra/claude/web_env/reference/versions-YYYY-MM-DD.yaml
2c. Metadata
/usr/local/bin/environment-manager print-sandbox-settings \
> devinfra/claude/web_env/reference/sandbox-settings.json
{
/usr/local/bin/environment-manager --help 2>&1
/usr/local/bin/environment-manager --version 2>&1
/usr/local/bin/environment-manager setup --help 2>&1
/usr/local/bin/environment-manager orchestrator --help 2>&1
/usr/local/bin/environment-manager task-run --help 2>&1
/usr/local/bin/environment-manager poll --help 2>&1
} > devinfra/claude/web_env/reference/subcommands.txt
env | grep -E '^(CLAUDE|CODESIGN|MCP_)' | sort \
> devinfra/claude/web_env/reference/claude-env-vars.txt
Phase 3: Update RE Source In-Place
RE source lives directly under re/process_api/src/ and
re/environment_manager/src/ (flat layout — no BuildID subdirectories).
The current Build ID is documented in each binary's README.md and
PLAN.md, not encoded in directory names.
NEW_EM_BUILDID=$(readelf -n /tmp/env-manager-new | grep 'Build ID' | awk '{print substr($NF,1,8)}')
NEW_PA_BUILDID=$(readelf -n /tmp/process_api_new | grep 'Build ID' | awk '{print substr($NF,1,8)}')
echo "environment-manager: $NEW_EM_BUILDID"
echo "process_api: $NEW_PA_BUILDID"
Phase 4: Parallel RE Subagents
Launch two parallel subagents — one per changed binary. Each works in
isolation (ideally in a worktree) to avoid conflicts.
Subagent 1: environment-manager (Go with DWARF)
The Go binary ships with full debug info. Use go tool objdump for actual
disassembly — not string-level guessing.
Census (run in parallel within subagent):
BIN=/tmp/env-manager-new
go version -m "$BIN"
go tool objdump "$BIN" 2>/dev/null | grep -oP 'TEXT \K\S+' | \
grep -E '(cmd/|internal/)' | sed 's/\..*//' | sort -u
go tool nm "$BIN" | grep -E '^0x[0-9a-f]+ T' | \
grep -E '(cmd\.|internal/)' > /tmp/em-new-functions.txt
strings "$BIN" | sort -u > /tmp/em-new-strings.txt
Diff against old RE:
- Compare source file lists (find new/removed Go files)
- Compare function lists (find new/removed/resized functions)
- Compare dependency versions
- Compare embedded content (install scripts, hook templates)
Reconstruction: For every new or significantly changed function:
go tool objdump -s 'package.FunctionName' "$BIN" — full annotated disassembly
- Read the assembly with DWARF source line references
- Reconstruct actual Go source from disassembly
- Annotate with
// Binary: 0xADDRESS
Subagent 2: process_api (Stripped Rust)
The Rust binary is stripped — no symbols, no debug info. Must use Ghidra
headless or detailed objdump -d analysis.
Census:
BIN=/tmp/process_api_new
diff <(strings /tmp/process_api_old | sort -u) \
<(strings "$BIN" | sort -u) > /tmp/pa-string-diff.txt
readelf -S "$BIN"
strings "$BIN" | grep '/build/src/'
strings "$BIN" | grep -E '^--(addr|port|max|block|fire|cgr|mem|cpu|oom|control)'
Decompilation: Use Ghidra headless if available, otherwise careful
objdump -d analysis:
- Map functions via string cross-references (panic paths → source files)
- For each new function: read actual decompiled C pseudocode
- Translate to idiomatic Rust guided by known types (serde, clap)
- Annotate with
/// Decompiled from 0xAAAA..0xBBBB
Verification (after each subagent)
- Build check:
bazel build //devinfra/claude/web_env/re/... succeeds
- String coverage: All application strings in the new binary appear in RE source
- Function coverage: All DWARF-listed functions (env-manager) have source files
- Address annotations: Every function has binary address annotation
Phase 5: Container Snapshot → Diff
This phase works from any machine with Docker and network access.
Update the Dockerfile if the version diff (Phase 2b) revealed changes:
- Node.js/Bun versions: Update download URLs
- npm globals: Update version pins
- Go versions: Update download URLs
- APT packages: Update
live-dpkg-versions.txt and optionally advance
SNAPSHOT_DATE in fetch_debs.py
Then fetch packages and rebuild:
bazel run //devinfra/claude/web_env/tools:fetch_debs
bazel run //devinfra/claude/web_env/tools:build_and_diff
Review diff_report.md. Update exclusions.yaml if new runtime artifacts
need exclusion. Commit diff_report.md with the new diff summary.
Phase 6: Documentation Update
Files to update:
| File | What to update |
|---|
devinfra/claude/web_env/diff_report.md | Current live-vs-built diff summary |
devinfra/claude/web_env/docs/environment_discovery.md | env-manager version, help, flags, env vars |
devinfra/claude/web_env/docs/container_spec.md | Binary info, new capabilities |
devinfra/claude/web_env/re/environment_manager/README.md | Target binary table, source tree, CLI docs |
devinfra/claude/web_env/re/process_api/README.md | Target binary, new features |
devinfra/claude/web_env/re/*/PLAN.md | Reconstruction status |
For environment_discovery.md, verify:
--version output
--help for all subcommands
print-sandbox-settings output
- Environment variables:
env | grep -E "^(CLAUDE|CODESIGN|MCP_)"
Phase 7: Commit
Commit together:
- Updated reference binaries and version snapshot
- New RE directories with reconstructed source
- Updated Dockerfile and diff report
- Updated documentation
Key Principles
- Binary is ground truth. Every RE decision traces to binary evidence.
- Full disassembly, not vibes. Use
go tool objdump (Go) or Ghidra (Rust), not string guessing.
- Parallel subagents for independent binary RE work.
- Verify results — builds compile, strings match, functions covered.
- Delta-focused — don't rewrite unchanged code, focus on what changed.
- Flat RE directories — source lives under
re/<binary>/src/, Build ID is
documented in README.md/PLAN.md headers, not encoded in directory names.
Previous versions are preserved in git history.
- Update references only where you edit. Don't mass-replace Build ID strings
across all files. Only update the Build ID marker in files where you actually
change the RE source or documentation to match the new binary.
- Documentation shows current state only. READMEs should describe the current
binary version without historical change summaries or diff sections.
Don't accumulate change history in PLAN.md — keep a single current status.
See /reverse_engineer skill for the detailed binary RE methodology.
Appendix: Docker Build Proxy Pitfalls
The gVisor sandbox requires an egress proxy for all network access. Docker
build containers don't inherit host env vars, so the proxy must be passed via
--build-arg http_proxy=... --build-arg https_proxy=....
Critical issue: SHELL wrapper + eval + proxy URLs.
The Dockerfile uses a logging SHELL wrapper:
SHELL ["/bin/bash", "-c", "exec 3>&2; set -euo pipefail; trap '...' ERR; exec > /tmp/build-step.log 2>&1; eval \"$0\""]
This wrapper uses eval "$0" to execute the actual RUN command. When the proxy
URL contains special characters (JWT tokens with =, +, /, @), eval can
corrupt the URL or prevent APT from parsing the proxy config correctly. Symptoms:
apt-get update fails with "Temporary failure resolving" even though the proxy
IS reachable (verified via docker run).
Fix: Use plain bash shell for APT-setup layers. Before any RUN that writes
APT proxy configuration or runs apt-get update, switch to:
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
Then restore the logging wrapper afterward. The APT proxy config is written as:
printf 'Acquire::http::Proxy "%s";\nAcquire::https::Proxy "%s";\n' \
"${http_proxy:-}" "${https_proxy:-}" > /etc/apt/apt.conf.d/01proxy
Docker cache key behavior: Docker excludes predefined proxy build-arg names
(http_proxy, https_proxy, etc.) from cache keys. This means layer caching
is preserved across sessions with different proxy JWTs. However, it also means
changing the RUN instruction text is the only way to bust cache for these layers
— clearing with docker builder prune --all -f is the nuclear option.
APT version alignment: The snapshot date pins packages to a specific point
in time, but the base image may have newer package versions. Use
apt-get dist-upgrade --allow-downgrades to align before installing -dev
packages, which have strict version dependencies on their library counterparts.