Skip to main content
A setup script is a bash script that runs inside the sandbox container before your main code executes. There are two independent levels:
LevelDeclared inWhen it runsPersists?
Image-levelprebuiltImages[].setupScript in configBaked into the Docker image; runs on every execution against that imageYes — part of the image
Request-levelsetupScript in ExecutionRequestPer execution call, on the live containerNo — ephemeral by default
When both are present, the image-level script always runs first, then the request-level script.

Image-level setup scripts

Declare setupScript inside a prebuiltImages entry to bake recurring setup into a custom Docker image. The script is written to /sandbox/.isol8-setup.sh inside the image and executed automatically before every execution against that image. Use this for setup that is constant across executions: git identity, SSH configuration, tool symlinks, shell aliases, or any one-time environment preparation.
isol8.config.json
{
  "prebuiltImages": [
    {
      "tag": "my-org/python-devbox:latest",
      "runtime": "python",
      "installPackages": ["numpy", "pandas", "pytest", "ruff"],
      "setupScript": "git config --global user.name 'agent' && git config --global user.email 'agent@ci.internal'"
    }
  ]
}
Build the image (once, or on every deploy — isol8 skips unchanged images):
isol8 setup
Or pass the script inline when building from the CLI:
isol8 build \
  --base python \
  --install numpy pandas pytest ruff \
  --setup "git config --global user.name 'agent' && git config --global user.email 'agent@ci.internal'" \
  --tag my-org/python-devbox:latest

Content-addressed caching

isol8 hashes runtime + packages + setupScript to produce a deps hash stored as a Docker image label. If neither the packages nor the script has changed, isol8 setup skips the build entirely. Changing a single character in the script invalidates the hash and triggers a rebuild.
# Safe to call on every deploy — rebuilds only when content changes
isol8 setup

Multi-line scripts

Supply a multi-line script using a JSON string with \n, or use a separate shell file with --setup ./setup.sh in the CLI:
isol8.config.json
{
  "prebuiltImages": [
    {
      "tag": "my-org/node-devbox:latest",
      "runtime": "node",
      "installPackages": ["typescript", "eslint", "prettier"],
      "setupScript": "git config --global user.name 'agent'\ngit config --global user.email 'agent@ci.internal'\ngit config --global core.autocrlf false\nnpm config set update-notifier false"
    }
  ]
}
# With the CLI, pass a path to a .sh file
isol8 build \
  --base node \
  --install typescript eslint prettier \
  --setup ./scripts/devbox-setup.sh \
  --tag my-org/node-devbox:latest

Request-level setup scripts

Pass setupScript directly in an ExecutionRequest to run ad-hoc setup on the container before your code. This is useful for tasks that differ per execution: cloning a specific repo, writing config files, setting environment variables from secrets, or installing a package not in the base image.
import { DockerIsol8 } from "@isol8/core";

const engine = new DockerIsol8({ network: "filtered", networkFilter: { whitelist: ["^github\\.com$"], blacklist: [] } });
await engine.start();

const result = await engine.execute({
  runtime: "python",
  setupScript: `
    git clone https://$GITHUB_TOKEN@github.com/my-org/my-repo.git /sandbox/repo
    cd /sandbox/repo && git checkout main
  `,
  code: `
    import subprocess
    result = subprocess.run(["python", "-m", "pytest", "/sandbox/repo/tests/", "-q"], capture_output=True, text=True)
    print(result.stdout)
  `,
  env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN! },
  timeoutMs: 120_000,
});

await engine.stop();

Execution environment

Both image-level and request-level scripts run with the same constraints:
  • User: sandbox (uid 100, non-root)
  • Working directory: /sandbox
  • Shell: bash
  • Timeout: subject to the same timeoutMs as the full execution request
  • Exit code: a non-zero exit from the setup script aborts the execution and throws Setup script failed (exit code N): <stderr>
  • Secrets: values from the engine’s secrets option are available as environment variables and are automatically masked in stdout/stderr

Execution order

When both levels are set, the order is deterministic:
1. Image-level setup script   (from prebuiltImages[].setupScript, baked into image)
2. Request-level setup script (from ExecutionRequest.setupScript, per-call)
3. Main code / agent prompt

Common patterns

Git identity for agent runs

Bake git identity into the image so every agent step can commit without extra setup:
{
  "setupScript": "git config --global user.name 'isol8-agent' && git config --global user.email 'agent@ci.internal' && git config --global core.autocrlf false"
}

Clone a private repo before execution

Use a request-level script to clone a specific branch per task:
await engine.execute({
  runtime: "bash",
  setupScript: `
    git clone https://$GITHUB_TOKEN@github.com/my-org/my-repo.git /sandbox/repo
    cd /sandbox/repo
    git checkout -b agent/task-${taskId} origin/main
  `,
  code: "echo 'repo ready'",
});

Configure package registries

Point npm or pip to a private registry for every execution against a custom image:
{
  "setupScript": "npm config set registry https://npm.my-org.internal && pip config set global.index-url https://pypi.my-org.internal/simple"
}

Install a non-standard tool

Use a request-level script to install a tool that isn’t in the base image:
await engine.execute({
  runtime: "bash",
  setupScript: "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo 'deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main' | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y",
  code: "gh --version",
});
For tools needed on every run, always prefer baking them into a custom image via prebuiltImages[].setupScript or installPackages. Request-level installs run on every call and add latency.

Write config files before agent runs

Inject .npmrc, .gitconfig, or other config files before the agent starts:
await engine.execute({
  runtime: "agent",
  setupScript: `
    cat > /sandbox/.npmrc << 'EOF'
registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=$NPM_TOKEN
EOF
  `,
  code: "Add unit tests for the auth module",
  agentFlags: "--model anthropic/claude-sonnet-4-5",
});

Warm up a persistent session

In persistent mode, run setup once at the start of the session rather than on every request:
const engine = new DockerIsol8({ mode: "persistent", network: "filtered", networkFilter: { whitelist: ["^github\\.com$", "^api\\.anthropic\\.com$"], blacklist: [] } });
await engine.start();

// One-time setup at session start
await engine.execute({
  runtime: "bash",
  setupScript: `
    git clone https://$GITHUB_TOKEN@github.com/my-org/my-repo.git /sandbox/repo
    cd /sandbox/repo && git checkout -b agent/fix-issue origin/main
    npm ci
  `,
  code: "echo 'workspace ready'",
  env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN! },
});

// Subsequent calls reuse the prepared workspace
await engine.execute({ runtime: "agent", code: "Fix the type errors in src/parser.ts" });
await engine.execute({ runtime: "bash", code: "cd /sandbox/repo && npx tsc --noEmit" });

await engine.stop();

Error handling

If the setup script exits non-zero, isol8 throws before running your code:
Error: Setup script failed (exit code 1): fatal: repository 'https://github.com/my-org/private-repo.git/' not found
Always check that secrets (tokens, passwords) are available before running scripts that depend on them:
await engine.execute({
  runtime: "bash",
  setupScript: `
    : "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
    git clone https://$GITHUB_TOKEN@github.com/my-org/my-repo.git /sandbox/repo
  `,
  code: "ls /sandbox/repo",
});