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 need | Use | Why |
|---|---|---|
| A few small packages, you’re still iterating on what to install | A startup script | Easy 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 stable | A baked Image | Built once, cached. Agents boot instantly with everything already installed. |
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 withmurmur set:
- Make it idempotent. Bake retries from scratch on a fresh VM, so a flaky
apt-getor network blip can fail one attempt and succeed the next.set -euo pipefailand 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’ssecret_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 withmurmur bake:
murmur bakes ls:
3. Use your new Image
Edit your Workspace to reference the baked Image by name:Things to watch for when writing your script
The murmur user contract
Provisioning runs as root. Agents run as the unprivilegedmurmur 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:
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:
Config files dropped as root
If your script copies a config file intomurmur’s home, fix the ownership or murmur will hit a permission-denied at runtime:
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:
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 redundantapt installs and save bake time.