For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /automations/cloud-setup-hook.md.

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 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. 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.

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 plugin, and the check 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

#!/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:

$ 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 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:

    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 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:

$ 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.