How to Configure Lifecycle Hooks in Sandcastle: Complete Guide to onWorktreeReady

Configure lifecycle hooks in Sandcastle by passing a hooks object to run() or createWorktree(), using host.onWorktreeReady to execute commands after the worktree is created but before the sandbox starts.

Sandcastle is a sandboxing framework for running AI agents in isolated environments. When you configure lifecycle hooks, you can execute custom commands at precise moments in the sandbox lifecycle—such as installing dependencies or preparing files before the container launches. The onWorktreeReady hook is particularly useful because it fires immediately after the git worktree is created, allowing you to modify the workspace before Sandcastle starts the Docker, Podman, or bind-mount sandbox.

Understanding the onWorktreeReady Hook

The host.onWorktreeReady hook executes after the worktree is created and after any copyToWorktree files are copied, but before the sandbox container starts. This timing ensures you can perform host-side setup operations while having access to the complete worktree directory.

According to the Sandcastle source code, this hook is triggered in three locations:

The hook system is defined by the SandboxHooks type in src/SandboxLifecycle.ts, which also exports the runHostHooks helper function that executes your commands.

How to Configure onWorktreeReady in run()

The most common way to configure lifecycle hooks is through the run() function's options object.

Basic Configuration

Pass a hooks object with host.onWorktreeReady containing an array of command objects:

import { run } from "sandcastle";
import { docker } from "sandcastle/sandboxes";

await run({
  agent: claudeCode("claude-opus-4-7"),
  sandbox: docker({ imageName: "myrepo/sandcastle:latest" }),
  prompt: "Please refactor the utils module.",
  hooks: {
    host: {
      onWorktreeReady: [{ command: "npm install" }],
    },
  },
});

In this example, Sandcastle executes npm install inside the worktree directory immediately after creation, then proceeds to launch the Docker sandbox only if the command succeeds.

Multiple Commands and Custom Timeouts

You can chain multiple commands with individual timeouts. The default timeout is 60 seconds (HOOK_TIMEOUT_MS = 60000 in src/SandboxLifecycle.ts), but you can override this per command:

await run({
  agent: claudeCode("claude-3-5-sonnet"),
  sandbox: docker({ imageName: "node:18" }),
  prompt: "Analyze the codebase.",
  hooks: {
    host: {
      onWorktreeReady: [
        { command: "npm ci", timeoutMs: 30_000 },
        { command: "npm run generate-types", timeoutMs: 45_000 },
        { command: "git status" },
      ],
    },
  },
});

Commands run sequentially using execAsync. If any command fails or times out, Sandcastle throws an ExecError or HookTimeoutError and aborts the entire run() operation.

Combining with copyToWorktree

The onWorktreeReady hook works seamlessly with the copyToWorktree option, which copies files from the host into the worktree before the hook runs:

await run({
  agent: claudeCode("claude-3-opus"),
  sandbox: docker({ imageName: "python:3.11" }),
  prompt: "Run the test suite.",
  copyToWorktree: ["node_modules", ".env.local"],
  hooks: {
    host: {
      onWorktreeReady: [
        { command: "npm rebuild" },
        { command: "chmod +x scripts/test.sh" },
      ],
    },
  },
});

As implemented in src/CopyToWorktree.ts (lines 33-40), the copy operation completes before runHostHooks executes your commands. This sequence allows you to rebuild native modules or set permissions on copied files before the sandbox starts.

Low-Level API Configuration

When using the low-level createWorktree() API directly, you configure hooks identically through the options parameter:

import { createWorktree } from "sandcastle";

const worktree = await createWorktree({
  branchStrategy: { type: "merge-to-head" },
  copyToWorktree: ["scripts", "config"],
  hooks: {
    host: {
      onWorktreeReady: [
        { command: "pip install -r requirements.txt", timeoutMs: 120_000 },
        { command: "python scripts/setup.py" },
      ],
    },
  },
});

const result = await worktree.run({
  agent: claudeCode("claude-3-5-sonnet"),
  prompt: "Review the configuration.",
});

This approach is defined in src/createWorktree.ts and follows the same execution order: worktree creation → file copying → hook execution → sandbox readiness.

Technical Implementation Details

Under the hood, runHostHooks (from src/SandboxLifecycle.ts) handles the execution:

  1. Worktree Creation – WorktreeManager.create establishes a temporary git worktree
  2. File Copying – Optional copyToWorktree paths are processed
  3. Hook Execution – runHostHooks receives the command array and executes each via execAsync with the configurable timeout
  4. Sandbox Launch – Only after successful hook completion does Sandcastle proceed to host.onSandboxReady or start the sandbox container

The current hook types available in src/SandboxLifecycle.ts include:

  • host.onWorktreeReady – After worktree creation, before sandbox start
  • host.onSandboxReady – After sandbox container is running, still on the host
  • sandbox.onSandboxReady – Inside the sandbox container itself

Summary

  • Configure lifecycle hooks in Sandcastle by adding a hooks object to your run() or createWorktree() configuration
  • host.onWorktreeReady executes after the git worktree is created and files are copied, but before the sandbox container launches
  • Commands run sequentially via runHostHooks in src/SandboxLifecycle.ts, with a default 60-second timeout that you can override per command
  • Combine with copyToWorktree to prepare files on the host before running setup commands in the hook
  • Error handling is strict—any hook failure aborts the entire operation with ExecError or HookTimeoutError

Frequently Asked Questions

What is the exact timing of onWorktreeReady relative to the sandbox container?

The onWorktreeReady hook runs before the sandbox container starts. Specifically, it executes after the git worktree is created and any copyToWorktree files are copied, but before Sandcastle initializes Docker, Podman, or bind-mount sandboxes. This timing allows you to modify worktree files or install host-side tools that affect the upcoming sandbox session.

Can I run hooks inside the sandbox container instead of on the host?

Yes, use sandbox.onSandboxReady instead of host.onWorktreeReady. While host.onWorktreeReady and host.onSandboxReady execute on the host machine, sandbox.onSandboxReady runs commands inside the sandbox container itself. This is useful for container-specific setup like installing additional packages or configuring the container environment, whereas host.onWorktreeReady is better for repository preparation.

What happens if an onWorktreeReady command times out?

If a command exceeds its specified timeoutMs (or the default 60,000ms), Sandcastle throws a HookTimeoutError and aborts the entire run() operation. The sandbox container will not start, and the temporary worktree will be cleaned up. You can catch this error to implement fallback logic or increase the timeout for long-running setup scripts.

Can I use shell features like pipes or redirection in hook commands?

No, runHostHooks uses execAsync which executes commands directly without shell interpolation. For complex operations requiring pipes, redirection, or shell variables, create a script file in your repository and call that script from the hook: { command: "./scripts/setup.sh" }. Ensure the script is executable or use an interpreter explicitly: { command: "bash ./scripts/setup.sh" }.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →