Skip to content
Draft
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
57 changes: 57 additions & 0 deletions Dockerfile.claude-code
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Task image with Claude Code preinstalled.
#
# Extends the default ax-task-runner image, so the runner contract
# (/usr/local/bin/ax-task-runner as PID 1, probes, metadata server) is
# unchanged. Only the tools available to spec.command differ.
#
# make build-claude-code-image # build locally
# make push-claude-code-image # push and print the digest
# make push-claude-code-image CLAUDE_CODE_VERSION=2.1.310
#
# Pin the digest the Makefile prints in your Task's spec.image. See
# examples/claude-code.yaml for a Task that uses this image.
#
# Build on a linux/amd64 host (or in CI). The installer executes the
# downloaded binary to finish setup, and that binary aborts under qemu
# emulation, so cross-building for amd64 on Apple Silicon fails.

# Pinned to the same digest the examples use so runner behavior is
# reproducible. Override with --build-arg BASE_IMAGE=... (or
# CLAUDE_CODE_BASE_IMAGE= for the Makefile target) to build on your own runner.
ARG BASE_IMAGE=gcr.io/ax-substrate/ate-images/ax-task-runner@sha256:3a0dea6ad8b55278685db58aca6e37dc4ba04056831d45bef3aaeafdca43cac6
FROM ${BASE_IMAGE}

# Claude Code's native installer ships a standalone binary, so no Node.js is
# needed. "latest", "stable", or an exact version such as 2.1.310.
ARG CLAUDE_CODE_VERSION=latest

# The installer puts the launcher in ~/.local/bin and versioned binaries in
# ~/.local/share/claude. Install as root (the task runs as root) and expose the
# launcher on the default PATH so spec.command can call `claude` directly.
RUN curl -fsSL https://claude.ai/install.sh | bash -s -- "${CLAUDE_CODE_VERSION}" \
&& ln -s /root/.local/bin/claude /usr/local/bin/claude \
&& claude --version

# The sandbox is the isolation boundary, so headless runs use
# --dangerously-skip-permissions. Claude Code refuses that flag when running
# as root unless IS_SANDBOX is set.
ENV IS_SANDBOX=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
DISABLE_AUTOUPDATER=1

# Keep the default runner entrypoint.
ENTRYPOINT ["/usr/local/bin/ax-task-runner"]
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ SHELL := /bin/bash
# Configuration
AX_IMAGE_REPO ?= gcr.io/ax-substrate/ate-images
TASK_RUNNER_REPO ?= $(AX_IMAGE_REPO)/ax-task-runner
CLAUDE_CODE_REPO ?= gcr.io/ax-images/ax-images
CLAUDE_CODE_VERSION ?= latest
CLAUDE_CODE_BASE_IMAGE ?=
CONTAINER_CLI ?= $(shell which podman 2>/dev/null || which docker 2>/dev/null)

.PHONY: all build build-binaries build-task-runner install push push-task-runner deploy deploy-server deploy-redis apply-example test test-e2e test-e2e-head e2e-setup e2e-run e2e-cleanup clean
.PHONY: all build build-binaries build-task-runner build-claude-code-image install push push-task-runner push-claude-code-image deploy deploy-server deploy-redis apply-example test test-e2e test-e2e-head e2e-setup e2e-run e2e-cleanup clean

all: build

Expand Down Expand Up @@ -56,6 +59,18 @@ push-task-runner: build-task-runner
@echo "==> Current pushed digest:"
@gcloud container images list-tags $(TASK_RUNNER_REPO) --filter="tags=latest" --format="get(digest)"

# Build a task image with Claude Code preinstalled, on top of the default runner image
build-claude-code-image:
@echo "==> Building container image $(CLAUDE_CODE_REPO):latest (Claude Code $(CLAUDE_CODE_VERSION)) using $(CONTAINER_CLI)..."
$(CONTAINER_CLI) build --platform linux/amd64 --build-arg CLAUDE_CODE_VERSION=$(CLAUDE_CODE_VERSION) $(if $(CLAUDE_CODE_BASE_IMAGE),--build-arg BASE_IMAGE=$(CLAUDE_CODE_BASE_IMAGE),) -t $(CLAUDE_CODE_REPO):latest -f Dockerfile.claude-code .

# Push the Claude Code task image to the registry and print the digest to pin in spec.image
push-claude-code-image: build-claude-code-image
@echo "==> Pushing Claude Code task image to $(CLAUDE_CODE_REPO):latest..."
$(CONTAINER_CLI) push $(CLAUDE_CODE_REPO):latest
@echo "==> Current pushed digest:"
@gcloud container images list-tags $(CLAUDE_CODE_REPO) --filter="tags=latest" --format="get(digest)"

# Build and push all images
push: push-task-runner

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ ax suspend task task123 # checkpoint and pause
ax resume task task123 # pick up where it left off
```

To run Claude Code against a repo, apply [`examples/claude-code.yaml`](examples/claude-code.yaml).

Want to see the whole lifecycle end to end? Run [`./demo.sh`](demo.sh). It applies a custom workspace, waits for readiness, runs commands over `ax ssh`, and suspends the task.

## Documentation
Expand Down
2 changes: 1 addition & 1 deletion docs/manifests.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Writing manifests

All four kinds can live in one multi-document YAML file. See [`examples/task.yaml`](../examples/task.yaml) for a complete, working set.
All four kinds can live in one multi-document YAML file. See [`examples/task.yaml`](../examples/task.yaml) for a complete, working set. [`examples/claude-code.yaml`](../examples/claude-code.yaml) shows a Task that runs Claude Code headlessly against a cloned repository.

`metadata.name` and `metadata.atespace` become Substrate resource names, so they must be lowercase RFC 1123 labels: at most 63 lowercase alphanumeric characters or `-`, starting and ending with an alphanumeric character. `ax apply` rejects anything else up front rather than letting the task fail later with `ActorCreationFailed`.

Expand Down
2 changes: 2 additions & 0 deletions docs/runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ RUN npm install -g my-agent

The runner binary stays at `/usr/local/bin/ax-task-runner`, so nothing else changes.

For a working example that layers Claude Code onto the default image, see [`Dockerfile.claude-code`](../Dockerfile.claude-code). `make push-claude-code-image` builds it, pushes it, and prints the digest to pin in `spec.image`; [`examples/claude-code.yaml`](../examples/claude-code.yaml) is a Task that uses it. Build it on an amd64 host: the Claude Code installer runs the binary it downloads, which aborts under qemu emulation.

### 2. Embed the runner package in your own binary

If you want the standard lifecycle but need to run code around it, import `github.com/google/ax/runner` and call `runner.Run` yourself. This gives you a hook for the command's exit and a place to do your own setup before or after the metadata server starts:
Expand Down
90 changes: 90 additions & 0 deletions examples/claude-code.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# A Task that runs Claude Code headlessly against a cloned repository.
#
# The Workspace clones the repo and drops a CLAUDE.md into it so the agent
# starts with project guidance. The Task uses an image with Claude Code
# preinstalled (Dockerfile.claude-code) and runs one non-interactive
# `claude -p` session in /workspace, writing the agent's report to
# /workspace/claude-output.md.
#
# Before applying:
#
# 1. Build and push the image, then pin its digest in spec.image below.
# Substrate rejects tag-only image references.
#
# make push-claude-code-image AX_IMAGE_REPO=<your-registry>
#
# 2. Credentials. The control plane only injects GEMINI_API_KEY from a Model
# resource; an Anthropic Model is not injected into the task container yet.
# So ANTHROPIC_API_KEY has to be set through spec.env. Substitute it at
# apply time instead of committing it:
#
# ANTHROPIC_API_KEY=sk-ant-... envsubst < examples/claude-code.yaml | ax apply -f -
#
# ax watch task claude-code
# ax ssh claude-code -- cat /workspace/claude-output.md
# ax ssh claude-code -- git -C /workspace diff
apiVersion: ax.io/v1alpha1
kind: Workspace
metadata:
name: itsdangerous
atespace: default
spec:
git:
- name: origin
repo: "https://github.com/pallets/itsdangerous.git"
branch: "main"
files:
- path: "CLAUDE.md"
content: |
# Working in this repo
- Pure-Python library. Install with `pip install -e . pytest`, test with `pytest`.
- Keep changes small and focused; do not add runtime dependencies.
- Do not push or open pull requests. Leave changes in the working tree.
---
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: claude-code
atespace: default
spec:
# Built from Dockerfile.claude-code; replace the digest with the one
# `make push-claude-code-image` prints.
image: "gcr.io/ax-images/ax-images@sha256:REPLACE_WITH_PUSHED_DIGEST"
command:
# Run one headless session in the workspace. The sandbox is the isolation
# boundary, so permission prompts are skipped; the image sets IS_SANDBOX=1,
# which that flag requires when running as root.
- sh
- -c
- claude -p --model claude-opus-5-5 --dangerously-skip-permissions --max-turns 50 "$PROMPT" > /workspace/claude-output.md
env:
- name: PROMPT
value: |
Read the repository, install it with its test dependencies, and run the
test suite. Fix any failing or flaky tests you find. If everything passes,
make one small, clearly correct improvement to the README. Finish with a
short markdown report of what you checked and what you changed.
# TODO: Inject the ANTHROPIC_API_KEY to the outbound calls automatically.
- name: ANTHROPIC_API_KEY
value: "${ANTHROPIC_API_KEY}"
resources:
limits:
cpu: "2"
memory: "4Gi"
workspace:
name: itsdangerous
debug: true # serve guest services so `ax ssh` works; off by default
Loading