Skip to main content
Every agent runs on a VM. The platform’s default Image gets you Debian, Node, Git, Claude Code, and gh, which is enough for most TypeScript or Node projects. If your repos need anything else (Go, Python, Docker, CUDA, a private SDK, ImageMagick, an internal CLI), you’ll want a custom Image so agents boot with your toolchain already in place. You can also install things at boot time with a startup script. Here’s how to choose:
You needUseWhy
A few small packages, you’re still iterating on what to installA startup scriptEasy to change. Re-runs every boot, so it adds a minute or two to spawn time.
A heavy toolchain (Go, Python, Docker, CUDA) that’s stableA baked ImageBuilt once, cached. Agents boot instantly with everything already installed.
Most teams start with a startup script while figuring out their stack, then move stable installs into a Recipe and bake when things settle. See recipe for every field on the Recipe resource.

1. Write a Recipe

A Recipe is a YAML file that names a base Image and a shell script. The shell script is what gets baked into your new Image. Apply it with murmur set:
cat <<'EOF' | murmur set recipe go-python
name: go-python
base_image_gce_ref: murmur-base-v1
provisioning_script: |
  #!/bin/bash
  set -euo pipefail
  curl -fsSL https://go.dev/dl/go1.22.4.linux-amd64.tar.gz | tar -C /usr/local -xz
  echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile.d/go.sh
  apt-get update && apt-get install -y python3 python3-pip python3-venv
  pip3 install pytest requests flask
  apt-get clean && rm -rf /var/lib/apt/lists/*
provisioning_timeout: "30m"
EOF
A few things worth knowing about the script:
  • Make it idempotent. Bake retries from scratch on a fresh VM, so a flaky apt-get or network blip can fail one attempt and succeed the next. set -euo pipefail and explicit cleanup (apt-get clean) keep things tidy and surface real failures.
  • Clean up after yourself. Anything you leave on disk ends up in the final Image, which makes it bigger and slower to load. Delete tarballs, package caches, and build artifacts before the script exits.

If your install needs a private registry

If your script needs a token for a private registry (Artifactory, npm Enterprise, a private GitHub repo), add the secret name to the Recipe’s secret_allowlist. The secret is injected as an environment variable during the bake only. It’s never written into the final Image, so anyone in your tenant can safely use the resulting Image without seeing your credentials.

2. Bake the Image

Kick off the build with murmur bake:
murmur bake go-python default us-central1
A bake takes 5 to 15 minutes. Check in any time with murmur bakes ls:
murmur bakes ls
Baked Images are content-addressed: their identity is a SHA-256 hash of the base Image, the script, and the target architecture. That means a no-op rebake is free because the platform recognizes the inputs haven’t changed and reuses the existing Image. Change one byte of your script and you get a fresh Image without having to manage versions yourself.

3. Use your new Image

Edit your Workspace to reference the baked Image by name:
image_ref: go-python
The next agent you spawn against this Workspace boots from your new Image. Rolling back is just as easy: edit the field to point at a previous Image and the next agent uses that one instead. Workspaces and Images are intentionally decoupled this way so you can experiment without rebuilding.

Things to watch for when writing your script

The murmur user contract

Provisioning runs as root. Agents run as the unprivileged murmur user (UID 1000, home directory /home/murmur). This split is on purpose: it means a compromised or runaway agent can’t install packages, modify system files, or escalate privileges. The tradeoff is that anything your Recipe installs has to be readable and executable by murmur at runtime, otherwise the agent will fail silently when it tries to use the tool. The patterns below cover the common cases.

Install to system paths

Things in /usr/local/bin are on murmur’s $PATH by default and world-executable, so this works without any extra steps:
curl -fsSLo /usr/local/bin/mytool https://example.com/mytool
chmod +x /usr/local/bin/mytool

Tools that install into a home directory

Some installers (sdkman, pyenv, rustup, etc.) drop files into ~/. Run them as murmur, otherwise they land in /root where the agent can’t see them:
sudo -u murmur bash -c 'curl -fsSL https://get.sdkman.io | bash'

Config files dropped as root

If your script copies a config file into murmur’s home, fix the ownership or murmur will hit a permission-denied at runtime:
chown murmur:murmur /home/murmur/.myconfig

Non-standard $PATH locations

If you install something to /opt/... or anywhere not already on $PATH, extend it through /etc/profile.d/ (not ~/.bashrc) so every shell murmur opens picks it up:
echo 'export PATH=$PATH:/opt/mytool/bin' >> /etc/profile.d/mytool.sh

Don’t redo what the base already has

In the dashboard, expand “What’s on <image>” under the base Image picker. It shows you the exact provisioning script that built the base, so you can skip redundant apt installs and save bake time.