Skip to content
Go back

How To Set Up Sandcastle With Codex

Posted on:June 9, 2026 at 11:00 AM

Sandcastle is tool to manage local sandboxed coding agents that you can run using TypeScript. Sandcastle is free to use and it is open source with an MIT license.

Sandcastle is an alternative to using something like docker sandbox create codex or sbx directly on your device. These are alternative tools I actually tried first, but they required a lot of configuration and I had issues getting those to work nicely in the end. And even after getting those to run nicely, I wouldn’t want to

Setting up Sandcastle itself is pretty straightforward. The part that took me a bit longer was getting it to work with Codex auth and Docker.

In this article, I’ll show you how I set up Sandcastle with Codex.

Why Sandcastle?

Instead of letting an agent loose directly in your working directory, Sandcastle creates a separate worktree, runs the agent there, and then lets you inspect the result. You can run it programmatically with and without sandboxes.

It supports different agents and different sandbox providers. In my case, I wanted to use:

What do you need?

To follow this setup, you need:

First, make sure the Codex CLI works on your machine:

which codex
codex --version

If that does not work, install it:

npm install -g @openai/codex

Then make sure you are logged in and that your ~/.codex/auth.json file exists.

Initializing Sandcastle

The easiest way to set up Sandcastle is to use sandcastle init.

This is the command I used:

npx sandcastle init \
  --image-name sandcastle:my-project \
  --sandbox docker \
  --agent codex \
  --model gpt-5.5 \
  --issue-tracker github-issues \
  --create-label true \
  --build-image true

Replace my-project with the name of your project.

The important part here is:

--build-image true

I initially skipped this and then ran into this error:

Image 'sandcastle:my-project' not found locally.
Build it first with 'sandcastle docker build-image'.

That just means Docker is running, but the Sandcastle image does not exist yet.

You can also build it manually later:

npx sandcastle docker build-image --image-name sandcastle:my-project

Installing Codex inside the Docker image

The host machine having codex installed is not enough when you use Docker.

The Docker container also needs the Codex CLI.

In .sandcastle/Dockerfile, add:

RUN npm install -g @openai/codex

For example:

FROM node:22-bookworm

RUN apt-get update && apt-get install -y \
  git \
  curl \
  jq \
  && rm -rf /var/lib/apt/lists/*

RUN npm install -g @openai/codex

USER agent

WORKDIR /home/agent

ENTRYPOINT ["sleep", "infinity"]
Full Example Dockerfile
FROM node:22-bookworm

# Install system dependencies
RUN apt-get update && apt-get install -y \
  git \
  curl \
  jq \
  && rm -rf /var/lib/apt/lists/*

# Install GitHub CLI
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
  | 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" \
  | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
  && apt-get update && apt-get install -y gh \
  && rm -rf /var/lib/apt/lists/*

# Build-args for UID/GID alignment: sandcastle docker build-image
# defaults these to the host user's UID/GID so image-built files
# and bind-mounted files share an owner without runtime chown.
ARG AGENT_UID=1000
ARG AGENT_GID=1000

# Rename the base image's "node" user to "agent" and align UID/GID.
RUN groupmod -o -g $AGENT_GID node && usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node

# Install Codex CLI (run as root before USER agent)
RUN npm install -g @openai/codex

USER ${AGENT_UID}:${AGENT_GID}

WORKDIR /home/agent

# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at /home/agent/workspace
# and overrides the working directory to /home/agent/workspace at container start.
# Structure your Dockerfile so that /home/agent/workspace can serve as the project root.
ENTRYPOINT ["sleep", "infinity"]

After changing the Dockerfile, rebuild the image:

npx sandcastle docker build-image --image-name sandcastle:my-project

If you forget this step, you will probably see:

sh: codex: command not found

Passing Codex auth into the container

This was the trickiest part.

Codex stores auth locally in ~/.codex. The Docker container does not automatically have access to that.

The setup I ended up using mounts the host ~/.codex directory read-only, then copies only the files Codex needs into the container’s Codex home.

In .sandcastle/main.ts, import os and path:

import os from "node:os";
import path from "node:path";

Then define these paths:

const hostCodexHome = path.join(os.homedir(), ".codex");
const sandboxCodexMount = "/mnt/host-codex";
const sandboxCodexHome = "/home/agent/.codex";

Then pass the mount and env vars to Docker:

sandbox: docker({
  env: {
    CODEX_HOME: sandboxCodexHome,
    GH_TOKEN: process.env.GH_TOKEN ?? "",
  },
  mounts: [
    {
      hostPath: hostCodexHome,
      sandboxPath: sandboxCodexMount,
      readonly: true,
    },
  ],
}),

Finally, copy the auth files when the sandbox starts:

hooks: {
  sandbox: {
    onSandboxReady: [
      {
        command: [
          `mkdir -p "${sandboxCodexHome}"`,
          `test -f "${sandboxCodexMount}/auth.json"`,
          `cp "${sandboxCodexMount}/auth.json" "${sandboxCodexHome}/auth.json"`,
          `if [ -f "${sandboxCodexMount}/config.toml" ]; then cp "${sandboxCodexMount}/config.toml" "${sandboxCodexHome}/config.toml"; fi`,
        ].join(" && "),
      },
      { command: "npm install" },
    ],
  },
},

The important detail is that CODEX_HOME is set to:

/home/agent/.codex

Sandcastle expects Codex sessions to show up there. I first used /tmp/codex-home, and Codex worked, but Sandcastle failed afterwards when trying to capture the session:

Session capture failed: session ... not found in /home/agent/.codex/sessions

So use /home/agent/.codex.

Full .sandcastle/main.ts example
import os from "node:os";
import path from "node:path";

import { run, codex } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";

const hostCodexHome = path.join(os.homedir(), ".codex");
const sandboxCodexMount = "/mnt/host-codex";
const sandboxCodexHome = "/home/agent/.codex";

// Simple loop: an agent that picks open issues one by one and closes them.
// Run this with: npx tsx .sandcastle/main.ts
// Or add to package.json scripts: "sandcastle": "npx tsx .sandcastle/main.ts"

await run({
  // A name for this run, shown as a prefix in log output.
  name: "worker",

  // Sandbox provider — runs the agent inside an isolated container.
  sandbox: docker({
    env: {
      CODEX_HOME: sandboxCodexHome,
      GH_TOKEN: process.env.GH_TOKEN ?? "",
    },
    mounts: [
      {
        hostPath: hostCodexHome,
        sandboxPath: sandboxCodexMount,
        readonly: true,
      },
    ],
  }),

  // The agent provider. Pass a model string to codex() — sonnet balances
  // capability and speed for most tasks. Switch to claude-opus-4-7 for harder
  // problems, or claude-haiku-4-5-20251001 for speed.
  agent: codex("gpt-5.5", { effort: "medium" }),

  // Path to the prompt file. Shell expressions inside are evaluated inside the
  // sandbox at the start of each iteration, so the agent always sees fresh data.
  promptFile: "./.sandcastle/prompt.md",

  // Maximum number of iterations (agent invocations) to run in a session.
  // Each iteration works on a single issue. Increase this to process more issues
  // per run, or set it to 1 for a single-shot mode.
  maxIterations: 3,

  // Branch strategy — merge-to-head creates a temporary branch for the agent
  // to work on, then merges the result back to HEAD when the run completes.
  // This is required when using copyToWorktree, since head mode bind-mounts
  // the host directory directly (no worktree to copy into).
  branchStrategy: { type: "merge-to-head" },

  // Lifecycle hooks — commands grouped by where they run (host or sandbox).
  hooks: {
    sandbox: {
      // onSandboxReady runs once after the sandbox is initialised and the repo is
      // synced in, before the agent starts. Use it to install dependencies or run
      // any other setup steps your project needs.
      onSandboxReady: [
        {
          command: [
            `mkdir -p "${sandboxCodexHome}"`,
            `test -f "${sandboxCodexMount}/auth.json"`,
            `cp "${sandboxCodexMount}/auth.json" "${sandboxCodexHome}/auth.json"`,
            `if [ -f "${sandboxCodexMount}/config.toml" ]; then cp "${sandboxCodexMount}/config.toml" "${sandboxCodexHome}/config.toml"; fi`,
          ].join(" && "),
        },
        { command: "npm install" },
      ],
    },
  },
});

You might not want to copy node_modules from macOS into Docker

The generated Sandcastle config may include this:

copyToWorktree: ["node_modules"],

That can be useful in some setups, but it caused problems for me because I was running Docker on macOS.

Native packages like better-sqlite3 were copied from macOS into a Linux container. Then tests failed with:

invalid ELF header

The fix was simple: remove copyToWorktree: ["node_modules"] and let the container run:

npm install

That way native dependencies are installed for Linux inside the container. It takes a bit longer so only do it if you need to.

Running Sandcastle

Once everything is set up, run:

npm run sandcastle

Or directly:

npx tsx .sandcastle/main.ts

If you want to rebuild the Docker image:

npx sandcastle docker build-image --image-name sandcastle:my-project

If you want to watch what the agent is doing, Sandcastle prints a log file path. You can tail it:

tail -f .sandcastle/logs/your-log-file.log

Common errors

codex: command not found

This means Codex is not installed in the environment where Sandcastle is running it.

For no-sandbox, install Codex on your host:

npm install -g @openai/codex

For Docker, install it in .sandcastle/Dockerfile and rebuild the image.

Image 'sandcastle:my-project' not found locally

Build the image:

npx sandcastle docker build-image --image-name sandcastle:my-project

Or rerun sandcastle init with:

--build-image true

Session capture failed

Check where CODEX_HOME points inside the container.

For Docker, I had to use:

CODEX_HOME=/home/agent/.codex

Using a temp directory worked for Codex itself, but Sandcastle could not find the session afterwards.

invalid ELF header

You probably copied host node_modules into a Linux container.

Remove:

copyToWorktree: ["node_modules"];

Then let the container run:

npm install

Conclusion

The basic Sandcastle setup is easy, but Codex with Docker needs a few extra details:

Once those pieces are in place, Sandcastle can run Codex in Docker and capture the session correctly.

Resources used