Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/sandbox-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
branches: [main]
paths:
- sandbox/**
# Daily rebuild so Claude Code and the bunny CLI pick up new releases without
# a commit. The Dockerfile keys each install layer on the upstream version, so
# a day with no releases is a fully cached no-op.
# Off the hour on purpose — the top of the hour is the busiest Actions slot.
schedule:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ok with this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude code does a lot of releases so it's a hassle to follow. and since it only effects the new sandboxes it seems fine.

- cron: "17 4 * * *"
workflow_dispatch:

jobs:
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,9 +853,9 @@ bunny scripts docs

### `bunny sandbox`

Manage on-demand cloud sandbox environments backed by Bunny Magic Containers. Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, and Claude Code pre-installed. A 10 GB persistent volume is mounted at `/workplace`, your default working directory.
Manage on-demand cloud sandbox environments backed by Bunny Magic Containers. Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, the bunny CLI, and Claude Code pre-installed, alongside the tooling agents reach for: `git`, `gh`, `ripgrep`, `fd`, `jq`, `tmux`, `sqlite3`, `tree`, and `fzf`. A 10 GB persistent volume is mounted at `/workplace`, your default working directory.

Claude Code is pre-installed but needs your own Anthropic credentials before it can do anything: pass an API key at create time (prefer `--env-file .env` so the key stays out of your shell history), or run `claude` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and the login flow writes to `/workplace/.claude` on the persistent volume.
Claude Code is pre-installed but needs your own Anthropic credentials before it can do anything: pass an API key at create time (prefer `--env-file .env` so the key stays out of your shell history), or run `claude` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and config and credentials are pinned to the persistent volume — `/workplace/.claude` for Claude Code, and `/workplace/.config` for the bunny CLI and `gh`.

Sandbox credentials (app ID, SSH endpoint, agent token) are stored in the CLI's local config file (`~/.config/bunnynet.json` by default) so you can reconnect without re-creating.

Expand Down
72 changes: 60 additions & 12 deletions sandbox/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# Ubuntu leaves the locale unset, which makes agent TUIs and Python misrender
# non-ASCII output. C.UTF-8 is built in, so this needs no locales package.
# Repeated in /etc/environment below, because ENV does not reach SSH sessions.
ENV LANG=C.UTF-8

# System tools + SSH + Python
RUN apt-get update && apt-get install -y \
openssh-server \
Expand All @@ -12,19 +17,34 @@ RUN apt-get update && apt-get install -y \
net-tools iputils-ping \
unzip zip \
jq \
ripgrep fd-find \
tmux \
sqlite3 tree file fzf \
python3 python3-pip python3-venv python3-dev \
build-essential \
&& ln -sf /usr/bin/python3 /usr/bin/python \
&& ln -sf /usr/bin/pip3 /usr/bin/pip \
&& ln -sf /usr/bin/fdfind /usr/local/bin/fd \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/run/sshd \
&& sed -i 's|#\?Port 22$|Port 8023|' /etc/ssh/sshd_config \
&& sed -i 's|#\?PermitRootLogin.*|PermitRootLogin yes|' /etc/ssh/sshd_config \
&& sed -i 's|#\?PasswordAuthentication.*|PasswordAuthentication yes|' /etc/ssh/sshd_config

# GitHub CLI (pinned apt repo with a verified signing key, same pattern as Node
# below) — agents need it for PR and issue work; plain git cannot do either.
RUN install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y gh \
&& rm -rf /var/lib/apt/lists/*

# Node.js (pinned NodeSource apt repo with a verified signing key — no curl | bash).
# apt then cryptographically verifies the nodejs package against the pinned key.
ARG NODE_MAJOR=22
ARG NODE_MAJOR=24
RUN install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
-o /etc/apt/keyrings/nodesource.asc \
Expand All @@ -49,19 +69,47 @@ RUN set -eux; \
rm -rf /tmp/bun.zip /tmp/bun
ENV PATH="/root/.bun/bin:$PATH"

# Claude Code
RUN install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://downloads.claude.ai/keys/claude-code.asc \
-o /etc/apt/keyrings/claude-code.asc \
&& echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main" \
> /etc/apt/sources.list.d/claude-code.list \
&& apt-get update && apt-get install -y claude-code \
&& rm -rf /var/lib/apt/lists/*
# Claude Code — official installer, always the newest release.
# Installs the launcher into /root/.local/bin; the installer itself
# checksum-verifies the binary it downloads.
#
# The ADD below is the cache key: its content is the current version string, so
# Docker re-runs the install exactly when a new release ships and reuses the
# layer when it hasn't. Kept after the apt/Node/Bun layers on purpose — busting
# it only reruns the cheap steps that follow.
ADD https://downloads.claude.ai/claude-code-releases/latest /tmp/claude-code-version
RUN curl -fsSL https://claude.ai/install.sh | bash \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail the build when downloading the Claude installer fails

If curl fails before producing installer content—for example because claude.ai returns an error or has a transient connection failure—the pipeline's status is still the status of bash, which exits successfully on empty input because pipefail is not enabled. The workflow can consequently publish an image with no claude executable even though the build passed; download the script before executing it or enable pipefail for this layer.

Useful? React with 👍 / 👎.

&& rm -f /tmp/claude-code-version
ENV PATH="/root/.local/bin:$PATH"

# bunny CLI — this repo, published to npm. No install scripts; the platform
# binary comes in as an optional dependency. Its config follows
# XDG_CONFIG_HOME, which is pinned to /workplace below, so credentials persist
# without any extra wiring.
ADD https://registry.npmjs.org/@bunny.net/cli/latest /tmp/bunny-cli-version
RUN npm install -g @bunny.net/cli@latest \
&& rm -f /tmp/bunny-cli-version

# Workplace — SSH sessions land here, bun on PATH, Claude config lives here
ENV CLAUDE_CONFIG_DIR=/workplace/.claude
# Workplace — SSH sessions land here, and Claude Code, the bunny CLI and gh all
# keep their config and credentials on the persistent volume (the latter two via
# XDG_CONFIG_HOME, which they both honour).
#
# These have to go in /etc/environment, not just ENV: sshd builds a fresh
# environment per session, so Docker ENV never reaches an SSH login. pam_env
# (UsePAM yes) reads this file for both interactive and non-interactive
# sessions, which is also why PATH is spelled out here — .bashrc alone misses
# `ssh <host> <cmd>`. ENV is kept as well so `docker exec` and the entrypoint
# see the same values.
ENV CLAUDE_CONFIG_DIR=/workplace/.claude \
XDG_CONFIG_HOME=/workplace/.config
RUN mkdir -p /workplace \
&& printf 'cd /workplace\nexport PATH="/root/.bun/bin:$PATH"\n' >> /root/.bashrc
&& printf '%s\n' \
'PATH=/root/.local/bin:/root/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
'LANG=C.UTF-8' \
'CLAUDE_CONFIG_DIR=/workplace/.claude' \
'XDG_CONFIG_HOME=/workplace/.config' \
>> /etc/environment \
&& printf 'cd /workplace\n' >> /root/.bashrc

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
Expand Down
7 changes: 7 additions & 0 deletions sandbox/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ if [ -z "${AGENT_TOKEN}" ]; then
fi

echo "root:${AGENT_TOKEN}" | chpasswd

# The persistent volume mounts over /workplace after the image is built, so the
# config directories have to be created here — anything created at build time is
# hidden by the mount.
mkdir -p /workplace/.claude \
/workplace/.config

exec /usr/sbin/sshd -D
4 changes: 2 additions & 2 deletions skills/bunny-cli/references/sandbox.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Sandbox Commands

All sandbox commands live under `bunny sandbox`. Each sandbox is a fully isolated Ubuntu container (Bunny Magic Containers) with Node.js, Bun, Python, and Claude Code pre-installed. A 10 GB persistent volume is mounted at `/workplace`, the default working directory; relative remote paths resolve against it.
All sandbox commands live under `bunny sandbox`. Each sandbox is a fully isolated Ubuntu container (Bunny Magic Containers) with Node.js, Bun, Python, the bunny CLI, and Claude Code pre-installed, alongside the tooling agents reach for: `git`, `gh`, `ripgrep`, `fd`, `jq`, `tmux`, `sqlite3`, `tree`, and `fzf`. A 10 GB persistent volume is mounted at `/workplace`, the default working directory; relative remote paths resolve against it.

Sandbox credentials (app ID, SSH endpoint, agent token) are stored in the CLI's local config (same candidate paths as in SKILL.md), so commands reference sandboxes by name.

Claude Code is pre-installed but needs the user's own Anthropic credentials: bake an API key in at create time (prefer `--env-file .env` so the key stays out of shell history), set it later with `bunny sandbox env set`, or run `claude` inside the sandbox and complete the login prompt. Both paths survive restarts and redeploys: baked env vars live on the container, and interactive login writes to `/workplace/.claude` (the image pins `CLAUDE_CONFIG_DIR` there) on the persistent volume.
Claude Code is pre-installed but needs the user's own Anthropic credentials: bake an API key in at create time (prefer `--env-file .env` so the key stays out of shell history), set it later with `bunny sandbox env set`, or run `claude` inside the sandbox and complete the login prompt. Both paths survive restarts and redeploys: baked env vars live on the container, and interactive logins write to the persistent volume. The image pins config directories there via `/etc/environment` (Docker `ENV` alone does not reach SSH sessions): `CLAUDE_CONFIG_DIR=/workplace/.claude`, plus `XDG_CONFIG_HOME=/workplace/.config`, which the bunny CLI and `gh` both honour. The entrypoint creates these at startup because the volume mounts over anything the image created at build time.

## Typical workflows

Expand Down
Loading