Reusable .devcontainer/ toolkit for projects that want the normal VS Code Dev
Containers workflow with the current Codegeist/planner development toolchain.
This repository is a reusable devcontainer kit that can be added to other
repositories at .devcontainer/, either as a Git subtree or as a Git submodule.
The main branch is the canonical source and contribution target. The generated
release branch is the runtime-only tree intended for consuming
.devcontainer/ submodules and is not an implementation target.
The source Dockerfile.base intentionally carries the full
Codegeist/planner-style toolchain, including Docker CE, Node 24, VS Code,
GitHub CLI, Maven, GraalVM, JBang, Hugo, Nix, PowerShell through pwsh, Task
with Bash completion, OpenCode tooling, the Codegeist CLI installed through the
upstream Linux installer, Repomix, Kubernetes and infrastructure CLIs, QEMU/KVM
virtualization tools, terminal capture tools, espeak-ng, network diagnostics,
security scan tools, password-store tooling through pass, and related CLI
tools. The release build publishes this file as
.devcontainer/Dockerfile for consuming repositories.
Project work is tracked through GitHub Issues, the Codegeist roadmap listing, and resumable local specifications described in the task guide. See CONTRIBUTING.md for the source workflow and extension boundaries. Codegeist's shared Code of Conduct, Security Policy, and Support Policy apply to this repository.
The consuming project should use the standard VS Code flow:
- Clone the consuming repository.
- Open the repository folder in VS Code.
- Run
Dev Containers: Reopen in Container.
The kit should not require a root-level launcher such as start.sh for normal
VS Code usage. It should also not open VS Code from its own scripts. VS Code and
the Dev Containers extension own the container lifecycle.
The devcontainer user follows the host $USER: remoteUser and
containerUser both use ${localEnv:USER}. initialize.sh writes matching
generated Docker build arguments and numeric runtime user values so the user
exists in the image and bind-mounted files use the host UID for both the
numeric user and group.
The same configuration can be smoke-tested without opening VS Code by running the Dev Containers CLI against the repository root:
npx --yes @devcontainers/cli up --workspace-folder <repo-root>VS Code opens the container workspace at an absolute host-matching path. Without
BRANCH, that path resolves back to the repository root. With BRANCH,
initializeCommand creates or reuses the matching Git worktree and
devcontainer.json opens .worktrees/<branch> as the remote workspace while
docker-compose.yml still mounts the repository root at its host path for
linked-worktree Git metadata. The generated Compose project name is
branch-first, for example develop0-myrepo, so multiple Remote SSH windows for
different branches get separate workspace containers. Keeping stable
per-checkout paths prevents OpenCode sessions from being mixed across projects or
branches.
Use one of the two installation modes below to place this kit at
.devcontainer/ in a consuming repository. After installing it, add these local
files to the consuming repository's .gitignore:
/.codegeist/.local.env
/.oc_local/
/.worktrees/
/.chrome/Ignore /.oc_local/ only when the consuming repository does not intentionally
track project-specific OpenCode overlay files there. Do not ignore
.codegeist/compose.local.yml or .codegeist/Dockerfile if the repository
creates them for intentional Compose or image overrides; they should stay
visible to Git.
If these patterns are missing, initialize.sh adds them to the repository root
.gitignore. It never writes generated-file ignores to .git/info/exclude, so
review and commit intentional .gitignore changes like normal repository state.
Open the consuming repository root in VS Code and run
Dev Containers: Reopen in Container:
code .To select a managed Git worktree from VS Code Remote SSH, set BRANCH in the
SSH environment and reopen the repository root in the container. The Dev
Containers lifecycle creates .worktrees/<branch> and opens that checkout as
the remote workspace. If BRANCH names the already checked-out branch, such as
BRANCH=main on main, .worktrees/<branch> is a symlink alias back to the
repository root. The Docker Compose project name is generated as
<branch-slug>-<repo-slug>, so two SSH hosts with different BRANCH values run
parallel containers such as codegeist-cloud-server-myrepo-workspace-1 and
install-scripts-myrepo-workspace-1:
Host project-dev0
SetEnv BRANCH=develop0The same path can be smoke-tested with the Dev Containers CLI:
BRANCH=develop0 npx --yes @devcontainers/cli up --workspace-folder <repo-root>For a plain local VS Code command where an existing VS Code process may not inherit new environment variables, prepare the worktree from the consuming repository root and then open that checkout:
BRANCH=develop0 .devcontainer/initialize.sh
code .worktrees/develop0The first start creates local and generated files when missing:
.codegeist/.local.env.chrome/.devcontainer/.env.devcontainer/.Xauthority.gen.devcontainer/Dockerfile.merged.gen.devcontainer/compose.local.gen.yml.devcontainer/compose.user.gen.yml, an ignored bridge to optional.codegeist/compose.local.ymloverrides- root
.oc_local/with a local.gitignorefor workspace-specific OpenCode config, when missing - root
.worktrees/;.worktrees/<branch>as a worktree or current-branch symlink alias whenBRANCHis set
The kit also ships .oc_local.opencode.json.example as an inactive template for
.oc_local/opencode.json. When consumed as .devcontainer/, copy
.devcontainer/.oc_local.opencode.json.example to .oc_local/opencode.json
only when the consuming project wants a tracked local OpenCode overlay. The
template loads README.md first, then local rule files with the rules/**/*.md
instruction pattern; in the runtime release, that README.md is generated from
README_release.md.
The initializer creates writable .oc_local/ and .oc_local/.gitignore, but it
never copies the template or overwrites .oc_local/opencode.json. If a consuming
repository tracks .oc_local/, remove or narrow generated ignores and keep
secrets out of tracked local overlay files.
When upgrading an older checkout, initialize.sh copies legacy root .local.env
or compose.local.yml into the matching .codegeist/ path only when the new
file does not exist. It does not delete the legacy files and does not migrate a
root Dockerfile; move devcontainer image extensions to .codegeist/Dockerfile
manually if needed.
Do not edit .devcontainer/.env, .devcontainer/.Xauthority.gen,
.devcontainer/Dockerfile.merged.gen, .devcontainer/compose.local.gen.yml, or
.devcontainer/compose.user.gen.yml; they are regenerated by initialize.sh.
Put manual runtime overrides in
.codegeist/.local.env, local Compose overrides in .codegeist/compose.local.yml,
and devcontainer image extensions in .codegeist/Dockerfile instead. Create the
Compose and Dockerfile override files only when the repository needs them.
Consuming repositories can extend the devcontainer image without editing the
.devcontainer/ submodule by adding .codegeist/Dockerfile only when a local
image extension is needed. During initializeCommand, initialize.sh writes
.devcontainer/Dockerfile.merged.gen from the release kit base at
.devcontainer/Dockerfile and appends root .codegeist/Dockerfile as a
project-local fragment when that file exists.
Create the extension from the template on demand:
mkdir -p .codegeist
cp .devcontainer/Dockerfile.example .codegeist/DockerfileA root Dockerfile remains available for application images and is not treated
as a devcontainer extension. Treat .codegeist/Dockerfile only as an extension
fragment:
# .codegeist/Dockerfile - project-local devcontainer extension
USER root
RUN npm install -g some-coding-agent-tool
USER ${CONTAINER_USER}Do not put FROM in the .codegeist/Dockerfile fragment. A FROM instruction
would start a new stage and can replace the prepared kit image, so
initialize.sh rejects it with a clear error. COPY and ADD paths are still
resolved from the consuming repository root because the Docker build context
remains the project root.
docker-compose.yml builds .devcontainer/Dockerfile.merged.gen; do not edit or
commit that generated file.
Consuming repositories can override Compose settings without editing the
.devcontainer/ submodule by creating .codegeist/compose.local.yml from the
template only when an override is needed:
mkdir -p .codegeist
cp .devcontainer/compose.local.yml.example .codegeist/compose.local.ymlinitialize.sh writes .devcontainer/compose.user.gen.yml on every start. The
generated bridge is an empty services: {} file by default, or a copy of
.codegeist/compose.local.yml when that on-demand override exists.
The devcontainer image includes Google Chrome for browser checks and visible
browser sessions that must run from inside the container's network, DNS, and
certificate trust context. The shared kit installs a chrome launcher for direct
visible browser startup when the devcontainer has access to a host display, and
the same launcher supports deterministic headless automation for tests. The image
also includes Xvfb for tools that need a virtual X11 display without a host UI.
It does not add VNC, noVNC, bookmarks, credentials, or project-specific service
URLs. Plain visible Chrome stores its project-local state under the opened
workspace's ignored .chrome/ directory.
Run visible Chrome from a terminal inside the devcontainer when you need to load a URL with container-side DNS and certificates:
chrome https://example.testThe visible command does not start VNC or noVNC, and it does not treat a non-empty
display variable as proof that a display server is reachable. During
initializeCommand, initialize.sh detects an existing host Wayland socket from
WAYLAND_DISPLAY and XDG_RUNTIME_DIR (or /run/user/<uid>/wayland-0) and adds a
generated bind for only that socket. When the mounted socket is reachable, the
launcher prefers it, removes inherited DISPLAY, and starts Chrome with
--ozone-platform=wayland. A local X11 value such as DISPLAY=:0 remains usable
only when /tmp/.X11-unix/X0 exists inside the container; the shared Compose
config does not mount local X11 sockets by default.
VS Code SSH reconnects can allocate a new loopback display number while reusing
an existing container. Each initialize run atomically refreshes the selected
workspace's .devcontainer/.env and ignored .devcontainer/.Xauthority.gen.
The launcher rereads those files on every visible start, probes
DISPLAY=localhost:N.0 or 127.0.0.1:N.0 with a short xdpyinfo check, and, if
needed, normalizes the matching /unix:N cookie through unique temporary
Xauthority aliases. It
exits before Google Chrome starts when no candidate is reachable. Worktrees keep
separate generated state and .chrome profiles, so multiple VS Code instances
on one host do not overwrite each other's runtime display files. Explicit
non-loopback X11 hosts remain caller-managed.
Wayland discovery can mount only a socket that exists when the container is
created. initialize.sh cannot create a graphical host session, and a socket
that appears later cannot be added to an already running container without
recreation. SSH X11 reconnect recovery does not have that limitation because it
uses host networking and workspace-local refreshed authority state. Use
chrome --headless ... when no visible backend is available; broad host access
such as xhost + is neither required nor recommended.
Plain visible chrome uses $DEVCONTAINER_WORKSPACE_FOLDER/.chrome unless the
caller passes an explicit --user-data-dir. Visible Chrome also disables
container-expensive defaults such as background networking, component updates,
extensions, sync, translation, notifications, audio, and GPU acceleration. The
kit does not mount a hostwide shared Playwright/CDP profile because Chrome locks
profile directories and parallel projects can block each other.
In this repository, the same command can be exercised from the kit image:
task browser-open-testPass a URL after -- when you want the visible test fixture to open a specific
page instead of its local data URL default.
For interactive account sign-in, start Chrome directly from a terminal with
chrome. Do not use the OpenCode/Playwright MCP browser session for account
login flows; it is automation-controlled through Chrome DevTools Protocol, and
providers such as Google can reject it as an insecure browser or app. Use an
explicit project-local profile when you need repeatable login state for one
project:
chromeThe default visible profile is .chrome in the opened workspace and is ignored
by Git. Pass a different --user-data-dir only when you need another isolated
profile.
Do not point Playwright/CDP at Chrome's default profile such as
~/.config/google-chrome; Chrome blocks remote debugging for the default data
directory, and symlinks to that directory are still detected as the default
profile.
For non-interactive tests and automation, use the same launcher in headless mode:
chrome --headless --dump-dom https://example.testUse xvfb-run when a browser or UI tool requires an X server but should not use
the host display.
The workspace service sets shm_size: '1gb' because Chrome and other browser
processes can fail with Docker's small default /dev/shm. Chrome hardware
acceleration is disabled by a managed policy at
/etc/opt/chrome/policies/managed/disable-hardware-accel.json.
The repository test suite also includes a Chrome DevTools Protocol UI smoke test
in tests/browser-smoke.sh. It starts a Dev Containers CLI fixture, launches
Chrome through chrome --headless inside the workspace container, captures
a PNG screenshot of a container-local HTML file, and compares rendered
accessibility text against the expected value. This keeps the test path aligned
with the visible launcher while staying deterministic in CI-like environments.
The same test also reproduces the local VS Code failure shape with
DEVCONTAINER_DISPLAY=:0, no /tmp/.X11-unix/X0, and a real Weston Wayland
socket. It starts non-headless Chrome, verifies --ozone-platform=wayland through
CDP, captures rendered output, and must pass as part of task tests-run before
scripts/release-build.sh accepts the current commit. Weston is installed only
in the disposable test fixture, not in the released image.
The devcontainer image includes QEMU/KVM tooling for local VM and ISO workflows:
qemu-system-x86_64, qemu-img, qemu-kvm, cloud-localds, bridge/network
utilities, and small automation helpers such as expect, sshpass, and
pwgen. The Compose runtime is privileged, maps /dev/kvm explicitly, and adds
the numeric KVM device group so QEMU can use host virtualization devices when the
host exposes them. initialize.sh writes DEVCONTAINER_KVM_GID from
stat -c %g /dev/kvm; existing generated env files can use KVM_GID in
.codegeist/.local.env as a manual override when needed.
The smoke path requires /dev/kvm to be available and writable inside the
container. It downloads pinned Alpine Linux 3.20.3 into
.test-tmp/qemu-cache/ and boots the ISO with QEMU KVM acceleration until the
fixed localhost login: prompt appears:
task qemu-alpine-smokeThe test fails when /dev/kvm is missing or not writable. Hosts that run the
devcontainer inside another VM must enable nested virtualization before this
suite can pass.
The devcontainer image includes vhs, ffmpeg, and ttyd for deterministic
terminal rendering and documentation-preview captures. Consuming repositories can
drive real native CLIs or TUIs through VHS without installing these tools in a
project-local .codegeist/Dockerfile fragment. Image-level tests verify all
three commands are present after the kit image builds.
The devcontainer image includes deterministic external security-scan tools used
by consuming infrastructure repositories: nmap and nping, hping3,
ssh-audit 3.9.0, testssl, sslscan, ssh, ssh-keygen, and sysctl from
procps. These tools are installed in the shared image so local QEMU checks and
approved remote scans use the same scanner versions instead of relying on
machine-local packages.
Clone this repository with submodules initialized because .devcontainer/ and
.opencode/ are shared workspace submodules:
git clone --recurse-submodules <this-repo-url>
cd <repo>If the repository was already cloned without submodules, initialize them later:
git submodule update --init --recursiveRun the normal fast, deterministic contributor check from this repository root:
task checkThis validates shell syntax and the focused source-to-release contract without
building the image, starting Docker, QEMU, Dev Containers, or browsers,
publishing a release, or modifying this repository's Git history. The release
fixture uses a bounded source input list under a cleanup-trapped OS temporary
directory and leaves no .test-tmp, cache, log, or copied local state in the
source checkout.
Run the broad local test suite when changing image contents, Dev Containers lifecycle behavior, Docker/Compose integration, QEMU, browser runtime, or another contract covered only by integration tests:
task tests-runOpen the current Git root with the real VS Code entrypoint:
task code-open
task code-open -- develop0BRANCH=develop0 task code-open is still accepted for shell-driven runs. When a
branch is selected, the command prepares .worktrees/<branch> and opens VS Code
from that worktree without forwarding BRANCH into the opened VS Code process.
Run the fixture-backed reality test when you need to exercise the same command against a temporary consuming repository:
task code-open-test
task code-open-test -- develop0The reality test intentionally leaves its temporary fixture and VS Code-started container in place because VS Code is opened against that fixture.
Update the runtime-only release branch when consuming repositories should pin
the kit as a stable .devcontainer submodule branch:
task release-buildThe release task must run from a clean main checkout. The first run creates an
orphan release branch and commits only the files required by the Dev
Containers runtime. Later runs update the same runtime-only branch. Add --push
to push the branch after it is updated locally:
task release-build -- --pushtask release-build requires a commit-bound verification written only after
task tests-run completes. This prevents a runtime release from being created
without the real DISPLAY=:0 plus Wayland Chrome regression passing on the exact
commit being released.
The release branch tree contains only:
.gitignore
.local.env.example
.oc_local.gitignore.example
.oc_local.opencode.json.example
Dockerfile
Dockerfile.example
LICENSE
README.md
compose.local.yml.example
devcontainer.json
docker-compose.yml
entrypoint.sh
initialize.sh
scripts/chrome.sh
scripts/release-build.sh copies source Dockerfile.base into the release tree
as Dockerfile and ships Dockerfile.example as the on-demand template for root
.codegeist/Dockerfile; do not add a tracked root Dockerfile to the source
checkout for the kit base image. The source and generated runtime trees both ship
the canonical 0BSD LICENSE.
This repository includes .opencode/ as a Git submodule pointing to
https://github.com/codegeist-ai/codegeist-agent-kit. It provides shared
OpenCode commands, rules, and skills used while maintaining this kit.
Keep it initialized for development work:
git submodule update --init --recursive .devcontainer .opencodeThe submodule is not part of the consuming .devcontainer/ runtime contract;
it is repository-local AI workflow support for this kit. Project-specific
OpenCode commands, rules, and skills belong in .oc_local/, not in the checked
out .opencode/ submodule.
Use a Git subtree when a project should vendor this kit into .devcontainer/
without making consumers initialize a submodule. The consuming repository stores
the kit files directly in its history, while still allowing updates from the
upstream kit repository.
Add the kit to a consuming repository:
git remote add devcontainer-kit <kit-repo-url>
git fetch devcontainer-kit
git subtree add --prefix=.devcontainer devcontainer-kit <branch> --squashReplace <kit-repo-url> with this repository URL and <branch> with the kit
branch to consume, for example main.
Update the vendored kit later:
git fetch devcontainer-kit
git subtree pull --prefix=.devcontainer devcontainer-kit <branch> --squashCommit the subtree add or pull like any other source change in the consuming repository.
If the consuming repository already has a .devcontainer/ directory, move or
remove that directory first. git subtree add expects the target prefix to be
absent or empty.
The consuming repository should ignore local files generated next to the subtree:
/.codegeist/.local.env
/.oc_local/
/.worktrees/
/.chrome/Do not ignore /.oc_local/ if the consuming repository deliberately tracks a
project-local OpenCode overlay there. Do not ignore .codegeist/compose.local.yml
or .codegeist/Dockerfile; they should remain visible to Git.
The kit also writes generated runtime files inside .devcontainer/:
.env, Dockerfile.merged.gen, compose.local.gen.yml, and
compose.user.gen.yml. They are ignored by the kit's own .gitignore and
should not be edited manually.
initializeCommand is not a bootstrap mechanism for downloading the kit. If
.devcontainer/devcontainer.json is not present in the checkout, VS Code cannot
discover or run the devcontainer configuration at all.
Use a Git submodule when the consuming repository should keep the kit as a
separate repository mounted at .devcontainer/.
Add the kit to a consuming repository:
git submodule add <kit-repo-url> .devcontainer
git commit -m "chore(devcontainer): add shared kit submodule"Pin the release branch when the consuming project should use a stable runtime-only tree:
git -C .devcontainer fetch origin release
git -C .devcontainer checkout origin/release
git add .devcontainer
git commit -m "chore(devcontainer): pin shared release kit"Clone or update consuming repositories with submodules initialized:
git submodule update --init --recursiveThe same generated root files must still be ignored by the consuming repository:
/.codegeist/.local.env
/.oc_local/
/.worktrees/
/.chrome/The .devcontainer/.env, .devcontainer/.Xauthority.gen,
.devcontainer/Dockerfile.merged.gen, .devcontainer/compose.local.gen.yml, and
.devcontainer/compose.user.gen.yml files are generated by the kit inside the
submodule checkout and intentionally ignored there.
The kit also creates root .oc_local/ for OPENCODE_CONFIG_DIR so OpenCode can
bootstrap in fresh devcontainers. When the consuming repository does not track a
.oc_local/ overlay, initialize.sh adds the missing local-file patterns to
that repository's root .gitignore; it never writes them to .git/info/exclude.
The test suite includes a real submodule-consuming fixture that starts
BRANCH=dev0 through devcontainer up, verifies the selected workspace path,
nested Docker, and a commit/fast-forward merge flow from inside the container.
The primary entrypoint is devcontainer.json.
The intended flow is:
VS Code opens repository
Dev Containers extension reads .devcontainer/devcontainer.json
initializeCommand creates local compose/env files when missing
Docker Compose builds and starts the workspace service
VS Code attaches to the workspace service
This means the kit should avoid old launcher-style behavior in the normal VS Code path:
- no
code --new-windowfrom repository scripts - no recursive reopen-in-container behavior
- no root
start.shdependency - no project-specific assumptions such as
CODEGEIST_*
initializeCommand is the only standard devcontainer lifecycle hook that runs
on the host before the container exists. It is useful for preparing local files
that Docker Compose or the container runtime will consume.
Recommended shape:
{
"initializeCommand": ".devcontainer/initialize.sh"
}The initializer must be:
- idempotent
- non-interactive
- fast enough for repeated starts
- safe when run multiple times in one VS Code session
- limited to host-side preparation
It must not:
- open VS Code
- start long-running foreground processes
- start or remove the devcontainer project
- delete user data or running containers
- assume it runs only once
- rely on exporting variables back into VS Code
It may:
- create
.codegeist/.local.envfrom.devcontainer/.local.env.example, or copy legacy root.local.envthere when it already exists and the new file is missing - create
.codegeist/compose.local.ymlfrom.devcontainer/compose.local.yml.example, or copy legacy rootcompose.local.ymlthere when it already exists and the new file is missing; this file is not ignored automatically - create root
.oc_local/for workspace-local OpenCode config - write
.devcontainer/.env,.devcontainer/.Xauthority.gen,.devcontainer/Dockerfile.merged.gen, and.devcontainer/compose.local.gen.ymlgenerated runtime values such as display state, container hostname, hostname loopback resolution, and numeric runtime user - update generated local env values when their content changed
- compute host UID
- compute a stable project name
- compute the host short name
- create local cache/config directories
- validate required host tools and print clear errors
initializeCommand cannot export environment variables back into the already
running VS Code process. Values produced by initialize.sh should therefore be
written to files, not expected to appear as ${localEnv:...} values.
Preferred pattern:
initialize.shwrites.codegeist/.local.env.initialize.shmigrates a legacy rootcompose.local.ymlinto.codegeist/compose.local.ymlonly when that legacy file exists.initialize.shwrites.devcontainer/.env, including host display state.initialize.shatomically refreshes.devcontainer/.Xauthority.gen.initialize.shwrites.devcontainer/Dockerfile.merged.gen.initialize.shwrites.devcontainer/compose.local.gen.yml.initialize.shwrites.devcontainer/compose.user.gen.yml, either empty or copied from.codegeist/compose.local.yml..devcontainer/docker-compose.ymlreads.envand../.codegeist/.local.envwithenv_file.devcontainer.jsonincludescompose.local.gen.ymlandcompose.user.gen.yml..devcontainer/docker-compose.ymlowns the workspace and parent Git mounts.devcontainer.jsonuses${localEnv:USER}forremoteUserandcontainerUser.docker-compose.ymlpasses initial display values into the container; the Chrome launcher rereads the workspace files after SSH reconnects.- Container-side tools read normal environment variables from Compose.
Example Compose shape:
services:
workspace:
env_file:
- path: .env
required: false
- path: ../.codegeist/.local.env
required: falseAvoid generating values for devcontainer.json to read through
${localEnv:...}. The Dev Containers extension may have captured host
environment variables before initializeCommand writes anything, and clients may
need a restart to pick up changed host environment.
Official Dev Container lifecycle behavior relevant to this kit:
initializeCommandruns on the host machine.- It can run during container creation and on later starts.
- It may run more than once in a session.
- String commands run through
/bin/sh. - Array commands execute directly without a shell.
- If a lifecycle command fails, later lifecycle commands are skipped.
Implementation consequence: initialize.sh must fail only for real blockers and
must produce clear diagnostics. Best-effort setup should not make the whole
container unusable unless the missing state is required.
VS Code and the devcontainer CLI do not behave identically in every path.
Known problem areas from upstream discussions include:
devcontainer buildmay not runinitializeCommandlike VS Code does.- CLI rebuild ordering around existing containers can differ from VS Code.
initializeCommandhas been observed to run on reopen, not only first create.
Tests should therefore cover the behavior the kit relies on directly. A build test alone is not enough to prove host initialization behavior.
The kit should be tested as close to the real workflow as possible with the
Dev Containers CLI. Prefer devcontainer up over only validating files or
running docker compose directly, because devcontainer up exercises the same
configuration model that VS Code uses.
Primary smoke command:
npx --yes @devcontainers/cli up --workspace-folder <fixture-repo>If the CLI is installed globally, the shorter form is equivalent:
devcontainer up --workspace-folder <fixture-repo>The smoke fixture should look like a real consuming repository:
- repository root contains project files
.devcontainer/contains this kit- VS Code/devcontainer configuration is read from
.devcontainer/devcontainer.json initializeCommandruns through the devcontainer lifecycle, not by calling the initializer directly as the only assertion
Recommended test layers:
- fast contract tests for generated files and static configuration
devcontainer read-configurationfor schema/config resolutiondevcontainer upfor the real lifecycle, includinginitializeCommanddevcontainer execordocker execfor observable checks inside the running workspace service- QEMU image-level smoke tests with KVM acceleration when virtualization tooling
changes, so the suite proves
/dev/kvmworks inside the container
The tests should verify at least:
.codegeist/.local.envis created or preserved byinitializeCommand- root
.oc_local/is created for OpenCode and ignored unless the repository tracks a project overlay there .devcontainer/.env,.devcontainer/.Xauthority.gen,.devcontainer/Dockerfile.merged.gen,.devcontainer/compose.local.gen.yml, and.devcontainer/compose.user.gen.ymlare regenerated byinitializeCommand- the generated container hostname matches host, repo, and branch context
- the generated runtime user and group match the host UID
- repeated
devcontainer upruns stay safe and idempotent - no VS Code window is opened by kit scripts
- no project-specific names such as
CODEGEIST_*are required - the workspace service starts and accepts a basic command
- QEMU can download and boot a small pinned Alpine Linux ISO to its login prompt
- generated local files are not accidentally tracked
- On-demand
.codegeist/compose.local.ymland.codegeist/Dockerfilefiles remain visible to Git so repository overrides are not hidden accidentally
Use direct docker compose tests only for focused Compose behavior that the CLI
does not expose clearly. Do not treat a plain Compose test as a substitute for a
full devcontainer smoke test.
Expected target layout when consumed as a subtree at .devcontainer/:
.codegeist/
.local.env # generated, ignored by the consuming repo
compose.local.yml # optional on-demand Compose override, visible to Git
Dockerfile # optional on-demand image extension fragment, visible to Git
.oc_local/ # generated or project-owned OpenCode local overlay
.devcontainer/
.env # generated, ignored by the kit
Dockerfile.merged.gen # generated, ignored by the kit
compose.local.gen.yml # generated, ignored by the kit
compose.user.gen.yml # generated bridge to optional .codegeist/compose.local.yml
devcontainer.json
docker-compose.yml
Dockerfile # kit base image file in the release branch
Dockerfile.example # template for root .codegeist/Dockerfile
entrypoint.sh
initialize.sh
scripts/
chrome.sh
.local.env.example
compose.local.yml.example
tests/
Roles:
devcontainer.jsonis the VS Code entrypoint.initialize.shperforms host-side setup forinitializeCommand.docker-compose.ymldefines the workspace runtime and the root/worktree bind mounts..devcontainer/Dockerfileis the release kit base image file; source checkouts keep the same content asDockerfile.base. Root.codegeist/Dockerfilecan extend it through the generatedDockerfile.merged.genfile.entrypoint.shruns inside the container.entrypoint.shlinks/usr/local/bin/chrometo the mounted.devcontainer/scripts/chrome.shin the selected workspace and prepends$DEVCONTAINER_WORKSPACE_FOLDER/.devcontainer/scriptstoPATH; the launcher starts visible Chrome on the current container display or headless Chrome for automation..local.env.exampledocuments.codegeist/.local.envvalues..oc_local.gitignore.exampleseeds root.oc_local/.gitignorewhen the consuming repository has no tracked.oc_local/overlay..oc_local.opencode.json.exampleis an inactive template for a tracked.oc_local/opencode.jsonthat loadsREADME.mdand project-localrules/**/*.mdguidance. Consuming repositories copy it from.devcontainer/.oc_local.opencode.json.exampleonly when they intentionally track a local OpenCode overlay.compose.local.yml.exampleis the on-demand template for.codegeist/compose.local.yml; the result is visible to Git.Dockerfile.exampleis the on-demand template for.codegeist/Dockerfile; the result is visible to Git and must not containFROM..envexposes generated runtime values to Compose and the container.Dockerfile.merged.genis the generated Docker build input used by Compose.compose.local.gen.ymlsets generated Compose-only values such as hostname, hostname loopback resolution for tools likesudo, build args, and runtime user.compose.user.gen.ymlis generated as an empty Compose override by default, or as a copy of.codegeist/compose.local.ymlwhen that on-demand override exists.tests/verifies host initialization and container configuration contracts.
The root .codegeist files intentionally live one directory above
.devcontainer/. This keeps consuming-repository env, Compose, and optional
image-extension state out of the vendored subtree while keeping
devcontainer-specific files grouped together. .codegeist/.local.env is still
machine-local and ignored; optional .codegeist/compose.local.yml and
.codegeist/Dockerfile remain visible to Git.
The generated .env, Dockerfile.merged.gen, compose.local.gen.yml, and
compose.user.gen.yml files intentionally live in .devcontainer/ because they
are kit-owned dynamic runtime state. They are rewritten by initialize.sh; users
should put manual overrides in .codegeist/.local.env, .codegeist/Dockerfile,
and .codegeist/compose.local.yml instead.
launch.sh is not part of the required normal VS Code workflow. If a launcher
is kept for compatibility or manual convenience, it should be documented as
optional and must not be required by devcontainer.json.
The kit supports managed Git worktrees under .worktrees/<branch>. When
BRANCH is present in the VS Code Remote SSH or Dev Containers CLI environment,
the repository root can be opened directly and the devcontainer opens the
matching worktree as the remote workspace. If BRANCH matches the branch already
checked out at the repository root, initialize.sh creates
.worktrees/<branch> as a symlink alias back to that root instead of asking Git
for a second checkout of the same branch. The helper flow below is still useful
for local code invocations where an already running VS Code process may not
inherit a newly exported BRANCH value.
From this repository, use the helper task:
task code-open -- develop0For a consuming repository that only has the runtime kit at .devcontainer/, a
Remote SSH host alias can select the worktree while the user opens the repository
root:
Host project-dev0
SetEnv BRANCH=develop0The same branch selection can be tested without VS Code:
BRANCH=develop0 npx --yes @devcontainers/cli up --workspace-folder <repo-root>For local code commands, prepare the worktree from the root and then open the
worktree path:
BRANCH=develop0 .devcontainer/initialize.sh
code .worktrees/develop0During root preparation, initialize.sh creates .worktrees/, creates or
reuses .worktrees/<branch> when BRANCH is set, or creates a current-branch
symlink alias when the selected branch is already checked out. It initializes any
consuming-repository submodules when the repository defines them, creates root
.codegeist/.local.env from .devcontainer/.local.env.example when missing,
migrates a legacy root .local.env into .codegeist/.local.env when needed,
and links the worktree .codegeist/.local.env back to the main root file. When
the devcontainer starts from an already selected checkout, initializeCommand
writes that checkout's
.devcontainer/.env, .devcontainer/.Xauthority.gen,
.devcontainer/Dockerfile.merged.gen,
.devcontainer/compose.local.gen.yml, and .devcontainer/compose.user.gen.yml
without nesting another worktree for the same branch.
For a BRANCH start, .devcontainer/compose.local.gen.yml sets the Compose
project name to <branch-slug>-<repo-slug>. This keeps separate Remote SSH
windows from reusing one workspace container when they open the same repository
root with different branch selections.
BRANCH is a startup input only. initialize.sh uses it to compute generated
workspace values and prepare worktrees, but does not persist BRANCH= into
.devcontainer/.env; later starts without BRANCH should resolve back to the
current checkout instead of reusing an older branch selection.
docker-compose.yml mounts the selected workspace at the same absolute path
inside the container. For linked worktrees it also mounts the parent repository
root at its same absolute path so Git metadata resolves.
.codegeist/compose.local.yml remains available for local overrides, but it does
not own the workspace or parent Git mounts.
Changing branches after a container already exists does not automatically remount the running container. Rebuild or remove the existing devcontainer first, then open the desired checkout again.
Use code-open as the real editor entrypoint from a repository root. It refuses
to run outside Git, from a Git subdirectory, or without
.devcontainer/devcontainer.json.
task code-open
task code-open -- develop0Use the manual reality test when you want to verify that same entrypoint through
a temporary consuming repository. It creates a temporary Git repository, copies
this kit into .devcontainer/, and then invokes the real code-open task
against that fixture.
task code-open-testTo verify branch selection through the normal helper flow:
task code-open-test -- develop0BRANCH=develop0 task code-open-test is still accepted when an environment
variable is more convenient. The helper prepares the worktree before invoking
code . from that checkout, which stays stable even when an existing VS Code
process handles the code request.
The temporary fixture is intentionally left on disk because VS Code is opened against it.
Generated or machine-local files should not be committed.
Typical examples:
.codegeist/.local.env.devcontainer/.env.devcontainer/.Xauthority.gen.devcontainer/Dockerfile.merged.gen.devcontainer/compose.local.gen.yml.devcontainer/compose.user.gen.yml.chrome/.oc_local/when it is only generated OpenCode local state- generated runtime metadata
- editor state
- tool caches
If a value must affect the runtime, write it to a local env file or another
documented generated file that Compose reads explicitly. Compose overrides in
.codegeist/compose.local.yml are visible to Git; commit only intentional
repository-wide overrides.
The kit should remain reusable across repositories, while currently preserving the copied Codegeist/planner image contents.
Avoid:
- product-specific names
- repository-specific paths
- assumptions about root
start.sh - hard-coded nested dependency names outside
.devcontaineritself - committed root
.envor.codegeist/.local.env - direct project-specific edits inside shared
.devcontainer/or.opencode/submodule checkouts - automatic VS Code window management
Prefer:
- stable Dev Container spec properties
- static
devcontainer.jsonwhere possible - local generated env files for host-dependent values
- explicit optional extension points for consuming repositories
- project-specific OpenCode overlays under
.oc_local/ - tests that exercise the exact documented contract
- Docker-in-Docker is enabled by default because it is part of the current tested smoke path. A future variant may make it opt-in if consuming projects need a lighter image.
- SSH helper behavior is intentionally left to consuming repositories for now.
- The base image currently keeps the copied Codegeist/planner toolchain intact; future work can split generic tools from project-specific features when there is a concrete consumer need.
The public roadmap listing is maintained at
https://github.com/users/codegeist-ai/projects/1. Contributor-sized work should
start from a GitHub Issue and, when implementation detail needs durable handoff,
a linked specification under docs/tasks/.
This repository and its generated runtime release are available under the
Zero-Clause BSD (0BSD) license.