# How to Configure Lifecycle Hooks like `onSandboxReady` in Sandcastle

> Learn to configure Sandcastle lifecycle hooks like onSandboxReady by passing a hooks object to createSandbox or the init command. Execute host or sandbox commands in parallel on container start.

- Repository: [Matt Pocock/sandcastle](https://github.com/mattpocock/sandcastle)
- Tags: how-to-guide
- Published: 2026-05-24

---

**To configure lifecycle hooks like `onSandboxReady` in Sandcastle, pass a `hooks` object to the `createSandbox()` function or the CLI `init` command, defining commands for either the `host` (your local machine) or `sandbox` (inside the container) environments, which Sandcastle executes in parallel when the container starts.**

Sandcastle, an open-source sandboxing tool by mattpocock, lets you automate setup tasks through lifecycle hooks declared in the `hooks` option. These hooks run arbitrary commands automatically when a sandbox is created, allowing you to install dependencies, build projects, or notify external systems before the main agent work begins.

## Understanding Host vs. Sandbox Hooks

The hook system in [`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts) splits execution into two distinct groups that run in parallel when `onSandboxReady` fires:

- **Host hooks**: Execute on the developer's machine outside the container. These use the type `{ command: string; timeoutMs?: number }` (defined in [[`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts) lines 61-78](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts#L61-L78)) and run via `execAsync` in the host worktree directory.
- **Sandbox hooks**: Execute inside the sandbox container using the type `{ command: string; sudo?: boolean; timeoutMs?: number }`. These run via `SandboxService.exec()` (provided by [`src/SandboxFactory.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxFactory.ts)) and support an optional `sudo` flag for elevated permissions.

Both groups accept an `onSandboxReady` array containing hook definitions. According to the implementation in [[`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts) lines 25-40](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts#L25-L40), Sandcastle extracts `hooks?.sandbox?.onSandboxReady` and `hooks?.host?.onSandboxReady`, then executes all hooks simultaneously using `Effect.raceFirst` with a default **60-second timeout** (configurable per hook).

## Configuring `onSandboxReady` via the JavaScript/TypeScript API

When calling `createSandbox()` programmatically, include a `hooks` object with `sandbox` and/or `host` keys. Each key contains an `onSandboxReady` array of command objects.

```typescript
import { createSandbox } from "sandcastle";

await createSandbox({
  hostRepoDir: "/path/to/host/repo",
  sandboxRepoDir: "/home/agent/workspace",
  hooks: {
    sandbox: {
      onSandboxReady: [
        { command: "npm install", timeoutMs: 120_000 },
        { command: "npm run build", sudo: true },
      ],
    },
    host: {
      onSandboxReady: [
        { command: "touch host-ready.txt" },
      ],
    },
  },
});

```

Key implementation details from [[`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts) lines 25-28](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts#L25-L28):

- Commands execute in the order defined within each array, but host and sandbox groups run concurrently.
- The `timeoutMs` parameter overrides the default 60-second limit for individual hooks.
- Sandbox hooks respect the `sudo` boolean to run commands with elevated privileges inside the container.

## Configuring Hooks via the CLI

When using the `sandcastle init` command, create a `.sandcastle` configuration file (e.g., [`.sandcastle/run.ts`](https://github.com/mattpocock/sandcastle/blob/main/.sandcastle/run.ts)) that exports a default configuration object. The CLI automatically reads this file and passes the hooks to `createSandbox()`.

```typescript
// .sandcastle/run.ts
export default {
  hooks: {
    sandbox: {
      onSandboxReady: [
        { command: "npm install && npm run build" },
      ],
    },
    host: {
      onSandboxReady: [
        { command: "echo 'Sandbox is ready' > ready.txt" },
      ],
    },
  },
};

```

Then execute:

```bash
sandcastle init

```

The CLI invokes `createSandbox()` with the configuration above, triggering the `onSandboxReady` hooks as soon as the container is up. See the working example in the repository's [`.sandcastle/run.ts`](https://github.com/mattpocock/sandcastle/blob/main/.sandcastle/run.ts) file at [lines 63-70](https://github.com/mattpocock/sandcastle/blob/main/.sandcastle/run.ts#L63-L70).

## Handling Abort Signals and Timeouts

Lifecycle hooks integrate with standard `AbortSignal` patterns for cancellation. When you pass a `signal` to the `run()` function, Sandcastle forwards it to all hooks, allowing immediate cancellation if the user interrupts the process (e.g., pressing **Ctrl+C**).

```typescript
import { run } from "sandcastle";

const controller = new AbortController();
process.once("SIGINT", () => controller.abort());

await run({
  // ...other options,
  signal: controller.signal,
});

```

As implemented in [[`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts) lines 44-66](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts#L44-L66), if the signal fires during hook execution:

- In-flight hooks are immediately aborted.
- The system returns either a `HookTimeoutError` or `ExecError` depending on the failure mode.

Each hook runs within a timeout window defined by `HOOK_TIMEOUT_MS` (default 60000), wrapped in `Effect.raceFirst` to handle both timeout and abort conditions gracefully.

## Summary

- **Lifecycle hooks** like `onSandboxReady` automate setup tasks when Sandcastle creates a sandbox.
- **Configuration** happens via the `hooks` option in `createSandbox()` or a `.sandcastle` config file for the CLI.
- **Execution contexts** split between `host` (local machine) and `sandbox` (container), running in parallel per [`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts).
- **Customization** includes per-hook `timeoutMs` settings and `sudo` privileges for sandbox commands.
- **Cancellation** supports standard `AbortSignal` for graceful shutdowns.

## Frequently Asked Questions

### What is the default timeout for `onSandboxReady` hooks?

The default timeout is **60 seconds** (60000ms) per hook. You can override this for individual hooks by specifying the `timeoutMs` property in the hook configuration object.

### Do host and sandbox hooks run sequentially or in parallel?

Host-side and sandbox-side `onSandboxReady` hooks run **in parallel** with each other. However, hooks within the same array (e.g., multiple sandbox hooks) execute sequentially in the order defined. This parallel execution is implemented in [`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts) using `Effect.raceFirst` logic.

### Can I use `sudo` in host hooks?

No, the `sudo` option is only valid for **sandbox hooks**. Host hooks run as the current user on your development machine and do not support the `sudo` flag in their type definition. If you need elevated permissions on the host, run the CLI or script with appropriate system-level permissions.

### How do I cancel hooks if the user presses Ctrl+C?

Pass an `AbortController.signal` to the `run()` function. Sandcastle automatically forwards this signal to all lifecycle hooks. If triggered, in-flight hooks abort immediately and return a `HookTimeoutError` or `ExecError` as handled in the abort logic of [`src/SandboxLifecycle.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts).