Skip to content

Building Custom Images

Scion agents run inside container images that bundle an LLM harness (Claude, Gemini, etc.) with the Scion toolchain. By default, Scion uses pre-built images from the upstream registry. This guide shows how to build your own images and configure Scion to use them.

  • Self-hosted registries: Push images to a registry you control (GHCR, Artifact Registry, ECR, etc.).
  • Pinned versions: Tag and version images to match your deployment lifecycle.
  • Custom modifications: Add tools, certificates, or configurations to the base images.

Scion images are built in layers:

core-base System dependencies on node:24-trixie-slim (Go, Node, Python, Git, CLI tools)
└── scion-base Scion CLI, sciontool binary, scion user, entrypoint
├── scion-claude Claude Code harness
├── scion-gemini-cli Gemini CLI harness
├── scion-opencode OpenCode harness
├── scion-codex Codex harness
└── scion-hub Scion hub server

The core-base layer changes infrequently, but needs to be built at least once as it is a prerequisite for all other layers. Most rebuilds only need scion-base, the harness layers, and the hub layer (the common build target).

Besides the language runtimes, core-base installs the command-line tools agents commonly use, through image-build/lib/install-core-toolchain.sh: gh, gcloud, gcsfuse, kubectl, golangci-lint, Chromium, and helm, plus the npm tools chrome-devtools-mcp and @playwright/cli. helm is pinned to an exact version, v3.17.3, the version used by the chart’s CI, so agents that work on the deploy/helm/scion-hub chart render the same output as CI without downloading helm at run time. To change the version, set the HELM_VERSION, HELM_SHA256_AMD64, and HELM_SHA256_ARM64 build args together; the build fails if the downloaded archive does not match the checksum for the build architecture. The thick-prep layer installs the same pinned helm.

For security and compatibility across runtimes (especially Kubernetes), Scion agents are required to run as a non-root user.

  • User: The base images create a scion user.
  • UID: The user must have UID 1000.
  • Permissions: Ensure your custom images do not require root privileges at runtime and that any added files or directories are accessible by the scion user. Home directory structure (/home/scion) and environmental variables (HOME, USER, LOGNAME) are automatically injected by the runtime.

Building Behind a Corporate Proxy (npm Registry)

Section titled “Building Behind a Corporate Proxy (npm Registry)”

Corporate networks that block registry.npmjs.org can point the build at an internal npm mirror (Artifactory, Nexus, Verdaccio, etc.) using two environment variables:

Variable Purpose Default
NPM_REGISTRY Registry URL passed as a Docker build arg. Sets NPM_CONFIG_REGISTRY inside the image so that all npm install commands during the build — and agents at runtime — use the mirror. https://registry.npmjs.org/
NPM_CONFIG_FILE Host path to an .npmrc file. Mounted as a BuildKit secret (id=npmrc) so authentication tokens are available during RUN steps but never written to an image layer. — (unauthenticated)
Terminal window
# Unauthenticated mirror
NPM_REGISTRY=https://npm.corp.example.com/ \
image-build/scripts/build-images.sh --target all
# Authenticated mirror (token stays out of the image)
NPM_REGISTRY=https://npm.corp.example.com/ \
NPM_CONFIG_FILE=~/.npmrc \
image-build/scripts/build-images.sh --target all --push --registry ghcr.io/myorg

NPM_REGISTRY is threaded as a build arg into the core-base layer and inherited by every downstream image. NPM_CONFIG_FILE is forwarded by the local-docker and local-podman builders; cloud-build does not support BuildKit secrets — use a private Artifact Registry npm repository with IAM-based auth instead.

Building Behind a Corporate Proxy (Python Package Index)

Section titled “Building Behind a Corporate Proxy (Python Package Index)”

The same pattern applies to Python packages. Corporate networks that block pypi.org can point the build at an internal PyPI mirror (Artifactory, Nexus, devpi, etc.) using two environment variables:

Variable Purpose Default
PIP_INDEX_URL Registry URL passed as a Docker build arg. Sets PIP_INDEX_URL inside the image so that all pip install commands during the build — and agents at runtime — use the mirror. https://pypi.org/simple
PIP_CONFIG_FILE Host path to a pip.conf file. Mounted as a BuildKit secret (id=pipconf) so authentication tokens are available during RUN steps but never written to an image layer. — (unauthenticated)
Terminal window
# Unauthenticated mirror
PIP_INDEX_URL=https://pypi.corp.example.com/simple/ \
image-build/scripts/build-images.sh --target all
# Authenticated mirror (credentials stay out of the image)
PIP_INDEX_URL=https://pypi.corp.example.com/simple/ \
PIP_CONFIG_FILE=~/.pip/pip.conf \
image-build/scripts/build-images.sh --target all --push --registry ghcr.io/myorg

PIP_INDEX_URL is threaded as a build arg into the core-base layer and inherited by every downstream image. PIP_CONFIG_FILE is forwarded by the local-docker and local-podman builders; cloud-build does not support BuildKit secrets — use a private Artifact Registry PyPI repository with IAM-based auth instead.

A single orchestrator script — image-build/scripts/build-images.sh — owns the build DAG (which images depend on which, in what order, with which tags). The execution backend is selected with --builder. Three backends ship today:

Builder Backend Multi-arch Push behavior
local-docker (default) docker buildx yes (auto-promotes to --push) honors --push; --load otherwise
local-podman podman build single-arch by default; multi-arch errors out (manual QEMU setup required) honors --push; built images live in the local store automatically
cloud-build gcloud builds submit against a static cloudbuild-*.yaml (group targets) or a config generated on the fly (individual harness targets) always linux/amd64 + linux/arm64 (server-side) always pushes

The orchestrator computes tags, threads BASE_IMAGE between layers, and dispatches to the selected builder. Switching backends is purely a --builder flag change — target names and other flags are uniform.

Build all images locally. Once core-base has been built, rebuilds can often use the default common build target.

Terminal window
# Build all layers locally without pushing — bare tags
# (scion-claude:latest, etc.) land in your local docker engine.
image-build/scripts/build-images.sh --target all
# Or build and push to your registry
image-build/scripts/build-images.sh --registry ghcr.io/myorg --push --target all
# When pushing, configure Scion to use them
scion config set image_registry ghcr.io/myorg

--registry is optional for local-only builds. Omit it and the orchestrator tags images with bare names that stay in your local image store. Supply it (without --push) if you’d rather have fully-qualified tags locally — nothing leaves the machine until you docker push separately. --registry becomes required as soon as you pass --push or --builder cloud-build.

Terminal window
# Single-arch local build, no registry needed
image-build/scripts/build-images.sh --builder local-podman --target all
# Or push to a registry
image-build/scripts/build-images.sh \
--builder local-podman \
--registry quay.io/myorg \
--push

Multi-arch Podman builds require manual QEMU binfmt setup. Until that is in place, passing --platform linux/amd64,linux/arm64 to local-podman exits with an actionable error.

If your project is hosted on GitHub:

  1. Fork the repo (or use it as a template).
  2. Go to Actions > Build Scion Images > Run workflow.
  3. Enter ghcr.io/<your-username> as the registry.
  4. Wait for the build to complete.
  5. Configure Scion:
    Terminal window
    scion config set image_registry ghcr.io/<your-username>

The workflow shells out to build-images.sh --builder local-docker after docker/setup-buildx-action, so it shares all the orchestration logic with local builds. It is also available as a reusable workflow via workflow_call for downstream repos.

For GCP-based workflows:

Terminal window
# One-time setup: enable APIs, create Artifact Registry repo, grant permissions
image-build/scripts/setup-cloud-build.sh --project my-gcp-project
# Submit a build
image-build/scripts/build-images.sh \
--builder cloud-build \
--registry us-central1-docker.pkg.dev/my-gcp-project/scion

Then point Scion at the registry:

Terminal window
scion config set image_registry us-central1-docker.pkg.dev/my-gcp-project/scion

The image_registry setting tells Scion to pull images from your registry instead of the upstream default. It rewrites the registry prefix of all standard harness images (those named scion-<harness>) while preserving the image name and tag.

When image_registry is set, Scion transforms the default image reference:

Default Image image_registry Resolved Image
us-central1-docker.pkg.dev/.../scion-claude:latest ghcr.io/myorg ghcr.io/myorg/scion-claude:latest
us-central1-docker.pkg.dev/.../scion-gemini-cli:latest ghcr.io/myorg ghcr.io/myorg/scion-gemini-cli:latest

Globally (applies to all projects):

Terminal window
scion config set image_registry ghcr.io/myorg

Or edit ~/.scion/settings.yaml directly:

schema_version: "1"
image_registry: "ghcr.io/myorg"

Per-profile (different registries for different environments):

profiles:
local:
runtime: docker
image_registry: "ghcr.io/myorg"
staging:
runtime: kubernetes
image_registry: "us-central1-docker.pkg.dev/myproject/staging"

Profile-level image_registry takes precedence over the top-level setting of the same source. Across sources the order is: the Runtime Broker’s SCION_IMAGE_REGISTRY, then values saved in the Hub (profile, then top level), then the settings file (profile, then top level). So a registry saved in the Hub, for example through the setup wizard, beats a settings-file profile value; save the profile value in the Hub to keep it. See Image registry order. The profile used is the one the agent was provisioned with, recorded broker-side, on every start and restart; see Settings precedence.

The image_registry setting is the lowest-priority way to configure images. Explicit overrides always win (summary; see the full chain linked below, which also covers inline config and settings harness_configs.<name>.image):

  1. CLI --image flag (highest priority)
  2. Profile harness_overrides image field, when explicitly set
  3. Agent inline config (--config) image field
  4. Settings harness_configs.<name>.image, then the harness-config’s own config.yaml image
  5. image_registry rewrite (lowest priority)

A template’s image field is ignored with a warning: a template describes the agent’s job, and the image comes from the harness-config and settings.

Each time a higher-priority source replaces another source’s image, the agent start logs it at Info. See Settings precedence for the full chain.

If any higher-priority override specifies a full image path, image_registry does not apply to that agent.

The image-build/scripts/build-images.sh orchestrator supports the following options:

Flag Description Default
--registry <path> Target registry path (e.g., ghcr.io/myorg). Required when --push is set or with --builder cloud-build. When omitted for a local-only build, images are tagged with bare names (e.g., scion-claude:latest) and stay in the local store. —
--builder <name> Backend: local-docker, local-podman, or cloud-build. local-docker
--target <target> Build target (see below). common
--tag <tag> Mutable image tag. The :<short-sha> tag is always added when in a git repo. latest
--platform <plat> Target platform(s). Use all for linux/amd64,linux/arm64. Ignored by cloud-build. builder’s native arch
--push Push images after building. Auto-enabled for multi-arch local builds. Ignored by cloud-build (always pushes). build only
--dry-run Print the resolved steps and the exact builder commands without executing. off

Targets resolve to an ordered list of step IDs (one step per image):

Target What It Builds Notes
core-base core-base Foundation tools layer.
scion-base scion-base Adds sciontool. Reuses existing core-base:<tag>.
harnesses All catalog harness images with a harnesses/<name>/Dockerfile Reuses existing scion-base:<tag>.
hub scion-hub Hub server image. Reuses existing scion-base:<tag>.
common (default) scion-base + harnesses + hub Skips core-base. Most common rebuild.
all Full DAG Rebuilds everything from core-base.

Every image is tagged with both :<tag> (controlled by --tag, defaults to latest) and :<short-sha> (computed once from git rev-parse --short HEAD). When no SHA is available (e.g. running outside a git working tree), only the mutable tag is emitted.

When two steps in the same run depend on each other, the orchestrator threads BASE_IMAGE=...:<short-sha> so chained builds are immune to concurrent overwrites of :latest. Standalone targets (e.g. --target harnesses on its own) reference the parent image as :<tag>.

When building scion-base, the orchestrator stamps scion and sciontool with the git commit and, when HEAD is on an exact git tag, the version. Inside an agent container, sciontool version reports both. scion-base also carries the OCI org.opencontainers.image.revision label, which every image built FROM scion-base (harnesses, hub) inherits. To check which commit an image was built from without running it:

Terminal window
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' scion-claude:latest

If a build uses scion-base but does not rebuild it in the same run (for example, --target harnesses), the orchestrator prints a warning. The resulting images inherit whatever sciontool is already baked into the existing scion-base:<tag>. The warning also shows that image’s revision when a local copy exists. Include scion-base in the build (for example, --target common) so sciontool fixes reach your agents.

The orchestrator and builders assume the caller is already authenticated to the target registry (via docker login, podman login, gcloud auth configure-docker, etc.) and to any required cloud APIs. No login steps are performed inside the script.

Terminal window
# Full rebuild for all platforms, pushed to GHCR
image-build/scripts/build-images.sh \
--registry ghcr.io/myorg \
--target all \
--platform all \
--push
# Build only harness images with a specific tag
image-build/scripts/build-images.sh \
--registry ghcr.io/myorg \
--target harnesses \
--tag v1.2.0 \
--push
# Local build for testing (no push, current architecture only, bare tags)
image-build/scripts/build-images.sh --target all
# Preview what would run, without executing anything
image-build/scripts/build-images.sh \
--registry ghcr.io/myorg \
--target all \
--platform all \
--dry-run
# Submit the same target DAG to Cloud Build
image-build/scripts/build-images.sh \
--builder cloud-build \
--registry us-central1-docker.pkg.dev/myproject/scion \
--target all

The workflow at .github/workflows/build-images.yml can be used in two ways:

Run it from the GitHub Actions UI with inputs for registry, target, tag, and platform.

Call it from your own workflows in downstream repos:

jobs:
build-images:
uses: google/scion/.github/workflows/build-images.yml@main
with:
registry: ghcr.io/myorg
target: common
tag: latest
platform: all

The workflow is a runner, not a builder — it shells out to build-images.sh --builder local-docker and shares the same Dockerfiles and orchestration as a local build.

The cloud-build builder maps each group --target to a static YAML file in image-build/:

Target Config file
all cloudbuild.yaml
common cloudbuild-common.yaml
core-base cloudbuild-core-base.yaml
scion-base cloudbuild-scion-base.yaml
harnesses cloudbuild-harnesses.yaml
hub cloudbuild-hub.yaml

An individual harness step ID (scion-claude, scion-muse-code, etc.) is also a valid --target under cloud-build. Rather than a static file, the builder generates a one-step config for just that harness — mirroring the matching step in cloudbuild-harnesses.yaml — submits it, and removes it afterward. This rebuilds one harness on Cloud Build without resubmitting the whole harnesses group.

These YAMLs reference $_TAG, $_SHORT_SHA, $_COMMIT_SHA, $_REGISTRY, and (in the five that build scion-base: all, common, scion-base, thick/thick-prep, omni) $_VERSION, all forwarded by the orchestrator. _TAG defaults to latest in every YAML’s substitutions: block; _VERSION defaults to '' in the YAMLs that declare it, preserving the prior behavior when --tag or a version tag is omitted. The orchestrator only forwards a non-empty _VERSION when HEAD is on an exact git tag; off-tag, it relies on that default.

Run the one-time setup script to configure your GCP project:

Terminal window
image-build/scripts/setup-cloud-build.sh --project my-gcp-project

This script:

  • Enables the Cloud Build and Artifact Registry APIs.
  • Creates an Artifact Registry repository named scion.
  • Grants Cloud Build the necessary IAM permissions.