> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Cloud setup hook

Your tests pass on your laptop because it has your toolchain. A cloud run starts from a fresh clone on a clean machine. If the repository needs a tool or dependency that machine lacks, the factory agent can't build or test your change, or the run fails before the agent starts.

The cloud setup hook fixes this. It's a script you commit at `.tessl/cloud-launch-setup.sh`, and Tessl runs it to prepare the machine before each loop run and cloud launch in your repository.

## What the hook is

- **Location:** `.tessl/cloud-launch-setup.sh`, at the root of your repository. The path is fixed. If the file isn't there, Tessl skips this step.
- **When it runs:** while Tessl prepares a cloud run, after it clones your repository and before it installs the run's skill and starts the agent. It runs on loops that start an agent, such as Ticket to PR, and on `tessl launch skill --cloud`. Tessl Code Review and vulnerability remediation runs don't use it.
- **Which version runs:** the hook on the branch the run starts from, usually your default branch. A hook on another branch has no effect until you merge it, so check it on a branch first, as [Check the hook on a branch](#check-the-hook-on-a-branch) describes.
- **Working directory:** the clone root. The script runs with `bash`, so it doesn't need to be executable.
- **Environment:** the run's environment variables, including the secrets in the run's [environment](/schedules/environments.md). `WORKDIR` holds the subdirectory the run works in (empty for the repository root), and `AGENT` holds the agent that will run.
- **Time limit:** preparation, including the hook, must finish within 30 minutes. The hook's time counts toward the run's timeout.
- **Reruns:** a resumed run prepares the machine again and runs the hook again, so the hook must be safe to run more than once.

## Cloud setup isn't eval setup

Tessl has two setup mechanisms with similar names. They don't share anything.

- **Cloud setup hook:** `.tessl/cloud-launch-setup.sh` in your repository. It prepares the cloud machine for loops, factory automations, and cloud launches.
- **Eval scenario setup:** a `setup.sh` next to a scenario's `task.md`, or the `setup` list in `scenario.json`. It prepares one eval scenario's working directory. See [scenario.json](/reference/configuration.md#scenario-json).

## What the cloud machine already has

The cloud machine runs Ubuntu with passwordless `sudo`, Docker with Compose, `build-essential`, Node.js, Python 3 with uv, Go, and Bun already installed. The current versions are listed in the setup hook reference of the [`tessl-labs/factory-setup`](https://tessl.io/registry/tessl-labs/factory-setup) plugin, and the [check](#check-the-hook-on-a-branch) prints the versions it finds on the machine.

The machine has 4 vCPUs, 8 GiB of memory with no swap, and a 20 GB disk with about 12 GB free. `/tmp` is held in memory (tmpfs, up to about 4 GiB) and counts against the 8 GiB, so put large files in `/var/tmp` or in the workspace.

Install only what your repository needs on top of that, and only what a cloud run needs to build and test. Leave out CI-only steps, such as browser downloads, unless your tests need them.

## Write the hook

```bash
#!/bin/bash
set -eo pipefail

echo "Installing dependencies from the lockfile"
npm ci

echo "Installing jq"
if ! command -v jq >/dev/null; then
  sudo apt-get update
  sudo apt-get install -y jq
fi
```

Follow these rules:

- **Start with `set -eo pipefail`.** Installers are often pipelines, such as `curl ... | bash`, and `set -e` alone lets a failed download pass. Leave out `-u`, because some version managers, such as `nvm.sh`, aren't safe under it.
- **Echo each step before you run it.** When the hook fails, the output then shows which step broke.
- **Install from your lockfile.** Use `npm ci`, `bun install --frozen-lockfile`, `uv sync --frozen`, or your package manager's equivalent, so the cloud run gets the same dependencies as CI.
- **Accept a version range.** Use the tool already on the machine when it satisfies the range your repository declares in `engines`, `.nvmrc`, `.tool-versions`, `go.mod`, or `.python-version`. Don't pin the exact patch version CI uses.
- **Prefer your package-manager pin.** When `package.json` sets `packageManager`, set `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` and run `corepack enable --install-directory ~/.local/bin`.
- **Put installed tools in `~/.local/bin`.** The hook runs in a child process, so `export PATH=...`, `nvm use`, or sourcing a profile doesn't reach the agent. Install tools into `~/.local/bin`, or link them there.
- **Use `sudo apt-get install -y` for system packages.**
- **Fail with a clear message.** When no installed or installable version satisfies the range, exit non-zero and name the tool, the range, and the version you found.
- **Make it safe to run twice.** Skip work that's already done, as the `jq` step above does.

Node.js is the one tool you shouldn't replace. Cloud runs use the Node.js on the machine. If your repository needs an older major version than the machine has, older Node.js versions aren't supported in cloud runs yet.

## What happens when the hook fails

The hook must exit with status 0. A non-zero exit fails the run while Tessl prepares it: the agent never starts, nothing reaches your repository, and `tessl launch view` shows the run as `error`. In its `--json` output, `data.attributes.status` is `failed`.

The run's preparation progress names the step that failed. For example, this run failed in the hook:

```sh
$ tessl launch progress 01a17562-7b84-70d7-9b38-298773321d15 --filename prepare-subphases.json
{"schema":1,"role":null,"outcome":"failed","first_failed":"cloud-launch-setup","total_ms":29340,"truncated":false,"subphases":[...,{"name":"clone","ms":25600,"outcome":"ok"},{"name":"workdir","ms":10,"outcome":"ok"},{"name":"cloud-launch-setup","ms":1200,"outcome":"failed"}]}
```

- `outcome` is `failed`.
- `first_failed` is `cloud-launch-setup`, which means your hook exited non-zero. Any other value names a different preparation step, such as `clone` or `skill-install`.
- `subphases` lists each step in order with its outcome. Steps after the failure don't appear.

`tessl launch view <id>` shows the run as `Error (RECIPE_FAILED)`. For workspace admins, the lines below the error hold the failure details, including the failing command. A hook that fails during preparation leaves no agent transcript, so `tessl launch logs` has nothing to show for it.

To find and fix the cause:

1. Confirm the failing step with `tessl launch progress <id> --filename prepare-subphases.json`.
2. Run the hook from a fresh clone of the branch on your own machine: `bash .tessl/cloud-launch-setup.sh`. The step it echoed last is the one that broke.
3. If it passes locally, check what it assumes about the machine: a tool, version, or path the cloud machine doesn't have.
4. Fix the hook on a branch, and [check it on that branch](#check-the-hook-on-a-branch) before you merge.

When you change a hook that's already on your default branch, every cloud run uses the new version as soon as you merge it. A broken hook stops every loop in the repository until you fix it, so check before you merge.

## Check the hook on a branch

Check a new or changed hook on a cloud machine before it reaches your default branch. The check is a cloud run of the `check-cloud-setup` skill from the `tessl-labs/factory-setup` plugin. It runs the branch's hook twice, which also shows the hook is safe to rerun, and then reports the versions of the tools on the machine. It changes nothing in your repository. Like any cloud run, it uses credits.

If you set up your factory with the `factory-setup` skill, the agent runs this check for you before it merges the hook. To run it yourself:

1. Push the hook to a branch, for example `add-setup-hook`.

2. From your repository, launch the check against your default branch, naming the branch to check:

   ```sh
   tessl launch skill tessl-labs/factory-setup#check-cloud-setup --cloud \
     --repo <owner/repo> --base-branch main \
     --environment <environment> --agent claude-code \
     --instructions "Branch: add-setup-hook" \
     --label "Check the setup hook" --no-wait --json
   ```

   Use the environment your factory's loops use, so the check runs with the same secrets. The check only reads the branch, so `read` GitHub access is enough. Keep the `launchRunId` from the output.

3. Check the run's status with `tessl launch view <id> --json`. A run usually finishes within 10 minutes.

4. When `data.attributes.status` is `completed` and `data.attributes.outcome.status` is `succeeded`, the hook passed twice. `data.attributes.result.summary` lists each tool on the machine and its version. Merge the hook.

5. When `data.attributes.outcome.status` is `failed`, `data.attributes.result.summary` names the step that failed and `data.attributes.result.reason` holds the last lines of the hook's output. Fix the hook on the branch, push, and check again.

6. When the run itself fails, read its preparation progress as [What happens when the hook fails](#what-happens-when-the-hook-fails) describes. If `first_failed` is `cloud-launch-setup`, the hook on your default branch failed while Tessl prepared the check: fix that hook first. Any other value names a different preparation step.

## Confirm a run prepared successfully

After you merge the hook, any cloud run's preparation progress shows whether the hook ran and passed:

```sh
$ tessl launch progress <id> --filename prepare-subphases.json
{"schema":1,"role":null,"outcome":"ok","first_failed":null,"total_ms":47500,"truncated":false,"subphases":[...,{"name":"clone","ms":19170,"outcome":"ok"},{"name":"workdir","ms":0,"outcome":"ok"},{"name":"cloud-launch-setup","ms":4250,"outcome":"ok"},...,{"name":"skill-install","ms":17260,"outcome":"ok"},...]}
```

`outcome` is `ok`, `first_failed` is `null`, and the `cloud-launch-setup` step is `ok`. A `skipped` outcome on that step means the run found no hook on the branch it started from.

## Related

- [Environments](/schedules/environments.md): the secrets and GitHub access a cloud run gets.
- [`tessl launch skill`](/reference/cli-commands.md#tessl-launch-skill): run a skill in the cloud.
- [`tessl launch view`](/reference/cli-commands.md#tessl-launch-view): a run's status and result.
