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,
the official Gitea tea CLI, shared terminal productivity and capture tools,
Terraform and OpenTofu, plus shared QEMU and security-scan tools for
infrastructure checks inside consuming devcontainers, including Trivy for
project, configuration, and container-image scans.
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.
- Documented optional Linux PipeWire microphone forwarding through a fixed loopback-only OpenSSH remote forward, including connection multiplexing for parallel VS Code sessions, client usage, verification, and security limits.
- Managed worktrees now initialize submodules anonymously with inherited Git
credential helpers disabled, preventing public repositories from blocking
initializeCommandon a credential prompt. - 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
/.codegeist/secrets/
/.oc_local/
/.worktrees/
/.chrome/
/.tmpDo 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.
The image installs oc globally at /usr/local/bin/oc. It starts
opencode --auto -c in tmux and forwards additional OpenCode arguments:
oc
oc --model provider/model
oc /path/to/projectOutside tmux, each invocation creates and attaches to a new session. Inside
tmux, it opens a new window in the current session instead of nesting tmux. The
wrapper does not name or reuse sessions. It enables tmux's set-clipboard on
server option so OpenCode's OSC 52 TUI copy action can update the outer terminal
clipboard. This also allows other applications in that tmux server to request
clipboard updates. Because of that behavior and because --auto approves
permissions that are not explicitly denied, use oc only in trusted workspaces
with trusted OpenCode configuration.
The wrapper binds both tmux Prefix + R (Ctrl+B, then uppercase R) and
direct Alt+R to the workspace microphone recorder. The direct binding is
global within these tmux sessions, so applications inside a pane do not receive
Alt+R. The first press starts one mono, 48 kHz WAV recording; the second press
stops FFmpeg with SIGINT and saves the finalized file under
.tmp/recordings/YYYYMMDD-HHMMSS.wav. It then waits for the CPU-only
whisper-cli transcription and writes the detected-language text to the matching
.tmp/recordings/YYYYMMDD-HHMMSS.txt file. A non-empty transcript is then pasted
at the cursor in the OpenCode pane where the recording was stopped. It is not
submitted automatically, so it can be reviewed and edited before pressing Enter.
Trailing line endings are removed before insertion; internal line breaks remain.
An empty transcript leaves the pane input unchanged. The tmux client remains
responsive because the binding runs the recorder command in the background.
The recorder uses automatic language detection when OC_RECORD_LANGUAGE is
unset or empty. To select a known spoken language, add a whisper.cpp language
code such as the following to the ignored .codegeist/.local.env file:
OC_RECORD_LANGUAGE=deCompose reads this file when it creates the container. Recreate the existing
devcontainer after changing the value so oc-record receives the new language.
The first transcription in a container downloads the approximately 488 MB
multilingual small model to /tmp/whisper.cpp/ggml-small.bin. A later
transcription reuses the file while it exists; a container restart may discard
it, so the next transcription requires network access and downloads it again.
While recording, the complete tmux status bar is yellow; stopping or a startup
failure restores its previous style. The shortcut requires the SSH microphone
forward described below. A missing forward fails only recorder startup. A model
download or transcription failure preserves the finalized WAV and reports the
.tmp/recordings/.oc-record.log diagnostic path in tmux. If the target pane
disappears before insertion, the finalized WAV and TXT remain available and the
recorder retains the same diagnostic log.
VS Code Remote SSH does not forward microphone audio. A local Linux client that uses PipeWire's PulseAudio compatibility layer can expose its Pulse server to programs on the SSH host through an OpenSSH remote forward. The complete path is:
Pulse-compatible client in the devcontainer
-> tcp:127.0.0.1:47130 on the SSH host
-> encrypted SSH remote-forward channel
-> /run/user/<uid>/pulse/native on the local Linux client
-> the local user's PipeWire microphone source
The TCP listener is remote, on the SSH host. The Unix socket is local, on the
computer running the SSH client. OpenSSH opens that socket as the local user, so
the numeric <uid> is the local user's UID, not the SSH account's UID.
Run these commands in a terminal on the local Linux client, before starting the VS Code Remote SSH connection:
id -u
test -S "/run/user/$(id -u)/pulse/native"The socket check must return exit status zero. Replace <uid> below with the
numeric output from id -u; OpenSSH configuration does not evaluate
$(id -u) or other shell substitutions.
Add the following options to the matching host in the local ~/.ssh/config:
Host <remote-host>
ControlMaster auto
ControlPath ~/.ssh/control-%C
ControlPersist 60
RemoteForward 127.0.0.1:47130 /run/user/<uid>/pulse/native
ExitOnForwardFailure yesRemoteForward creates the 127.0.0.1:47130 TCP listener on the SSH host and
forwards each accepted connection to the local Unix socket. The explicit
127.0.0.1 bind is required: do not replace it with an empty address, *,
0.0.0.0, or another externally reachable address. Port 0 would ask OpenSSH
to allocate a dynamic remote port, but clients would then need to discover that
port for every connection. This kit deliberately keeps the stable endpoint
tcp:127.0.0.1:47130.
ControlMaster auto lets parallel VS Code and terminal sessions share one
underlying SSH connection. ControlPath ~/.ssh/control-%C names its local Unix
control socket with OpenSSH's hash of the effective connection tuple.
ControlPersist 60 keeps that SSH master alive for up to 60 seconds after its
last multiplexed session exits; it does not install or start a separate system
service.
Only the master connection owns the remote listener. A later multiplexed SSH
session recognizes the existing forwarding and reuses it. Without multiplexing,
two independent connections using this same RemoteForward both try to bind
remote port 47130: the first succeeds and the second fails because the port is
already occupied. With ExitOnForwardFailure yes, that second independent SSH
connection terminates instead of continuing without its requested tunnel.
When several host aliases resolve to the same SSH account and must share this
forward, ensure they resolve to the same effective host, user, and port and use
the same ControlPath. A literal shared ControlPath may be used across those
specific host entries when their %C values would differ. Do not share one
control path between unrelated SSH destinations.
The SSH server must permit remote TCP forwarding. Its effective configuration
must allow AllowTcpForwarding yes or AllowTcpForwarding remote, must not set
DisableForwarding yes, and, when PermitListen is restricted, must allow
127.0.0.1:47130. Ask the SSH host administrator to change server policy when
necessary. ExitOnForwardFailure yes makes a port conflict or rejected
forwarding request fail the SSH connection instead of silently omitting the
listener; it does not guarantee that later connections to the local Pulse socket
will succeed.
Fully reconnect the VS Code Remote SSH session after changing the client configuration. Restarting a VS Code window may reuse a pre-existing master that was created before the forwarding option was added. Close the affected remote windows and run the following on the local Linux client when an old master must be checked or stopped:
ssh -O check <remote-host>
ssh -O exit <remote-host>ssh -O check only inspects an existing master; it does not create a connection.
A missing control socket therefore means no master currently exists, not that
the configuration is invalid. After the final session closes, the listener
exists only for the configured ControlPersist interval. A later VS Code
connection creates a new master and listener.
Validate the effective local configuration without opening a connection:
ssh -G <remote-host> |
grep -E '^(hostname|user|controlmaster|controlpath|controlpersist|remoteforward|exitonforwardfailure) 'OpenSSH may display the listener as [127.0.0.1]:47130; this is the normalized
form of the configured IPv4 loopback endpoint. For connection-level diagnosis,
run ssh -vvv <remote-host> locally. Successful setup includes messages
equivalent to remote forward success, while a reused multiplexed forward is
reported as found existing forwarding. A debug destination ending in :-2
is OpenSSH's internal representation for the Unix-socket target and is not an
error when forwarding success is subsequently reported.
Run these checks directly on the SSH host or in a terminal already attached to the devcontainer:
ss -ltn '( sport = :47130 )'
nc -vz -w 3 127.0.0.1 47130The ss result must show 127.0.0.1:47130, never 0.0.0.0:47130,
[::]:47130, or another non-loopback address. nc must report a successful TCP
connection. Because the devcontainer uses host networking, the same loopback
endpoint is available from inside the container.
Do not run ssh <remote-host> from inside the remote devcontainer merely to
perform these checks. The host alias and control socket belong to the local
Linux client and may not exist in the container. Run ss and nc directly in
the existing remote terminal. To invoke the checks remotely from the local
computer instead, run:
ssh <remote-host> \
"ss -ltn '( sport = :47130 )' && nc -vz -w 3 127.0.0.1 47130"The TCP check proves that the remote listener is reachable, but it does not prove that a Pulse-compatible client can read audio. The devcontainer image includes FFmpeg, so verify the complete protocol and audio path without writing an output file:
PULSE_SERVER=tcp:127.0.0.1:47130 \
ffmpeg -hide_banner -loglevel info \
-f pulse -i default \
-t 5 -f null -A successful run identifies a Pulse input, processes five seconds at real-time
speed, and exits without connection, authentication, or input errors. The local
default source may be stereo. Recorder commands that require mono can request
one output channel independently with -ac 1.
Pulse-compatible programs on the SSH host or in the devcontainer select the
forwarded server through PULSE_SERVER. Set it for one command:
PULSE_SERVER=tcp:127.0.0.1:47130 <pulse-compatible-command>Alternatively, export it for the current shell:
export PULSE_SERVER=tcp:127.0.0.1:47130For example, this direct FFmpeg command records the default source as mono,
48 kHz WAV until q or Ctrl+C stops FFmpeg:
PULSE_SERVER=tcp:127.0.0.1:47130 \
ffmpeg -f pulse -i default -ac 1 -ar 48000 microphone.wavOne SSH remote-forward listener accepts multiple TCP connections, so multiple Pulse-compatible clients and multiplexed VS Code sessions can use the endpoint concurrently. The SSH master that owns the listener must remain alive.
The local SSH client opens the Unix socket as the local Linux user. Binding to loopback prevents access from other network hosts, but processes that can reach loopback on the SSH host may access the forwarded Pulse server with that local user's permissions. Use this forwarding only through a trusted SSH host and disconnect the SSH session when it is no longer needed.
Microphone forwarding is an optional, manually managed prerequisite. Missing or failed forwarding does not affect normal devcontainer startup, OpenCode, or tmux. The kit does not modify client SSH configuration, server SSH policy, PipeWire configuration, initialization behavior, or Compose configuration.
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/develop0When a new worktree needs submodules, initialize.sh disables inherited Git
credential helpers and terminal prompts for that checkout. Public submodules are
therefore cloned anonymously without waiting for a username or password.
The first start creates local runtime files when missing:
.codegeist/.local.env.codegeist/secrets/, the ignored location for new persistent secret files.tmp, a workspace-visible symlink to/tmp/ws-datafor new disposable artifacts- 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.
Use .tmp/ for new temporary directories, downloads, fixtures, and disposable
agent artifacts instead of creating new temporary roots directly in the
workspace. The initializer creates /tmp/ws-data when needed and leaves an
existing .tmp path unchanged. Existing temporary paths are not migrated, and
the target is not a managed tmpfs; retention follows the host or container
environment. Store new persistent secrets that are not disposable test inputs
under .codegeist/secrets/. Existing .codegeist/.local.env and .chrome/
contracts remain unchanged.
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 image includes pass, GnuPG, and the verified official
docker-credential-pass helper. Docker looks for this helper by default on
Linux, so registry credentials can be kept in the user's encrypted password
store instead of as base64-encoded values in ~/.docker/config.json.
The kit does not create a GPG key, initialize pass, change Docker
configuration, or perform a registry login. Set up that personal state once
inside the devcontainer:
gpg --list-secret-keys --keyid-format=long
pass init <gpg-key-id>
docker logindocker login uses Docker Hub's browser-based device flow by default. For
another registry, pass only its host and optional port, for example
docker login registry.example.com. Credentials entered during that login are
then available to later Docker commands without putting a token or password in
.codegeist/.local.env.
Docker normally discovers docker-credential-pass automatically on Linux. To
select it explicitly, merge this property into the user-owned
~/.docker/config.json without discarding any existing settings:
{
"credsStore": "pass"
}If credentials were saved before the helper was available, run docker logout
for the affected registry and then docker login again after initializing
pass. Keep the GPG private key, password store, and Docker configuration in
the user's home directory and out of the repository.
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.
Each image build resolves the latest official Linux x86_64 releases of Neovim
(nvim), Gum, ripgrep (rg), bat, btop, eza, dust, and fzf. Use them directly
for editing, interactive prompts, text search, highlighted output, resource
monitoring, directory listings, disk-usage inspection, and fuzzy selection:
nvim README.md
gum choose development staging production
rg 'initializeCommand' .
bat Dockerfile
btop
eza --long --git --icons
dust --depth 2 .
printf '%s\n' alpha beta gamma | fzfVersioned asset names are derived from GitHub's normal latest-release redirects, so image builds do not consume the shared unauthenticated GitHub API quota.
The release kit does not replace vim, cat, ls, or du, create command
aliases or symlinks, enable fzf shell keybindings, or install Neovim and Gum user
configuration. Interactive btop, Gum, and fzf flows require a usable TTY. Eza
icons require a Nerd Font in the terminal. Dust scans the selected directory tree
and can be expensive on large workspaces, mounted filesystems, or generated
dependency trees, so prefer a focused path and depth when appropriate.
The image includes uuidgen from Debian's uuid-runtime package for generating
UUIDs from scripts or the terminal:
uuidgenThe release kit provides the command but does not configure uuidd as a
persistent service or impose a project-specific UUID workflow.
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 image includes tea, the official Gitea CLI. Put the server URL and
application token in the ignored .codegeist/.local.env file so Compose injects
the names that tea login add reads natively:
GITEA_SERVER_URL=https://git.codegeist.ai
GITEA_SERVER_TOKEN=your-application-tokenAdd the login once, then use the stored active login to work with repositories, issues, and pull requests:
tea login add
tea login list
tea repos ls
tea issues ls
tea pulls lsRun tea --help or tea <command> --help for the available commands and flags.
Use GITEA_SERVER_TOKEN, not GITEA_TOKEN; the latter is not a tea login
environment variable. Keep the token only in the ignored machine-local env file,
never in shell history or tracked repository files.
The image includes both Terraform as terraform and OpenTofu as tofu.
OpenTofu is installed from its official signed Debian repository and remains a
separate native command rather than replacing or aliasing Terraform. From a
project with OpenTofu configuration, use the normal CLI workflow:
tofu fmt -check
tofu validate
tofu planThe 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.
Gitleaks 8.30.1 is available for detecting committed or local secrets. Scan Git history or the current directory without printing detected values:
gitleaks git --redact
gitleaks dir --redact .Gitleaks reports potential secrets but does not remove or revoke them. Rotate a real exposed credential and clean the Git history when necessary.
Trivy 0.74.0 is available inside the devcontainer. From the consuming project, scan Dockerfiles and other supported Infrastructure as Code configuration for policy and construction problems:
trivy config --severity HIGH,CRITICAL --exit-code 1 .Scan the project filesystem for vulnerable dependencies, misconfigurations, and secrets, or scan the packages contained in a built image:
trivy fs --scanners vuln,misconfig,secret --severity HIGH,CRITICAL --exit-code 1 .
trivy image --severity HIGH,CRITICAL --exit-code 1 my-image:tagtrivy config evaluates source configuration such as Dockerfile instructions;
it does not determine which vulnerable packages are present in the resulting
image. Use trivy image after building to inspect the final image contents.
--severity limits reported findings, while --exit-code 1 makes matching
findings fail a CI step instead of returning Trivy's default successful status.
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,.codegeist/secrets/,.tmp, 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/. - Put new disposable artifacts under
.tmp/and new persistent non-test secrets under.codegeist/secrets/; do not invent additional workspace-local temp or secret roots. - 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.