Skip to content
 
 

Repository files navigation

Devcontainer Release Usage

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.

Purpose

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.

Project And Contributions

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.

Release Notes

  • 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 initializeCommand on a credential prompt.
  • The generated runtime manifest includes the canonical root LICENSE, so the license travels with every .devcontainer/ release checkout.

Consumer Setup

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.

Required Ignores

The consuming repository should ignore the root-local files generated by the kit:

/.codegeist/.local.env
/.codegeist/secrets/
/.oc_local/
/.worktrees/
/.chrome/
/.tmp

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.

OpenCode Agent Setup

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.json

The 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.sh creates writable .oc_local/ and .oc_local/.gitignore when needed, but it never copies .oc_local.opencode.json.example or overwrites an existing .oc_local/opencode.json.
  • initialize.sh writes missing generated-file ignore patterns to the root .gitignore, never to .git/info/exclude.
  • The generated .oc_local/.gitignore ignores 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.

OpenCode In tmux

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/project

Outside 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=de

Compose 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.

SSH Microphone Forwarding

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.

Configure The Local Linux Client

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 yes

RemoteForward 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.

Server Requirements And Connection Lifecycle

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.

Verify The Remote Endpoint

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 47130

The 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.

Use The Forwarded Microphone

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:47130

For 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.wav

One 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.

Security And Ownership

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.

Daily Use

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=develop0

The 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/develop0

When 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-data for new disposable artifacts
  • root .oc_local/ when no tracked project overlay exists
  • root .worktrees/; .worktrees/<branch> as a worktree or current-branch symlink alias when BRANCH is 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.yml overrides

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.

Local Dockerfile Extensions

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/Dockerfile

Use .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.

Local Compose Overrides

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.yml

initialize.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.

Docker Registry Credentials

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 login

docker 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.

Browser Support

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.test

The 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:

chrome

The 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.test

Use 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.

QEMU Support

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-reboot

The 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.

Terminal Productivity Tools

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 | fzf

Versioned 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.

UUID Generation

The image includes uuidgen from Debian's uuid-runtime package for generating UUIDs from scripts or the terminal:

uuidgen

The release kit provides the command but does not configure uuidd as a persistent service or impose a project-specific UUID workflow.

Terminal Capture Tools

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.

Gitea CLI

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-token

Add 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 ls

Run 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.

Infrastructure As Code CLIs

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 plan

Security Scan Tools

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.

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:tag

trivy 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.

Updating The Kit

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 --oneline

The consuming project commit should normally contain only the .devcontainer gitlink update and any intentional consumer-side documentation or ignore-file updates.

Coding Agent Rules For Consumer Repos

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.yml visible to Git and keep .codegeist/Dockerfile visible to Git without FROM; commit either only when its overrides are intentional repository state.
  • Do not pin consumers to this kit's main branch 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 --hard or 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:

  1. Make the change in this kit repository, not in the consuming repository's .devcontainer/ checkout.
  2. Verify the upstream kit change before publishing a new runtime-only release branch commit.
  3. Publish the updated runtime-only release branch from this kit repository.
  4. In the consuming repository, update only the .devcontainer submodule gitlink to the new origin/release commit.
  5. 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 --oneline

If .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.

Troubleshooting

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 df

This 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.

About

Shared reproducible devcontainer source and generated consumer releases for Codegeist projects

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages