diff --git a/README.md b/README.md index 7f72ec0..fe2e8ae 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Public container images for cloud work, built multi-arch and rootless with secur | Image | Description | |---|---| | `ghcr.io/cloudsnacks/actions-runner` | Rootless GitHub Actions runner for [Actions Runner Controller](https://github.com/actions/actions-runner-controller) | +| `ghcr.io/cloudsnacks/claude-code` | Headless Claude Code agent with git-repo and agent-profile bootstrap | | `ghcr.io/cloudsnacks/infisical-mcp` | Infisical MCP server (secrets management over MCP) | | `ghcr.io/cloudsnacks/kubectl` | Rootless kubectl CLI | | `ghcr.io/cloudsnacks/sandbox-agent` | Rootless base image for sandboxed coding agents (Node, Python, uv, git, gh, ripgrep) | @@ -31,6 +32,33 @@ Every image is tagged `X.Y.Z`, `X.Y`, `X`, and `latest`. - Base images pinned by digest, tool versions pinned and updated by Renovate - SBOM and SLSA provenance attestations attached to every image +### claude-code + +The entrypoint bootstraps the sandbox, then execs the container command (default `claude -p`). Everything is optional; with no variables set the agent starts in the empty `/workspace` scratch space. + +| Variable | Effect | +|---|---| +| `AGENT_REPO` | Repo cloned into `AGENT_WORKSPACE` (skipped if it already contains `.git`) | +| `AGENT_REPO_REF` | Branch or tag to clone | +| `AGENT_PROFILE_REPO` | Repo of agent profiles, shallow-cloned at startup | +| `AGENT_PROFILE` | Subdirectory of that repo to install; defaults to the repo root | +| `AGENT_PROFILE_REF` | Branch or tag of the profile repo | +| `AGENT_PROMPT` | Prompt piped to the command on stdin; omit to pipe it in yourself | +| `AGENT_WORKSPACE` | Working directory (default `/workspace`) | +| `GITHUB_TOKEN` | Configures git to authenticate `https://github.com/` clones | + +The profile's contents are copied into `CLAUDE_CONFIG_DIR` (`/home/agent/.claude`), so a profile directory holds `settings.json`, `agents/`, `skills/`, `CLAUDE.md`, and anything else Claude Code reads from there. + +```shell +docker run --rm \ + -e ANTHROPIC_API_KEY \ + -e AGENT_REPO=https://github.com/cloudsnacks/containers.git \ + -e AGENT_PROFILE_REPO=https://github.com/cloudsnacks/agent-profiles.git \ + -e AGENT_PROFILE=reviewer \ + -e AGENT_PROMPT="Summarise the CI workflow" \ + ghcr.io/cloudsnacks/claude-code:latest +``` + ### Verifying provenance ```shell diff --git a/images/claude-code/Dockerfile b/images/claude-code/Dockerfile new file mode 100644 index 0000000..01c3fd1 --- /dev/null +++ b/images/claude-code/Dockerfile @@ -0,0 +1,24 @@ +FROM ghcr.io/cloudsnacks/sandbox-agent:1.0.0@sha256:35ea6452c36f453785b340b21980ac81641edaffde67b51033159a30bbf9898a + +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION=2.1.220 + +USER root + +RUN NPM_CONFIG_PREFIX=/usr/local npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ + && npm cache clean --force \ + && claude --version + +COPY --chmod=0755 entrypoint.sh /usr/local/bin/entrypoint + +USER agent + +ENV AGENT_WORKSPACE=/workspace \ + CLAUDE_CONFIG_DIR=/home/agent/.claude + +WORKDIR /workspace + +LABEL org.opencontainers.image.description="Headless Claude Code agent with git-repo and agent-profile bootstrap" + +ENTRYPOINT ["/usr/local/bin/entrypoint"] +CMD ["claude", "-p"] diff --git a/images/claude-code/entrypoint.sh b/images/claude-code/entrypoint.sh new file mode 100644 index 0000000..f0ea183 --- /dev/null +++ b/images/claude-code/entrypoint.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +workspace="${AGENT_WORKSPACE:-/workspace}" +profile_dir="${CLAUDE_CONFIG_DIR:-${HOME}/.claude}" + +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + git config --global \ + "url.https://x-access-token:${GITHUB_TOKEN}@github.com/.insteadOf" \ + "https://github.com/" +fi + +if [[ -n "${AGENT_PROFILE_REPO:-}" ]]; then + checkout="$(mktemp -d)" + git clone --depth 1 ${AGENT_PROFILE_REF:+--branch "${AGENT_PROFILE_REF}"} \ + "${AGENT_PROFILE_REPO}" "${checkout}" + source="${checkout}${AGENT_PROFILE:+/${AGENT_PROFILE}}" + if [[ ! -d "${source}" ]]; then + echo "agent profile not found in repo: ${AGENT_PROFILE:-}" >&2 + exit 1 + fi + rm -rf "${checkout}/.git" + mkdir -p "${profile_dir}" + cp -aT "${source}" "${profile_dir}" + rm -rf "${checkout}" +fi + +mkdir -p "${workspace}" +if [[ -n "${AGENT_REPO:-}" && ! -d "${workspace}/.git" ]]; then + git clone ${AGENT_REPO_REF:+--branch "${AGENT_REPO_REF}"} "${AGENT_REPO}" "${workspace}" +fi +cd "${workspace}" + +if [[ -n "${AGENT_PROMPT:-}" ]]; then + exec "$@" <<<"${AGENT_PROMPT}" +fi + +exec "$@" diff --git a/images/claude-code/metadata.yaml b/images/claude-code/metadata.yaml new file mode 100644 index 0000000..ae4e2dd --- /dev/null +++ b/images/claude-code/metadata.yaml @@ -0,0 +1,9 @@ +--- +name: claude-code +description: Headless Claude Code agent with git-repo and agent-profile bootstrap +# renovate: datasource=npm depName=@anthropic-ai/claude-code +version: 2.1.220 +platforms: + - linux/amd64 + - linux/arm64 +test: claude --version && node --version && git --version && test "$(pwd)" = /workspace