From 72ab54e098ad2b996af8a34cf0c2fe54979be429 Mon Sep 17 00:00:00 2001 From: Amir Keshavarz Date: Mon, 27 Jul 2026 11:59:16 +0200 Subject: [PATCH 1/3] chore(sandbox): Node 24, agent CLIs, persistent config, agent tooling --- packages/cli/README.md | 4 +- sandbox/Dockerfile | 97 ++++++++++++++++++++++---- sandbox/entrypoint.sh | 12 ++++ skills/bunny-cli/references/sandbox.md | 4 +- 4 files changed, 101 insertions(+), 16 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index c26f48bf..2bcf9538 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -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, Claude Code, Codex CLI, and opencode 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. +The agent CLIs are pre-installed but each needs your own credentials before it can do anything: pass keys at create time (prefer `--env-file .env` so they stay out of your shell history), or run `claude`, `codex`, or `opencode` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and each CLI's config, credentials, and session history are pinned to the persistent volume — `/workplace/.claude` for Claude Code, `/workplace/.codex` for Codex, and `/workplace/.config/opencode` plus `/workplace/.local/share/opencode` for opencode. 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. diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index bf2683e1..6ee90e0d 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -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 \ @@ -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 \ @@ -49,19 +69,72 @@ 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 \ + && rm -f /tmp/claude-code-version +ENV PATH="/root/.local/bin:$PATH" + +# Codex CLI — installed from the npm registry, which integrity-checks the +# tarball, rather than the piped installer the docs lead with. The package has +# no install scripts; the platform binary comes in as an optional dependency, +# so amd64 and arm64 both resolve automatically. +# +# Same cache-key trick as above: this manifest changes only when a new version +# is published, so @latest is re-resolved exactly then. +ADD https://registry.npmjs.org/@openai/codex/latest /tmp/codex-version +RUN npm install -g @openai/codex@latest \ + && rm -f /tmp/codex-version + +# opencode — same reasoning as Codex: npm over the piped installer, so the +# tarball is integrity-checked. Its postinstall picks the matching platform +# binary out of the optional dependencies, so this resolves per build platform. +ADD https://registry.npmjs.org/opencode-ai/latest /tmp/opencode-version +RUN npm install -g opencode-ai@latest \ + && rm -f /tmp/opencode-version \ + && rm -rf /root/.config/opencode /root/.local/share/opencode \ + /root/.local/state/opencode /root/.cache/opencode + +# 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 every agent CLI keeps its config, +# credentials and session history on the persistent volume. +# +# 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 `. ENV is kept as well so `docker exec` and the entrypoint +# see the same values. +ENV CLAUDE_CONFIG_DIR=/workplace/.claude \ + CODEX_HOME=/workplace/.codex \ + XDG_CONFIG_HOME=/workplace/.config \ + XDG_DATA_HOME=/workplace/.local/share \ + XDG_STATE_HOME=/workplace/.local/state 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' \ + 'CODEX_HOME=/workplace/.codex' \ + 'XDG_CONFIG_HOME=/workplace/.config' \ + 'XDG_DATA_HOME=/workplace/.local/share' \ + 'XDG_STATE_HOME=/workplace/.local/state' \ + >> /etc/environment \ + && printf 'cd /workplace\n' >> /root/.bashrc COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/sandbox/entrypoint.sh b/sandbox/entrypoint.sh index c911aff8..171388a0 100644 --- a/sandbox/entrypoint.sh +++ b/sandbox/entrypoint.sh @@ -7,4 +7,16 @@ if [ -z "${AGENT_TOKEN}" ]; then fi echo "root:${AGENT_TOKEN}" | chpasswd + +# The persistent volume mounts over /workplace after the image is built, so the +# agent config directories have to be created here — anything created at build +# time is hidden by the mount. Codex is strict about this: it aborts with +# "CODEX_HOME points to ... but that path does not exist" rather than creating +# it, so a missing directory would break `codex` entirely. +mkdir -p /workplace/.claude \ + /workplace/.codex \ + /workplace/.config \ + /workplace/.local/share \ + /workplace/.local/state + exec /usr/sbin/sshd -D diff --git a/skills/bunny-cli/references/sandbox.md b/skills/bunny-cli/references/sandbox.md index ed1e0184..959e647b 100644 --- a/skills/bunny-cli/references/sandbox.md +++ b/skills/bunny-cli/references/sandbox.md @@ -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, Claude Code, Codex CLI, and opencode 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. +The agent CLIs are pre-installed but each needs the user's own credentials: bake a key in at create time (prefer `--env-file .env` so it stays out of shell history), set it later with `bunny sandbox env set`, or run `claude`, `codex`, or `opencode` 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 each CLI's directories there via `/etc/environment` (Docker `ENV` alone does not reach SSH sessions): `CLAUDE_CONFIG_DIR=/workplace/.claude`, `CODEX_HOME=/workplace/.codex`, and `XDG_CONFIG_HOME`/`XDG_DATA_HOME`/`XDG_STATE_HOME` under `/workplace` for opencode. Only caches stay ephemeral. The entrypoint creates these at startup because the volume mounts over anything the image created at build time — Codex refuses to start if `CODEX_HOME` is missing. ## Typical workflows From 33972f9ac94ac90c6ddeffde9f99ac2b177fea80 Mon Sep 17 00:00:00 2001 From: Amir Keshavarz Date: Mon, 27 Jul 2026 12:02:58 +0200 Subject: [PATCH 2/3] chore(sandbox): add daily docker build --- .github/workflows/sandbox-agent.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/sandbox-agent.yml b/.github/workflows/sandbox-agent.yml index a570f563..6735d37d 100644 --- a/.github/workflows/sandbox-agent.yml +++ b/.github/workflows/sandbox-agent.yml @@ -5,6 +5,12 @@ on: branches: [main] paths: - sandbox/** + # Daily rebuild so the agent CLIs (Claude Code, Codex, opencode, bunny) 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: + - cron: "17 4 * * *" workflow_dispatch: jobs: From 6f3048c4030b9a23e70172e3aa7ef970e2ca9957 Mon Sep 17 00:00:00 2001 From: Amir Keshavarz Date: Mon, 27 Jul 2026 13:40:50 +0200 Subject: [PATCH 3/3] chore(sandbox): remove codex and opencode from the main sandbox image --- .github/workflows/sandbox-agent.yml | 6 ++--- packages/cli/README.md | 4 ++-- sandbox/Dockerfile | 33 ++++---------------------- sandbox/entrypoint.sh | 11 +++------ skills/bunny-cli/references/sandbox.md | 4 ++-- 5 files changed, 14 insertions(+), 44 deletions(-) diff --git a/.github/workflows/sandbox-agent.yml b/.github/workflows/sandbox-agent.yml index 6735d37d..3d160f4c 100644 --- a/.github/workflows/sandbox-agent.yml +++ b/.github/workflows/sandbox-agent.yml @@ -5,9 +5,9 @@ on: branches: [main] paths: - sandbox/** - # Daily rebuild so the agent CLIs (Claude Code, Codex, opencode, bunny) 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. + # 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: - cron: "17 4 * * *" diff --git a/packages/cli/README.md b/packages/cli/README.md index 2bcf9538..dc22e665 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -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, the bunny CLI, Claude Code, Codex CLI, and opencode 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. +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. -The agent CLIs are pre-installed but each needs your own credentials before it can do anything: pass keys at create time (prefer `--env-file .env` so they stay out of your shell history), or run `claude`, `codex`, or `opencode` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and each CLI's config, credentials, and session history are pinned to the persistent volume — `/workplace/.claude` for Claude Code, `/workplace/.codex` for Codex, and `/workplace/.config/opencode` plus `/workplace/.local/share/opencode` for opencode. +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. diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index 6ee90e0d..c0ab0af9 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -82,26 +82,6 @@ RUN curl -fsSL https://claude.ai/install.sh | bash \ && rm -f /tmp/claude-code-version ENV PATH="/root/.local/bin:$PATH" -# Codex CLI — installed from the npm registry, which integrity-checks the -# tarball, rather than the piped installer the docs lead with. The package has -# no install scripts; the platform binary comes in as an optional dependency, -# so amd64 and arm64 both resolve automatically. -# -# Same cache-key trick as above: this manifest changes only when a new version -# is published, so @latest is re-resolved exactly then. -ADD https://registry.npmjs.org/@openai/codex/latest /tmp/codex-version -RUN npm install -g @openai/codex@latest \ - && rm -f /tmp/codex-version - -# opencode — same reasoning as Codex: npm over the piped installer, so the -# tarball is integrity-checked. Its postinstall picks the matching platform -# binary out of the optional dependencies, so this resolves per build platform. -ADD https://registry.npmjs.org/opencode-ai/latest /tmp/opencode-version -RUN npm install -g opencode-ai@latest \ - && rm -f /tmp/opencode-version \ - && rm -rf /root/.config/opencode /root/.local/share/opencode \ - /root/.local/state/opencode /root/.cache/opencode - # 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 @@ -110,8 +90,9 @@ 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, and every agent CLI keeps its config, -# credentials and session history on the persistent volume. +# 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 @@ -120,19 +101,13 @@ RUN npm install -g @bunny.net/cli@latest \ # `ssh `. ENV is kept as well so `docker exec` and the entrypoint # see the same values. ENV CLAUDE_CONFIG_DIR=/workplace/.claude \ - CODEX_HOME=/workplace/.codex \ - XDG_CONFIG_HOME=/workplace/.config \ - XDG_DATA_HOME=/workplace/.local/share \ - XDG_STATE_HOME=/workplace/.local/state + XDG_CONFIG_HOME=/workplace/.config RUN mkdir -p /workplace \ && 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' \ - 'CODEX_HOME=/workplace/.codex' \ 'XDG_CONFIG_HOME=/workplace/.config' \ - 'XDG_DATA_HOME=/workplace/.local/share' \ - 'XDG_STATE_HOME=/workplace/.local/state' \ >> /etc/environment \ && printf 'cd /workplace\n' >> /root/.bashrc diff --git a/sandbox/entrypoint.sh b/sandbox/entrypoint.sh index 171388a0..c409925f 100644 --- a/sandbox/entrypoint.sh +++ b/sandbox/entrypoint.sh @@ -9,14 +9,9 @@ fi echo "root:${AGENT_TOKEN}" | chpasswd # The persistent volume mounts over /workplace after the image is built, so the -# agent config directories have to be created here — anything created at build -# time is hidden by the mount. Codex is strict about this: it aborts with -# "CODEX_HOME points to ... but that path does not exist" rather than creating -# it, so a missing directory would break `codex` entirely. +# config directories have to be created here — anything created at build time is +# hidden by the mount. mkdir -p /workplace/.claude \ - /workplace/.codex \ - /workplace/.config \ - /workplace/.local/share \ - /workplace/.local/state + /workplace/.config exec /usr/sbin/sshd -D diff --git a/skills/bunny-cli/references/sandbox.md b/skills/bunny-cli/references/sandbox.md index 959e647b..6a294a2a 100644 --- a/skills/bunny-cli/references/sandbox.md +++ b/skills/bunny-cli/references/sandbox.md @@ -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, the bunny CLI, Claude Code, Codex CLI, and opencode 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. +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. -The agent CLIs are pre-installed but each needs the user's own credentials: bake a key in at create time (prefer `--env-file .env` so it stays out of shell history), set it later with `bunny sandbox env set`, or run `claude`, `codex`, or `opencode` 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 each CLI's directories there via `/etc/environment` (Docker `ENV` alone does not reach SSH sessions): `CLAUDE_CONFIG_DIR=/workplace/.claude`, `CODEX_HOME=/workplace/.codex`, and `XDG_CONFIG_HOME`/`XDG_DATA_HOME`/`XDG_STATE_HOME` under `/workplace` for opencode. Only caches stay ephemeral. The entrypoint creates these at startup because the volume mounts over anything the image created at build time — Codex refuses to start if `CODEX_HOME` is missing. +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