How to Configure Lifecycle Hooks like `onSandboxReady` in Sandcastle
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 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.tslines 61-78](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxLifecycle.ts#L61-L78)) and run viaexecAsyncin the host worktree directory. - Sandbox hooks: Execute inside the sandbox container using the type
{ command: string; sudo?: boolean; timeoutMs?: number }. These run viaSandboxService.exec()(provided bysrc/SandboxFactory.ts) and support an optionalsudoflag for elevated permissions.
Both groups accept an onSandboxReady array containing hook definitions. According to the implementation in [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.
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 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
timeoutMsparameter overrides the default 60-second limit for individual hooks. - Sandbox hooks respect the
sudoboolean 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) that exports a default configuration object. The CLI automatically reads this file and passes the hooks to createSandbox().
// .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:
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 file at lines 63-70.
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).
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 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
HookTimeoutErrororExecErrordepending 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
onSandboxReadyautomate setup tasks when Sandcastle creates a sandbox. - Configuration happens via the
hooksoption increateSandbox()or a.sandcastleconfig file for the CLI. - Execution contexts split between
host(local machine) andsandbox(container), running in parallel persrc/SandboxLifecycle.ts. - Customization includes per-hook
timeoutMssettings andsudoprivileges for sandbox commands. - Cancellation supports standard
AbortSignalfor 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →