This document explains how consuming projects should use this repository's
runtime-only release branch as a .devcontainer Git submodule. It also gives
coding agents the rules they need when maintaining projects that consume this
kit.
This repository provides a reusable VS Code Dev Containers kit. Development of
the kit happens on this repository's normal source branches, while consuming
projects should pin .devcontainer to the release branch.
The release branch is a runtime-only tree. It contains the files needed by the
Dev Containers extension and excludes this repository's development-only files,
tests, and local AI workflow support. The image toolchain includes PowerShell as
pwsh for cross-platform shell and automation work, Task with Bash completion,
shared terminal-capture tools for documentation previews, plus shared QEMU and
security-scan tools for infrastructure checks inside consuming devcontainers.
The runtime tree includes the repository's LICENSE and is
distributed under the Zero-Clause BSD (0BSD) license.
Changes to the kit belong on the source repository's main branch, not in this
generated release tree or a consuming repository's .devcontainer/ checkout.
Use the source repository's
contributor guide,
GitHub Issues,
task guide,
and Codegeist roadmap listing
to propose and track work.
In a source main checkout, run task check as the normal fast contributor
check. Run the broader task tests-run suite when a change affects the image,
Dev Containers lifecycle, Docker/Compose behavior, QEMU, or browser runtime.
The fast check uses cleanup-trapped OS temporary state and leaves no repo-local
test directory behind. These source tasks are intentionally omitted from the
generated runtime tree.
Codegeist's account-wide Code of Conduct, Security Policy, and Support Policy apply without being duplicated in this runtime tree.
- The generated runtime manifest includes the canonical root
LICENSE, so the license travels with every.devcontainer/release checkout.
From the consuming project root, add this kit as a submodule at
.devcontainer:
git submodule add <kit-repo-url> .devcontainer
git -C .devcontainer fetch origin release
git -C .devcontainer checkout origin/release
git add .gitmodules .devcontainer
git commit -m "chore(devcontainer): add release kit"Use the real kit repository URL in place of <kit-repo-url>. The submodule is
intentionally pinned to a commit from origin/release, not to this repository's
main branch.
The consuming repository should ignore the root-local files generated by the kit:
/.codegeist/.local.env
/.oc_local/
/.worktrees/
/.chrome/Do not ignore /.oc_local/ if the consuming repository intentionally tracks a
project-local OpenCode overlay 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 consuming
repository's root .gitignore. It never writes generated-file ignores to
.git/info/exclude, so review and commit intentional .gitignore changes like
normal repository state.
The generated .devcontainer/.env, .devcontainer/.Xauthority.gen,
.devcontainer/Dockerfile.merged.gen, .devcontainer/compose.local.gen.yml, and
.devcontainer/compose.user.gen.yml files are written inside the submodule
checkout and are ignored by the release kit itself.
The release kit creates root .oc_local/ when no tracked project overlay exists
so OPENCODE_CONFIG_DIR can point at the selected workspace's .oc_local
directory inside the container. The container workspace path resolves to the
selected checkout's host path instead of a shared /workspace path, which keeps
OpenCode sessions separated by project and branch. It does not include this
repository's development-only .opencode/ checkout.
When a consuming project should use the shared OpenCode commands, rules, and skills, add the OpenCode agent kit as a separate submodule from the consuming project root:
git submodule add https://github.com/codegeist-ai/codegeist-agent-kit .opencode
git add .gitmodules .opencode
git commit -m "chore(opencode): add shared agent kit"Then track a project-local .oc_local/ overlay only when the project needs its
own OpenCode configuration, commands, rules, or skills. In that case, do not
ignore /.oc_local/; keep generated local files out with narrower ignore rules
instead.
The release kit includes .oc_local.opencode.json.example as an inactive
starting point for .oc_local/opencode.json. In a consuming repository that
uses this kit as .devcontainer/, the template lives at
.devcontainer/.oc_local.opencode.json.example. Copy it into .oc_local/ only
when the consuming project wants tracked local OpenCode configuration:
mkdir -p .oc_local
cp .devcontainer/.oc_local.opencode.json.example .oc_local/opencode.jsonThe template loads README.md first, then project-local rules with the
rules/**/*.md instruction pattern. This makes the release README the first
agent-facing explanation of how the devcontainer kit and local overlay work. In
the release branch, README.md is generated from this source
README_release.md file.
Important local overlay constraints:
initialize.shcreates writable.oc_local/and.oc_local/.gitignorewhen needed, but it never copies.oc_local.opencode.json.exampleor overwrites an existing.oc_local/opencode.json.initialize.shwrites missing generated-file ignore patterns to the root.gitignore, never to.git/info/exclude.- The generated
.oc_local/.gitignoreignores everything in.oc_local/for a purely local overlay. If the consuming repository tracks.oc_local/, remove or narrow the generated ignore file and do not ignore/.oc_local/at the repo level. - Keep product-specific commands, rules, skills, and OpenCode config in
.oc_local/. Keep shared behavior in the separate.opencode/agent-kit submodule or upstream it to that shared kit. - Restart OpenCode after changing
.oc_local/opencode.json, local rules, commands, skills, or.opencode/config; running sessions keep the config they loaded at startup. - Do not put secrets, credentials, or machine-local paths in tracked
.oc_local/files.
Recommended layout for a consuming project that uses both shared and local OpenCode guidance:
.opencode/ # shared agent kit submodule
.oc_local/opencode.json # project-local OpenCode config
.oc_local/rules/ # project-specific agent rules, optional
.oc_local/commands/ # project-specific slash commands, optional
.oc_local/skills/ # project-specific skills, optional
Project-specific OpenCode behavior belongs in .oc_local/. Only change the
.opencode/ submodule itself when updating the shared agent kit for every
consumer, and commit that as a normal submodule gitlink update.
Open the consuming project root in VS Code and let the Dev Containers extension own the container lifecycle:
code .To start the container with a managed Git worktree mounted at its stable host
path from VS Code Remote SSH, set BRANCH in the SSH environment and reopen the
repository root in the container. 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 branch selection can be smoke-tested with the Dev Containers CLI:
BRANCH=develop0 npx --yes @devcontainers/cli up --workspace-folder <repo-root>For local code commands where an already running VS Code process may not
inherit new environment variables, prepare the worktree from the consuming
project root and then open that checkout:
BRANCH=develop0 .devcontainer/initialize.sh
code .worktrees/develop0The first start creates local runtime files when missing:
.codegeist/.local.env- root
.oc_local/when no tracked project overlay exists - root
.worktrees/;.worktrees/<branch>as a worktree or current-branch symlink alias whenBRANCHis set .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
The generated Compose override sets a branch-aware Compose project name, sets the
container hostname, and maps that same name to 127.0.0.1 through extra_hosts,
so tools such as sudo can resolve the active container hostname.
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 generated .devcontainer files directly. Put environment overrides in
.codegeist/.local.env, Compose overrides in .codegeist/compose.local.yml,
and devcontainer image extensions in .codegeist/Dockerfile. Create the Compose
and Dockerfile override files only when the repository needs them. Commit
.codegeist/compose.local.yml and .codegeist/Dockerfile only when their
overrides are intentional repository state.
BRANCH is a startup input only. The kit uses it to prepare .worktrees/<branch>
and compute generated workspace values, but it does not persist BRANCH= into
.devcontainer/.env; later starts without BRANCH resolve back to the current
checkout.
Consuming projects can extend the shared image by adding .codegeist/Dockerfile
only when they need repository-specific image changes. During
initializeCommand, the kit writes .devcontainer/Dockerfile.merged.gen from
the release kit base at .devcontainer/Dockerfile and then 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/DockerfileUse .codegeist/Dockerfile only as an extension fragment for this pattern:
# .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 another stage and can replace the prepared kit image, so
initialize.sh rejects it with a clear error. COPY and ADD paths are
resolved from the consuming repository root because the Docker build context
remains the project root.
A root Dockerfile remains available for application images and is not treated
as a devcontainer extension. Do not commit
.devcontainer/Dockerfile.merged.gen.
Consuming projects can override Compose settings by creating
.codegeist/compose.local.yml only when they need repository-specific Compose
changes:
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 release kit includes Google Chrome for visible, headless, and automated UI
browser checks that must use the devcontainer's DNS, networking, and installed
certificates. It also includes Xvfb for tools that need a virtual X11 display
without a host UI. Start visible Chrome from inside the container when a resource
is only reachable from that runtime context:
chrome https://example.testThe visible command does not start VNC or noVNC, and it validates display
transport before starting Google Chrome. 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 is usable only
when /tmp/.X11-unix/X0 exists; 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 existing container without recreation.
SSH X11 reconnect recovery does not have that limitation because it uses host
networking and refreshed workspace-local 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.
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.
Non-interactive automation can use the same launcher without a visible session:
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' for browser stability, and Chrome
hardware acceleration is disabled through the managed policy file at
/etc/opt/chrome/policies/managed/disable-hardware-accel.json.
Bookmarks, credentials, browser profiles, and project-specific service URLs belong in consuming-repository overrides or future focused kit work.
The release kit 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.
Hosts that run the devcontainer inside another VM must enable nested
virtualization before KVM-accelerated QEMU can work inside the container. If
/dev/kvm is missing or not writable, QEMU commands that require KVM will fail
until the host or outer VM exposes the device with suitable permissions.
To check QEMU from inside a consuming project's devcontainer, download a small Alpine ISO and boot it with KVM acceleration:
mkdir -p .qemu
curl -fL \
-o .qemu/alpine-standard-3.20.3-x86_64.iso \
https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.3-x86_64.iso
qemu-img info .qemu/alpine-standard-3.20.3-x86_64.iso
test -r /dev/kvm && test -w /dev/kvm
qemu-system-x86_64 \
-machine accel=kvm \
-cpu host \
-m 512M \
-cdrom .qemu/alpine-standard-3.20.3-x86_64.iso \
-boot d \
-display none \
-serial stdio \
-no-rebootThe command is healthy when Alpine reaches a localhost login: prompt. Press
Ctrl-a then x to exit QEMU from the terminal. For non-interactive project
checks, wrap the same QEMU command with expect and fail if the login prompt is
not printed within the chosen timeout.
The release kit 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 adding these generic tools through a
project-local .codegeist/Dockerfile fragment.
The release kit includes deterministic external security-scan tools for consuming
infrastructure repositories: nmap and nping, hping3, ssh-audit 3.9.0,
testssl, sslscan, ssh, ssh-keygen, and sysctl from procps. Keeping
these tools in the shared image lets local QEMU checks and approved remote scans
use the same scanner versions instead of depending on host-local packages.
To update an existing consuming project to the latest runtime release:
git -C .devcontainer fetch origin release
git -C .devcontainer checkout origin/release
git add .devcontainer
git commit -m "chore(devcontainer): update release kit"Verify the submodule state before committing:
git status --short
git submodule status .devcontainer
git -C .devcontainer status --short --branch
git -C .devcontainer log -1 --onelineThe consuming project commit should normally contain only the .devcontainer
gitlink update and any intentional consumer-side documentation or ignore-file
updates.
When working in a consuming repository, treat .devcontainer/ as a submodule,
not as ordinary project source.
- Do not edit files inside
.devcontainer/directly to customize one consuming project. - Do not commit
.devcontainer/.env,.devcontainer/.Xauthority.gen,.devcontainer/Dockerfile.merged.gen,.devcontainer/compose.local.gen.yml,.devcontainer/compose.user.gen.yml,.codegeist/.local.env, or generated.worktrees/files. Keep.codegeist/compose.local.ymlvisible to Git and keep.codegeist/Dockerfilevisible to Git withoutFROM; commit either only when its overrides are intentional repository state. - Do not pin consumers to this kit's
mainbranch unless a human explicitly asks for development-branch testing. - Do not replace the submodule with copied files unless the consuming project is intentionally migrating to a Git subtree workflow.
- Do not use destructive submodule commands such as
git reset --hardor forced checkouts unless a human explicitly requests them. - If
.opencode/is present, treat it as a shared agent-kit submodule. Do not edit files inside.opencode/for one consuming project. - Put project-specific OpenCode instructions, commands, rules, and skills under
.oc_local/instead of changing.opencode/. - If
.oc_local/is tracked, do not also ignore the whole directory. Ignore only generated local artifacts such as package caches or machine-local state.
If the kit behavior itself needs to change:
- Make the change in this kit repository, not in the consuming repository's
.devcontainer/checkout. - Verify the upstream kit change before publishing a new runtime-only
releasebranch commit. - Publish the updated runtime-only
releasebranch from this kit repository. - In the consuming repository, update only the
.devcontainersubmodule gitlink to the neworigin/releasecommit. - Commit the consuming repository gitlink update with a focused message such as
chore(devcontainer): update release kit.
Before any consuming-repository commit that touches .devcontainer, inspect:
git status --short --branch
git submodule status .devcontainer
git -C .devcontainer status --short --branch
git -C .devcontainer rev-parse HEAD
git -C .devcontainer log -1 --onelineIf .devcontainer has uncommitted content changes, stop and decide whether the
change belongs upstream in this kit repository. Do not hide submodule content
changes by committing only the parent gitlink.
If changing BRANCH does not change the selected workspace path, rebuild or
remove the existing devcontainer before starting VS Code again. Docker Compose
cannot remount an already running container just because .env changed.
If the Dev Containers image build fails, inspect Docker storage first:
df -h /var/lib/docker
docker system dfThis kit currently uses Docker-in-Docker and a large development image, so builds need enough Docker storage. A storage failure is an environment blocker, not a valid release verification pass.
If OpenCode cannot write under the selected workspace's .oc_local, make sure
the consuming repository either tracks its own .oc_local/ overlay intentionally
or lets the kit generate a local ignored .oc_local/ directory during
initializeCommand.