Skip to main content

Sandbox profiles and setup scripts

A sandbox profile is a setup script that runs in a new sandbox after your repository is cloned and before the agent starts. Use one when every session needs something the sandbox does not have yet: dependencies installed, a database running, a private registry signed in to, a tool on the PATH.

The agent starts only once the script has finished, so it begins work in a sandbox that is already set up.

Where profiles live, and which one runs

Profiles are kept at three levels, each under Sandbox profiles in its settings: your organization, a workspace, and a project. When a sandbox launches without a choice, the narrowest level that has a default wins:

  1. the project's default, then
  2. the workspace's default, then
  3. the organization's default.

A level's default is the profile marked as default there — or, when a level has exactly one profile, that profile. With two or more unmarked profiles a level has no default and the next level up is used. If no level has one, no setup script runs.

To choose for one launch, open Advanced settings in the launcher and pick a profile, or None for no setup script. A restart keeps the choice its launch made. Automations that start sandboxes on your behalf can name a profile in their own settings.

A profile that has since been archived

Restarts and automations fall back to the default when the profile they chose has been archived, and the launch timeline says so. A launch you start yourself with an archived profile is refused, so the choice is never silently changed under you.

Writing a setup script

What the script runs in

  • Ubuntu 22.04, as the sandbox user, with sudo without a password.
  • In the repository folder (your home folder when the project has no repository).
  • Preinstalled: git, curl, jq, a C/C++ build toolchain, Python 3.10 with venv and pip, Node.js 22 and 24 (a .nvmrc, .node-version or engines.node picks one; corepack is enabled for pnpm and yarn), Go, and a stable Rust toolchain.
  • Bash. Start with set -euo pipefail so the first failing command stops the script and is reported, instead of carrying on.

Showing progress

Print ::step:: followed by a name to say what the script is doing:

echo "::step::Install dependencies"

The step and the latest output show while the sandbox is setting up, and every step is recorded on the launch timeline.

Handing values to the agent

The script's own environment variables do not reach the agent. To hand something over, append to two files:

FileAppendExample
$PS_ENVNAME=value lines — environment variables for the agent's commandsecho "DATABASE_URL=postgres://localhost/app" >> "$PS_ENV"
$PS_PATHone directory per line — added to the agent's PATHecho "$PWD/.venv/bin" >> "$PS_PATH"

Only the names are recorded on the launch timeline, never the values.

Services

Start services in the background (for example redis-server --daemonize yes or sudo service postgresql start). They keep running after the script ends, and the agent can use them.

Secrets

Add a secret reference to the profile (the Secrets section of the profile), naming a secret you have already stored. At launch it is looked up for the project being launched and given to the script as an environment variable.

  • A profile's secrets reach the setup script only — not the agent.
  • Their values are masked in the script's output.
  • A $PS_ENV line containing a secret's value is refused, so a secret cannot be handed to the agent by accident.
  • Anything the script writes to a file stays in the sandbox, where the agent can read it. Keep credentials in temporary files and remove them before the script ends — the private registry example shows how.

When the script fails

If setup fails decides what a failing script, a timeout or a secret that does not resolve does to the launch:

  • Continue with a warning (the default): the agent starts anyway, and the session shows a warning naming the step that failed.
  • Fail the launch: the launch stops and reports the failing step and exit code.

The timeout is 5 minutes unless you change it, up to 60. A script that installs large packages may need more.

To find out what went wrong, open the launch timeline: see A launch that will not finish.

Examples

Each example runs as it is in a sandbox; copy it and adjust it to your project. The same examples are beside the script editor, under Examples.

Start here

The shape every script shares: stop on the first error, name each step, and hand values and PATH entries to the agent.

starter.sh
#!/usr/bin/env bash
# Runs as the sandbox user, in the repository folder, before the agent starts.
# Stop at the first failing command so a broken setup is reported, not ignored.
set -euo pipefail

echo "::step::Check the toolchain"
node --version
python3 --version

echo "::step::Prepare the workspace"
mkdir -p "$HOME/.cache/my-project"

# Hand values to the agent: NAME=value lines go to $PS_ENV,
# extra PATH directories go to $PS_PATH.
echo "APP_ENV=development" >> "$PS_ENV"
echo "$HOME/.cache/my-project/bin" >> "$PS_PATH"

Node.js

Installs dependencies with pnpm, yarn or npm — whichever the lockfile names — and runs the build script if there is one.

node.sh
#!/usr/bin/env bash
# Node.js project: install dependencies with the package manager the lockfile names.
# Node 22 and 24 are preinstalled; a .nvmrc, .node-version or "engines.node" picks one.
set -euo pipefail
# Let corepack fetch pnpm or yarn without asking; nobody can answer a prompt here.
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0

echo "::step::Install dependencies"
if [ -f pnpm-lock.yaml ]; then
corepack pnpm install --frozen-lockfile
elif [ -f yarn.lock ]; then
corepack yarn install --immutable
elif [ -f package-lock.json ]; then
npm ci
elif [ -f package.json ]; then
npm install
else
echo "No package.json here; nothing to install."
fi

echo "::step::Build"
if [ -f package.json ] && jq -e '.scripts.build' package.json > /dev/null; then
npm run build
fi

Python

Creates a virtual environment in the repository and hands it to the agent, so its python and tools are the ones on the PATH.

python.sh
#!/usr/bin/env bash
# Python project: a virtual environment in the repository, handed to the agent.
set -euo pipefail

echo "::step::Create the virtual environment"
python3 -m venv .venv
.venv/bin/pip install --quiet --upgrade pip

echo "::step::Install dependencies"
if [ -f requirements.txt ]; then
.venv/bin/pip install --quiet -r requirements.txt
fi
if [ -f pyproject.toml ]; then
.venv/bin/pip install --quiet -e .
fi

# The agent's commands use this environment's python and tools.
echo "VIRTUAL_ENV=$PWD/.venv" >> "$PS_ENV"
echo "$PWD/.venv/bin" >> "$PS_PATH"

System packages and tools

Installs Ubuntu packages and a pinned command-line tool. ~/.local/bin is already on the agent's PATH.

system-packages.sh
#!/usr/bin/env bash
# System packages and extra command-line tools. The sandbox runs Ubuntu 22.04 and the
# script can use sudo without a password.
set -euo pipefail

echo "::step::Install system packages"
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
postgresql-client redis-tools graphviz

echo "::step::Install a pinned command-line tool"
# ~/.local/bin is already on the agent's PATH.
mkdir -p "$HOME/.local/bin"
curl -fsSL -o "$HOME/.local/bin/yq" \
"https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_$(dpkg --print-architecture)"
chmod +x "$HOME/.local/bin/yq"
yq --version

Databases for tests

Starts PostgreSQL and Redis and hands their connection URLs to the agent. Installing PostgreSQL takes a minute or two — set the timeout to 10 minutes.

services.sh
#!/usr/bin/env bash
# Databases for tests: PostgreSQL and Redis running in the sandbox.
# Installing PostgreSQL takes a minute or two; allow a timeout of 10 minutes.
set -euo pipefail

echo "::step::Install PostgreSQL and Redis"
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
postgresql redis-server

echo "::step::Start PostgreSQL"
sudo service postgresql start
sudo -u postgres psql -qc "CREATE ROLE app LOGIN SUPERUSER PASSWORD 'app';"
sudo -u postgres createdb -O app app_test

echo "::step::Start Redis"
# Services started here keep running after the script ends.
redis-server --daemonize yes --save "" --appendonly no

echo "DATABASE_URL=postgres://app:app@localhost:5432/app_test" >> "$PS_ENV"
echo "REDIS_URL=redis://localhost:6379/0" >> "$PS_ENV"

Private registry

Installs from a private npm registry with a token only the script sees. Add a secret reference named NPM_TOKEN to the profile, and replace @your-org and the registry URL with yours. The token is written to a temporary file that is deleted when the script ends.

private-registry.sh
#!/usr/bin/env bash
# Install from a private npm registry with a token the agent never sees.
# Add a secret reference named NPM_TOKEN to this profile first.
set -euo pipefail

echo "::step::Authenticate to the private registry"
# Keep the token in a temporary config that is deleted when the script ends,
# so it is not left on disk for the agent to read.
NPM_CONFIG_USERCONFIG="$(mktemp)"
export NPM_CONFIG_USERCONFIG
trap 'rm -f "$NPM_CONFIG_USERCONFIG"' EXIT
cat > "$NPM_CONFIG_USERCONFIG" <<NPMRC
@your-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
NPMRC

echo "::step::Install dependencies"
if [ -f package-lock.json ]; then
npm ci
elif [ -f package.json ]; then
npm install
fi

Go and Rust

Downloads Go modules and Rust crates ahead of time and puts an installed Go tool on the agent's PATH.

go-rust.sh
#!/usr/bin/env bash
# Go and Rust projects: warm the module caches and add developer tools.
# Go and a stable Rust toolchain are preinstalled.
set -euo pipefail

if [ -f go.mod ]; then
echo "::step::Download Go modules"
go mod download

echo "::step::Install Go tools"
go install golang.org/x/tools/cmd/goimports@latest
echo "$(go env GOPATH)/bin" >> "$PS_PATH"
fi

if [ -f Cargo.toml ]; then
echo "::step::Fetch Rust crates"
cargo fetch
fi