Skip to content

feat: add devcontainer with Claude Code agent for autonomous collector development#3118

Open
robbycochran wants to merge 51 commits intomasterfrom
rc-claude-dev
Open

feat: add devcontainer with Claude Code agent for autonomous collector development#3118
robbycochran wants to merge 51 commits intomasterfrom
rc-claude-dev

Conversation

@robbycochran
Copy link
Collaborator

@robbycochran robbycochran commented Mar 19, 2026

Summary

Adds a devcontainer-based development environment where a Claude Code agent can autonomously implement, build, test, and submit changes to collector. The agent works inside an isolated container based on the collector-builder image with all C++ dependencies pre-installed.

What's included

Devcontainer (.devcontainer/)

  • Dockerfile — extends collector-builder:master with Claude Code, Go, Node.js, gcloud CLI, clang-format, bubblewrap (sandboxing), ripgrep, fd
  • devcontainer.json — works with both VSCode and CLI, forwards Vertex AI + GitHub credentials
  • run.sh — launcher script with multiple modes:
    • run.sh "task" — autonomous: implement, PR, CI loop
    • run.sh --interactive — isolated clone + TUI for manual control
    • run.sh --local — edit working tree directly
    • run.sh --shell — shell into the container
  • entrypoint.sh — creates .claude/ dirs, registers GitHub MCP server via PAT
  • init-firewall.sh — optional iptables network restrictions (Vertex AI, GitHub, registries)

Skills (.claude/skills/)

  • /task — implement a change: edit, build, test, format, commit. Stops after commit, no push.
  • /watch-ci — push via GitHub MCP, create PR, monitor CI, fix failures, loop until green

Security model

  • No SSH keys in container — git push fails at auth, only GitHub MCP can push
  • GitHub PAT scoped to stackrox/collector (Contents, PRs, Actions)
  • MCP deny rules — merge, delete, fork, create-repo, trigger-actions blocked globally
  • Read-only mounts — gcloud credentials, gitconfig
  • Isolated clone — agent works on a git clone --local in /tmp, not the user's checkout
  • bubblewrap — Claude Code's built-in command sandboxing
  • Branch protection — agent can push to branches but cannot merge to master

Agent guide (CLAUDE.md)

  • Build commands, key paths, testing rules
  • Git rules: use skills, don't create branches, don't push unless /watch-ci

Workflow

Interactive (review before PR):

.devcontainer/run.sh --interactive
> /task add unit tests for ExternalIPsConfig
# review the diff
cd /tmp/collector-agent-XXX && git push -u origin HEAD && gh pr create --draft
# back in session:
> /watch-ci

Autonomous (hands-off):

.devcontainer/run.sh "add unit tests for ExternalIPsConfig"
# agent implements, pushes via MCP, creates PR, monitors CI until green

Prerequisites

  • Docker (Docker Desktop, OrbStack, or Colima)
  • gcloud auth login && gcloud auth application-default login
  • Vertex AI env vars (CLAUDE_CODE_USE_VERTEX, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION)
  • GITHUB_TOKEN — fine-grained PAT for stackrox/collector

Test plan

  • Image builds on arm64 Mac
  • Collector compiles inside container (cmake configure + build)
  • 17/17 unit tests pass inside container
  • Claude Code authenticates to Vertex AI
  • Claude Code responds to prompts and uses tools
  • /task skill discovered and invocable
  • Git clone isolation works (separate from host checkout)
  • /watch-ci pushes via GitHub MCP and creates PR
  • CI monitoring loop detects failures and iterates
  • Autonomous end-to-end run completes

robbycochran and others added 23 commits March 18, 2026 23:08
Add a devcontainer based on the collector-builder image that enables
agent-driven development of collector. The devcontainer includes all
C++ build dependencies, Go, Node.js, Claude Code, gcloud CLI, and
developer tooling (ripgrep, fd, gh).

Verified: cmake configure, full collector build, and 17/17 unit tests
pass inside the container. Claude Code authenticates to Vertex AI via
read-only gcloud credential mount.

- .devcontainer/: Dockerfile, devcontainer.json, network firewall
- CLAUDE.md: agent development guide with build/test workflows
- .claude/skills/: /build, /ci-status, /iterate slash commands
- .claude/settings.json: deny Read(.devcontainer/**) for security
- Security: bubblewrap sandboxing, npm hardening, read-only mounts,
  optional iptables firewall with NET_ADMIN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…DE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The run.sh script launches Claude Code in the devcontainer with:
- Git worktree isolation: agent works on its own copy, never touches
  the user's checkout. Worktree is cleaned up on exit.
- GitHub auth: supports fine-grained PAT via GITHUB_TOKEN or
  host gh CLI config (read-only mount)
- Modes: autonomous (-p task), interactive, shell, no-worktree

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace gh CLI and Docker-based MCP server with official GitHub MCP
  server at api.githubcopilot.com/mcp (OAuth, project-scoped .mcp.json)
- Add permissions.deny for dangerous MCP tools (merge, delete, fork)
- Add bubblewrap, socat, iptables to Dockerfile for sandboxing
- Remove gh CLI install from Dockerfile
- Fix run.sh: suppress git worktree output, use bash array for docker
  args instead of eval with string (fixes --interactive mode)
- Remove Docker socket mount and GITHUB_TOKEN forwarding from run.sh
- Update skills to reference mcp__github__* tools instead of gh CLI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…missions

Move skills from .claude/skills/ to .claude/plugins/collector-dev/ as a
proper Claude Code plugin. Each skill now declares only the tools it needs
via allowed-tools frontmatter:

- /collector-dev:build — cmake, make, git describe, strip (no GitHub)
- /collector-dev:ci-status — git branch/log + GitHub MCP read-only tools
- /collector-dev:iterate — build tools + git + clang-format + GitHub MCP
  PR/push tools

The GitHub MCP server config moves from root .mcp.json into the plugin's
.mcp.json so it's bundled with the skills that use it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
run.sh now creates the branch, pushes it, and opens a draft PR before
launching the agent. The agent receives the branch name and PR URL in
its prompt and only needs to commit and push.

iterate skill drops all GitHub MCP write tools (create_branch, push_files,
create_pull_request, update_pull_request). It retains only read-only
GitHub MCP tools for checking CI status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New skill that checks CI status and reacts to failures:
- PASSED: all checks green, stop
- PENDING: still running, wait for next loop
- FIXED: diagnosed failure, pushed fix, awaiting new CI
- FLAKE: infra issue, not code
- BLOCKED: needs human intervention

Usage: /loop 30m /collector-dev:watch-ci

Same restricted tool set as iterate — read-only GitHub MCP, build
tools, git push to existing branch only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New /collector-dev:task skill that runs the full lifecycle:
1. Implement the task (edit, build, unit test, format, push)
2. Monitor CI in a loop (sleep 10m, check status, fix failures)
3. Stop when all checks pass, or after 6 cycles (~3h)

Reports final status: PASSED, BLOCKED, or TIMEOUT.

run.sh now invokes /collector-dev:task directly so a single command
goes from task description to green CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Code doesn't auto-discover plugins from .claude/plugins/.
Add --plugin-dir /workspace/.claude/plugins/collector-dev to all
claude invocations so skills like /collector-dev:task are available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use --output-format stream-json --verbose for autonomous task mode so
all messages (tool calls, responses, thinking) stream to container
stdout in real time. Interactive mode keeps the normal TUI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
--local edits the working tree directly with interactive TUI.
No worktree, no branch, no PR. For debugging and experimentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
git worktree add does not init submodules. Without this, cmake fails
because falcosecurity-libs and other submodules are missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only init falcosecurity-libs and collector/proto/third_party/stackrox.
The 17 builder/third_party/* submodules are baked into the builder
image and not needed for compiling collector. This avoids cloning
49 recursive submodules (was hanging on large repos like grpc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… mode

Both headless and default task mode now use the same task_prompt()
that explicitly invokes /collector-dev:task, ensuring the skill's
allowed-tools restrictions are enforced. Only difference is headless
skips PR creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validate before launching the container:
- Docker running and image exists (with build command hint)
- gcloud ADC credentials file exists
- Vertex AI env vars set (CLAUDE_CODE_USE_VERTEX, GOOGLE_CLOUD_PROJECT,
  GOOGLE_CLOUD_LOCATION)
- gh CLI authenticated (only for PR mode)
- ~/.gitconfig and ~/.ssh exist (warnings)
- git push and gh pr create errors are no longer silent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove gh from run.sh entirely (no check_gh, no setup_pr)
- Remove --headless (was identical to default mode without PR)
- task skill now has create_pull_request and create_branch in allowed-tools
- Agent pushes branch and creates draft PR via GitHub MCP server
- iterate skill stays read-only on GitHub (only task can create PRs)
- Simplified to 4 modes: default task, --interactive, --local, --shell

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move skills from .claude/plugins/collector-dev/ to .claude/skills/
  (standalone skills, no plugin wrapper). Fixes skills not loading in
  worktrees since the plugin directory was never committed.
- Delete collector-dev plugin entirely (caused phantom GitHub MCP)
- Remove --plugin-dir from run.sh
- Add entrypoint.sh that creates .claude dirs and registers GitHub
  MCP server via claude mcp add-json when GITHUB_TOKEN is set
- Add --skip-submodules and --debug flags to run.sh
- Add COPY --chmod=755 for entrypoint.sh in Dockerfile
- Simplify CLAUDE.md from 249 to 30 lines — just build commands,
  key paths, testing rules, and conventions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The git push deny was blocking the host too. Move it to the
entrypoint which writes /home/dev/.claude/settings.json (user scope)
inside the container only. Project-level settings.json keeps only
the MCP deny rules which apply everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No SSH keys in container = git push fails at auth. No deny rule needed.
GitHub MCP (PAT via GITHUB_TOKEN) is the only push path.
Also removes git push deny from entrypoint settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
git clone --local sets origin to the local filesystem path. Fix the
remote to the real GitHub URL so git push works from the clone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robbycochran robbycochran requested a review from a team as a code owner March 19, 2026 06:17
@codecov-commenter
Copy link

codecov-commenter commented Mar 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 27.38%. Comparing base (ec207af) to head (76f1e6c).
⚠️ Report is 13 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3118   +/-   ##
=======================================
  Coverage   27.38%   27.38%           
=======================================
  Files          95       95           
  Lines        5427     5427           
  Branches     2548     2548           
=======================================
  Hits         1486     1486           
  Misses       3214     3214           
  Partials      727      727           
Flag Coverage Δ
collector-unit-tests 27.38% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@robbycochran robbycochran changed the title Claude dev environment feat: add devcontainer with Claude Code agent for autonomous collector development Mar 19, 2026
robbycochran and others added 4 commits March 19, 2026 09:57
Mount the main repo's .git/ directory at its original absolute path
inside the container (read-only). The worktree's .git file resolves
correctly without any path patching. No cleanup hacks needed.

Replaces the git clone --local approach which had issues with the
remote URL pointing to the local filesystem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mount falcosecurity-libs and collector/proto/third_party/stackrox
read-only from the main repo into the container. Eliminates submodule
init entirely — worktree creation is now just git worktree add.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Default: git submodule update --init --depth 1 (slower, independent copy)
--symlink-submodules: mount from main repo read-only (instant, shared)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
robbycochran and others added 24 commits March 19, 2026 10:11
--branch <name> sets a custom branch name instead of auto-generated.
Worktrees now created under /tmp/collector-worktrees/<branch-name>
with slashes replaced by dashes in the directory name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
git commit writes to .git/worktrees/<name>/ (HEAD, index, refs).
Read-only mount was blocking commits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
.git/ is mounted read-only so the agent can't modify shared refs,
objects, or config. Only .git/worktrees/<name>/ is read-write,
which is what git commit needs (HEAD, index, refs for that branch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mpts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Host uid (502) != container uid (1000). Make the worktree's
.git/worktrees/<name>/ world-writable so the container's dev user
can write index.lock, HEAD, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a+rw doesn't add execute on directories, so creating files inside
modules/ subdirs fails. a+rwX adds execute on directories only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Submodule init and git commit both need to write to various places
under .git/ (worktrees/, modules/, index.lock). The ro mount with
rw overlay on the worktree subdir is insufficient. Mount .git rw.
Agent still can't push (no SSH keys).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…bmodules

- Remove SYS_PTRACE (container escape risk), NET_ADMIN, NET_RAW from
  devcontainer.json runArgs
- Remove --symlink-submodules flag entirely (broke cmake builds,
  added complexity for marginal speedup)
- Show submodule init progress instead of silencing output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Request repos, pull_requests, and actions toolsets so the agent can
create PRs, push files, list workflow runs, and download job logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an Agent Status section to the PR description with timestamp,
last commit SHA, cycle count, status, and one-line details. Keeps
the original PR description above the status section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Planning doc for Trail of Bits mcp-context-protector as a security
proxy for GitHub MCP. Covers architecture, open questions, threat
model comparison, and phased implementation approach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ask skill

The /task skill's STOP condition caused the agent to exit before
running /watch-ci. Instead, give the agent inline instructions for
step 1 (implement) and explicitly tell it to run /watch-ci for step 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ling

Single skill for the full autonomous cycle: implement → build → test →
commit → push via MCP → create PR → monitor CI → address review
comments → loop until green.

run.sh autonomous mode now invokes /dev-loop directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If the task starts with /, pass it as-is (e.g., /task, /watch-ci).
Otherwise default to /dev-loop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ext-protector

mcp-watchdog is lightweight (pattern-based, no ML), covers 70+ attack
patterns including credential redaction, prompt injection, rug pull
detection, SSRF blocking. mcp-context-protector requires PyTorch/
LlamaFirewall (~3GB), better suited for centralized deployment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The agent can't git push (no SSH keys) and GitHub MCP push_files
requires an existing remote branch. Push from the host in run.sh
so /watch-ci and /dev-loop can push via MCP immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…skills

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All modes now use interactive TUI by default, including autonomous
task mode. This means you can watch, interrupt, and interact with
the agent even in /dev-loop mode. Use --no-tui to get the old
stream-json output for piping or logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explain that push_files sends file contents via GitHub API — it does
not sync local git history. Provide explicit steps: get branch name,
get changed files list, read contents, call push_files. Remove the
remote branch prerequisite from watch-ci since run.sh handles it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uide

Covers: quick start, modes, skills, token permissions (required vs
optional vs not needed), security model, env vars, worktree management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use tabs for indentation (shfmt default)
- Fix SC2064: replace trap with quoted variable expansion with an
  on_exit function that reads ACTIVE_WORKTREE global
- Consistent formatting throughout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants