Skip to content

Latest commit

 

History

History
1109 lines (885 loc) · 46.6 KB

File metadata and controls

1109 lines (885 loc) · 46.6 KB

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. The image uses Ubuntu 26.04 LTS.

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 Go, PowerShell as pwsh for cross-platform shell and automation work, Task with Bash completion, the official Gitea tea CLI, Renovate, 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. It also includes Podman as a daemonless alternative alongside the existing Docker toolchain. 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

  • Consuming repositories can add an optional tracked .codegeist/extensions/custom_initialize.sh Bash hook for host-side setup after normal kit initialization in the opened checkout.
  • 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.
  • Directly opened Git worktrees mount their exact common Git metadata while worktree creation and submodule initialization remain explicit user steps.
  • 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. /.worktrees/ is only a conventional ignore for explicitly created repository-local worktrees; the kit does not create the directory.

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. It also removes obsolete exact /.local.env and /compose.local.yml ignore lines so unsupported legacy root files remain visible.

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 use a Git worktree, create it explicitly, initialize its submodules when the repository has any, and open that checkout directly:

git worktree add .worktrees/feature-x feature-x
git -C .worktrees/feature-x submodule update --init --recursive
code .worktrees/feature-x

For a new branch, select its base explicitly:

git worktree add -b feature-x .worktrees/feature-x origin/main
git -C .worktrees/feature-x submodule update --init --recursive
code .worktrees/feature-x

The Dev Containers CLI uses the same direct-checkout workflow:

npx --yes @devcontainers/cli up --workspace-folder .worktrees/feature-x

The kit does not create worktrees, aliases, branches, or submodules. It mounts the opened checkout at its host-identical absolute path and mounts the exact absolute common Git directory reported by git rev-parse --path-format=absolute --git-common-dir. This lets the linked worktree's .git file resolve without mounting another checkout. The actual checked-out branch determines the branch-aware Compose project name and hostname.

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

Root .local.env and compose.local.yml files are not configuration inputs and are never copied or migrated. The initializer removes their obsolete generated ignore lines so accidental legacy files remain visible to Git. Move intended settings to .codegeist/.local.env or .codegeist/compose.local.yml explicitly; move devcontainer image extensions to .codegeist/Dockerfile.

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.

Each directly opened checkout owns independent .codegeist local state, .oc_local bootstrap, .chrome profile, generated .devcontainer files, and Compose project. The kit does not link or synchronize that state across worktrees.

Custom Initialize Hook

A consuming repository can extend host-side initialization with an optional .codegeist/extensions/custom_initialize.sh Bash script. After the normal kit setup completes, initialize.sh runs the hook from the opened checkout.

The file does not need to be executable. A missing hook is ignored, while a failing hook fails initializeCommand. The hook runs on the host outside container isolation and may run repeatedly, so keep tracked hook code trusted, non-interactive, idempotent, and bounded. Environment exports made by the hook do not survive after initialization; write required runtime values through an existing documented file contract instead.

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.

Container Engines

The release image provides both the existing Docker toolchain and Podman. Docker remains the default engine: the container entrypoint starts the nested Docker daemon, and Docker CLI, Compose, and Buildx continue to use it without redirection.

Podman is an additional daemonless option. The normal workspace user can run a fully qualified image without sudo:

podman run --rm docker.io/library/hello-world

The image includes the uidmap, slirp4netns, fuse-overlayfs, and passt prerequisites required by this rootless path on the containerized workspace filesystem. It does not start a Podman API service, replace the docker command, or persist Podman storage across devcontainer recreation.

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. Visible interactive Chrome is supported only through a Remote SSH loopback X11 display. 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. It accepts only DISPLAY=localhost:N[.screen] or 127.0.0.1:N[.screen] from Remote SSH X11 forwarding. Local DISPLAY=:N, arbitrary non-loopback X11 hosts, and Wayland are not supported visible transports. The shared Compose configuration does not mount host display sockets.

VS Code SSH reconnects can allocate a new loopback display number while reusing an existing container. Each initialize run atomically refreshes the opened checkout'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 only the requested display's matching /unix:N cookie into a private temporary authority file. It exits before Google Chrome starts when the display is missing, stale, unreachable, or unauthorized. Directly opened worktrees keep independent generated display state, Xauthority files, and .chrome profiles. Reconnect recovery uses host networking and refreshed checkout-local authority state. Use chrome --headless ... for automation when no supported visible session 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 source release regression starts real non-headless Chrome through authenticated loopback X11 shaped like Remote SSH forwarding, verifies rendered content through CDP, and confirms that profile state stays in the opened checkout. Headless rendering is tested separately. Both paths must pass before the source release workflow accepts a commit.

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, 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 Ubuntu'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 per persistent container user configuration. After a container rebuild, repeat the setup when tea login list no longer shows the login. This command reads the token from GITEA_SERVER_TOKEN, stores it in Tea's user-owned login configuration, and registers Tea as Git's HTTPS credential helper without placing the token in the command or remote URL:

tea login add --name codegeist --url "$GITEA_SERVER_URL" \
  --insecure --git-credentials --no-version-check
tea login list
tea repos ls
tea issues ls
tea pulls ls

Use --oauth instead of the environment token when an interactive browser login is preferred. For this repository's Gitea server, --insecure applies to Tea only. Git operations require their own command-local TLS exception while the server's certificate chain cannot be verified:

GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/false SSH_ASKPASS=/bin/false \
  git -c http.sslVerify=false fetch origin main
GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/false SSH_ASKPASS=/bin/false \
  git -c http.sslVerify=false push origin main

Disabling inherited VS Code Askpass fallbacks makes a missing Tea credential fail instead of opening another authentication path. If the Tea login still exists but its Git integration is missing, restore the host-specific helper with tea login helper setup. Do not replace Tea with a temporary credential helper, an authorization header, or credentials in the remote URL.

Never persist http.sslVerify=false in Git configuration. Use these commands only for the known git.codegeist.ai origin; servers with a valid certificate chain should omit both --insecure and the Git TLS exception.

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.

Bitwarden CLI

The image includes the official Bitwarden CLI as bw for user-initiated access to Bitwarden-compatible servers such as Vaultwarden. The release kit does not configure a server, log in, unlock a vault, or provide credentials. Point the CLI at the self-hosted server before the first login:

The CLI stores its project-local configuration, account metadata, login state, and encrypted cache under $DEVCONTAINER_REPO_ROOT/.codegeist/secrets/bitwarden-cli. The repository root is mounted persistently and the parent secrets directory is ignored by Git. Each directly opened checkout has independent .codegeist state. The first bw invocation creates the directory and data.json; the release kit does not migrate an existing ~/.config/Bitwarden CLI directory.

bw config server https://vault.example.com

Choose one login method. A normal interactive login prompts for the email, master password, and any supported two-step login code. Personal API-key login prompts for the client ID and client secret so they do not need to appear in the command or shell history:

bw login
bw login --apikey

Browser-based login is available when the Vaultwarden deployment has compatible OpenID Connect SSO enabled and the devcontainer can open a usable browser:

bw login --sso

API-key and SSO login authenticate the CLI but normally do not decrypt vault contents. Unlock explicitly, keep the returned session key only in the current shell, and synchronize before reading items:

export BW_SESSION="$(bw unlock --raw)"
bw sync
bw list items

Do not put a client secret, master password, or BW_SESSION in tracked files, Docker build arguments, or shell command arguments. Lock the vault and remove the session variable when finished; use bw logout instead when the saved login itself should be removed:

bw lock
unset BW_SESSION
# bw logout

Infrastructure As Code CLIs

The image includes Terraform as terraform, OpenTofu as tofu, and the HashiCorp Vault CLI as vault. Vault is installed from HashiCorp's official signed Ubuntu repository alongside Terraform; the release kit does not configure a Vault server, address, authentication method, or token. Point the CLI at your Vault server and authenticate through the method required by that server:

export VAULT_ADDR=https://vault.example.com
vault status
vault login

Keep Vault tokens and other credentials out of tracked files and shell command arguments.

Workspace Vault Agent Configuration

When secrets.hcl exists at the selected workspace root, the container entrypoint runs vault agent -config=<workspace>/secrets.hcl before the requested container command. A missing file is ignored, while an invalid configuration or another Vault Agent failure stops container startup. The configuration owns its render destinations and must let the Agent exit when rendering is complete.

The workspace service always provides /run/secrets as a tmpfs for configurations that choose to render secrets there. Using that path is optional; the kit does not inspect or modify secrets.hcl.

OpenTofu is installed from its official signed APT 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 repository-local worktrees. 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 VS Code opens the wrong checkout, close that window and open the intended checkout path directly. The kit does not select a checkout or change branches.

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.