gdkbox
Spin up a GitLab Development Kit (GDK) "in a box" with a single command, then connect to it from VS Code and run Claude Code agents against a real GitLab development environment.
gdkbox runs GitLab's official GDK image in a Docker container, enables SSH so
VS Code Remote-SSH can attach, and installs Claude Code inside the box for you.
gdkbox up myenv # pull GDK image, start container, wire SSH + Claude Code
gdkbox code myenv # open the box in VS Code (Remote-SSH)
gdkbox ssh myenv # drop into a shell inside the box
gdkbox ls # list your boxes and their status
How it works
gdkbox up demo
│
┌──────────────┼─────────────────────────────────────────────┐
│ ▼ │
│ 1. docker pull <official GDK image> │
│ 2. docker run -p 127.0.0.1:2222:22 -p 127.0.0.1:3000:3000
│ 3. provision install + start sshd, authorize gdkbox key │
│ 4. provision npm install -g @anthropic-ai/claude-code │
│ 5. register Host gdkbox-demo -> ~/.gdkbox/ssh_config │
└──────────────────────────────────────────────────────────────┘
│
┌───────────┴────────────┐
▼ ▼
VS Code Remote-SSH ssh gdkbox-demo -t claude
(ssh-remote+gdkbox-demo) (run agents in the box)
- Infra: a Docker container per box, ports published to
127.0.0.1. - Image: GitLab's official GDK-in-a-box image (overridable).
- Editor: VS Code Remote-SSH, via a generated
~/.gdkbox/ssh_configthat isIncluded from your~/.ssh/config. - Agents: Claude Code is installed inside each box.
Requirements
- Ruby >= 3.0
- Docker installed and running
sshandssh-keygen(standard on macOS/Linux)- VS Code with the Remote - SSH
extension, and the
codecommand on yourPATH(optional, only forgdkbox code)
Install
From this directory:
gem build gdkbox.gemspec
gem install ./gdkbox-0.1.0.gem
Or run it straight from a checkout without installing:
./bin/gdkbox up myenv
Commands
| Command | Description |
|---|---|
gdkbox up NAME |
Pull the GDK image, start a box, enable SSH, install Claude Code, register VS Code host. |
gdkbox ls |
List boxes and their container status (--json for orchestrators). |
gdkbox status NAME |
Show container state and connection details (--json). |
gdkbox dispatch NAME |
Run a Claude Code agent task headlessly in the box (--task/--task-file). |
gdkbox code NAME |
Open the box in VS Code via Remote-SSH. |
gdkbox ssh NAME |
Open an interactive SSH session into the box. |
gdkbox claude NAME |
Install Claude Code inside the box. |
gdkbox set-key NAME |
Seed/rotate the Anthropic API key in the box for unattended dispatch. |
gdkbox start NAME |
Start a stopped box (and re-enable SSH). |
gdkbox stop NAME |
Stop a running box. |
gdkbox rm NAME |
Remove a box: container, metadata, and SSH entry. |
gdkbox install-skill [NAME] |
Install the agent skill(s) bundled in this repo into your host Claude Code (--project, --force). |
gdkbox skills |
List agent skills available to install (bundled + ~/.claude/skills + ./.claude/skills). |
gdkbox add-skill BOX [SKILL...] |
Install skill(s) into a box so dispatched agents can use them; interactive picker if no SKILL (--force). |
gdkbox completion SHELL |
Print a bash or zsh completion script (completes subcommands, flags, live box and skill names). |
gdkbox harnesses |
List the agent harnesses gdkbox can install (claude, codex, opencode, pi). |
gdkbox install-agent NAME |
(Re)install the box's agent harness inside it. |
gdkbox up options
| Option | Default | Description |
|---|---|---|
--image |
official GDK image | Override the container image. |
--ssh-port |
next free from 2222 | Host port to publish SSH on. |
--web-port |
next free from 3000 | Host port to publish the GDK web UI on. |
--harness ID |
claude (or config.yml) |
Agent harness to install: claude, codex, opencode, pi. |
--no-agent |
(agent installed) | Skip installing the agent harness. |
--skill NAME [NAME...] |
(none) | Seed skill(s) into the box at creation for dispatched agents. |
Typical workflow
# 1. Create a box (first run pulls a large image, so give it a few minutes)
gdkbox up demo
# 2. Edit the code with VS Code Remote-SSH
gdkbox code demo
# 3. Inside the box, start GDK and run an agent
gdkbox ssh demo
# gdk start # boot the GitLab services
# claude # launch a Claude Code agent against the repo
# 4. The GitLab web UI is published locally
open http://127.0.0.1:3000
# 5. Stop or remove the box when you're done
gdkbox stop demo
gdkbox rm demo
Orchestrating a fleet of agents
The end goal of gdkbox is to back an orchestrator that runs a pool of
reusable boxes and dispatches a Claude Code agent into each to execute tasks in
parallel. Two primitives make this possible:
- Headless agent runs:
gdkbox dispatch NAME --task "..."runsclaude -pnon-interactively inside the box and streams the agent's output. Exit status mirrors the agent's. Add--jsonfor structured output and--timeout Nto bound a run. - Machine-readable state:
gdkbox ls --json/gdkbox status NAME --jsonemit JSON descriptors so an orchestrator can see which boxes exist and their state.
For unattended dispatch the agents need Anthropic credentials. Seed an API
key into each box (kept only inside the container, in a 0600 file owned by the
GDK user — never in host-side metadata). gdkbox up and gdkbox set-key both
default to the ANTHROPIC_API_KEY environment variable:
export ANTHROPIC_API_KEY=sk-ant-...
# Warm a pool of 3 boxes (in parallel; first run pulls a large image).
# The key is seeded automatically because ANTHROPIC_API_KEY is set.
for i in 1 2 3; do gdkbox up "pool-$i" --json & done; wait
# Or seed/rotate the key on existing boxes:
gdkbox set-key pool-1
# Fan three tasks out, one per box. Agents authenticate with the seeded key.
gdkbox dispatch pool-1 --task "Run the test suite and fix the first failure" --json &
gdkbox dispatch pool-2 --task "Update the README install section" --json &
gdkbox dispatch pool-3 --task "Add a changelog entry" --json &
wait
gdkbox ls --json reports "api_key_set": true|false per box so an
orchestrator can tell which boxes are ready for unattended work. Prefer the
ANTHROPIC_API_KEY env var over --anthropic-api-key, which can leak into
shell history.
In this repo the orchestrator is meant to be another Claude Code session,
guided by the bundled gdkbox-fleet skill at
skills/gdkbox-fleet/SKILL.md,
which documents the pool model, dispatch loop, state-reset-between-tasks
caveat, and guardrails.
Installing the skill
The skills shipped with this repo live in the top-level
skills/ directory. Install them into Claude Code with:
# Install every bundled skill into ~/.claude/skills (available everywhere)
gdkbox install-skill
# Install one skill, into the current project only
gdkbox install-skill gdkbox-fleet --project
# Re-install over an existing copy
gdkbox install-skill gdkbox-fleet --force
This copies each skill verbatim into ~/.claude/skills/<name> (or
./.claude/skills/<name> with --project). Start a new Claude Code session
afterwards to pick it up.
Giving skills to agents in a box
install-skill targets your host Claude Code. Agents dispatched into a box
(gdkbox dispatch) run inside the container and read the box's own
~/.claude/skills, so they need the skill installed there. Skills can come
from this repo (skills/), your host (~/.claude/skills), your project
(./.claude/skills), or any directory path.
# See everything gdkbox can install (bundled + host + project), with summaries
gdkbox skills
# Install skill(s) into a running box; agents dispatched there can then use them
gdkbox add-skill pool-1 gdkbox-fleet
gdkbox add-skill pool-1 ./path/to/my-skill --force
# No skill given → list and pick interactively
gdkbox add-skill pool-1
# Or seed skills when the box is first created
gdkbox up pool-1 --skill gdkbox-fleet --skill code-history
Each skill is copied to ~/.claude/skills/<name> inside the box and owned by
the GDK user, so a dispatched claude agent discovers it automatically.
Agent harnesses
Each box runs one agent harness, chosen at gdkbox up --harness (default
claude, or the harness: in config.yml). gdkbox dispatch then runs whatever
harness the box was created with.
| Harness | CLI | Headless dispatch | Default key env | Skills dir (in box) |
|---|---|---|---|---|
claude |
Claude Code | claude -p |
ANTHROPIC_API_KEY |
~/.claude/skills |
codex |
OpenAI Codex | codex exec |
OPENAI_API_KEY |
~/.codex/skills |
opencode |
opencode (sst) | opencode run |
OPENAI_API_KEY |
~/.config/opencode/skills |
pi |
pi (earendil-works) | pi -p |
ANTHROPIC_API_KEY |
~/.pi/skills |
gdkbox up reviewer --harness codex # a Codex box
gdkbox set-key reviewer # seeds $OPENAI_API_KEY
gdkbox add-skill reviewer my-skill # installs into ~/.codex/skills
gdkbox dispatch reviewer --task "review the diff"
gdkbox harnesses # list available harnesses
Skills use the same SKILL.md format across all four, so a skill installed by
add-skill/--skill works regardless of harness. The key env / provider for
opencode and pi are configurable defaults — adjust per your provider.
Shell completion
gdkbox completion <shell> prints a completion script for bash or zsh. It
completes subcommands and flags, and — dynamically, by calling back into
gdkbox — live box names (for ssh, dispatch, rm, …) and skill
names (for add-skill, install-skill, and --skill).
# bash — add to ~/.bashrc
eval "$(gdkbox completion bash)"
# zsh — add to ~/.zshrc (after `compinit` has run)
eval "$(gdkbox completion zsh)"
Or write it to your completions directory, e.g.
gdkbox completion bash > $(brew --prefix)/etc/bash_completion.d/gdkbox.
Configuration
| Environment variable | Purpose |
|---|---|
GDKBOX_HOME |
Where gdkbox stores keys, box metadata, and the SSH config (default ~/.gdkbox). |
GDKBOX_IMAGE |
Default image used by gdkbox up when --image is not given. |
config.yml
Defaults for gdkbox up can be set in ~/.gdkbox/config.yml (or
$GDKBOX_HOME/config.yml). Most usefully, list skills to transfer into every
new box so dispatched agents always have them:
# ~/.gdkbox/config.yml
# Default image for `gdkbox up` (overridden by --image or $GDKBOX_IMAGE).
# image: registry.gitlab.com/gitlab-org/gitlab-development-kit/gitlab-gdk-in-a-box:main
# Default agent harness for `gdkbox up` (overridden by --harness).
# harness: claude
# Skills transferred into every new box. Each entry is a discovered skill
# name (see `gdkbox skills`) or a path to a skill directory.
skills:
- gdkbox-fleet
# - code-history
# - ./path/to/a/local/skill
# API keys seeded into boxes for unattended `gdkbox dispatch`, per provider.
# anthropic_api_key: sk-ant-... # used by the claude / pi harnesses
# openai_api_key: sk-... # used by the codex / opencode harnesses
gdkbox up installs the skills on top of any --skill you pass; use
--no-default-skills to skip them for one run. Precedence for the image is
--image > $GDKBOX_IMAGE > config.yml > built-in default; for the API key
it is --api-key > the harness's provider env var (e.g. $ANTHROPIC_API_KEY,
$OPENAI_API_KEY) > config.yml.
API keys in
config.ymlare plaintext secrets on disk. Prefer the provider env var; if you do put a key inconfig.yml, keep the file mode600(chmod 600 ~/.gdkbox/config.yml).
State on disk:
~/.gdkbox/
├── config.yml # optional; defaults for `gdkbox up`
├── boxes/<name>.json # metadata for each box
├── keys/id_ed25519(.pub) # dedicated keypair authorized into every box
└── ssh_config # generated; Included from ~/.ssh/config
gdkbox never touches your personal SSH keys: it creates and uses its own
id_ed25519 under ~/.gdkbox/keys.
Development
bundle install
bundle exec rspec # or: ruby -e "require 'rspec/core'; RSpec::Core::Runner.run(['spec'])"
The Docker, SSH, and filesystem boundaries are isolated behind small wrapper
classes (Docker, Shell, Store, SSHConfig, SSHKey, Provisioner,
VSCode), so the orchestration logic in Box is fully unit-tested without
needing a running Docker daemon.
Notes & limitations
- Ports are published to
127.0.0.1only, so boxes are reachable from your machine but not the wider network. - Processes started via
docker exec(likesshd) do not survive a container restart, sogdkbox startre-runs the SSH provisioning step to bring it back. - Claude Code is installed in the box but still needs your Anthropic
credentials at runtime — sign in the first time you run
claudeinside it.