Creating Images
There are two kinds: base images built from Dockerfiles, and overlay images built by running setup commands on a running base VM.
Prerequisites
Install dev tools:
quicksand install dev
# or: pip install 'quicksand[dev]'This gives you the quicksand dev CLI with scaffold and image-building commands.
Base images
Base image packages (like quicksand-ubuntu) bundle a kernel, initrd, and qcow2 disk built from a Dockerfile.
Scaffold
quicksand dev scaffold base quicksand-mylinuxThis creates quicksand-mylinux/ with a Dockerfile, build hook, Python API, and pyproject.toml. In Claude Code, /new-base-image wraps this interactively.
Use --output-dir to control where the package is created (defaults to ./<name>).
Customize the Dockerfile
Edit quicksand_mylinux/docker/Dockerfile. Every base image must provide:
Kernel + initramfs:
# Ubuntu
RUN apt-get install -y linux-image-virtual \
&& KVER=$(ls /lib/modules/ | head -1) \
&& update-initramfs -c -k ${KVER}
# Alpine
RUN apk add --no-cache linux-virtInit system:
# Ubuntu (systemd)
RUN apt-get install -y systemd systemd-sysv
# Alpine (OpenRC)
RUN apk add --no-cache openrc busybox-openrc9p modules, user, networking, agent:
RUN echo "9p" >> /etc/modules && echo "9pnet" >> /etc/modules && echo "9pnet_virtio" >> /etc/modules
RUN useradd -m -s /bin/bash quicksand
COPY quicksand-sudoers /etc/sudoers.d/quicksand
# Agent via multi-stage build
FROM rust:bookworm AS agent-builder
COPY agent/ /build/
RUN cd /build && cargo build --release && strip target/release/quicksand-guest-agent
# In final stage:
COPY --from=agent-builder /build/target/release/quicksand-guest-agent /usr/local/bin/
CMD ["/sbin/init"]Build and test
uv buildThe hatch build hook runs quicksand_image_tools.build_image() automatically: Docker build, export filesystem, extract kernel/initrd, create ext4, convert to qcow2.
import asyncio
from quicksand import Sandbox
async def main():
async with Sandbox(image="mylinux") as sb:
print((await sb.execute("cat /etc/os-release")).stdout)
asyncio.run(main())Key files
docker/Dockerfileis the VM definition__init__.pyexportsimage(ImageProvider) andMylinuxSandbox(thin wrapper accepting**kwargs)pyproject.tomlregisters thequicksand.imagesentry pointhatch_build.pybuilds the image duringuv build
Reference: packages/contrib/quicksand-ubuntu/ for a complete example.
Overlay images
Overlay packages (like quicksand-agent) boot a base VM, run setup commands, and save the result. They layer on top of existing base images and can stack on each other.
Scaffold
quicksand dev scaffold overlay my-overlay --base ubuntu--base is required. It specifies which image the overlay builds on. This can be a base image (ubuntu, alpine) or another overlay (quicksand-agent). Stacking overlays is encouraged because each layer only stores its own changes, keeping wheel sizes small.
Creates my-overlay/ in the current directory. Use --output-dir to override. In Claude Code, /new-overlay-image wraps this interactively.
Write the setup function
Edit hatch_build.py:
async def _setup(shell: Shell) -> None:
await shell("apt-get update", timeout=120)
await shell("apt-get install -y your-packages", timeout=300)
await shell("pip install your-python-deps", timeout=300)shell() runs commands inside the VM, streams output, and raises on failure.
Build and test
uv buildThe first build boots a real VM, runs _setup(), and saves the overlay. Subsequent builds reuse the cached overlay. To force a rebuild, delete the images/ directory and rebuild.
Key files
hatch_build.pycontains_setup()and the build hook__init__.pyexportsimage(ImageProvider)pyproject.tomlregisters entry points and declares the base image dependency
Reference: packages/contrib/quicksand-agent/ for a complete example.
Release integration
The release pipeline auto-discovers image packages via the quicksand.images entry point. It uses provider.type = "base" for base images and provider.type = "overlay" for overlays. No manual registration needed.