A collection of Dev Container Features published to GHCR. Drop any of them into a devcontainer.json to get the same tooling we use internally.
| ID | Name | Description | Reference |
|---|---|---|---|
claude |
Claude Code | Installs the Claude Code CLI (@anthropic-ai/claude-code). |
ghcr.io/synergy-shock/devcontainer-features/claude |
gitbutler |
GitButler CLI | Installs the GitButler CLI (but). |
ghcr.io/synergy-shock/devcontainer-features/gitbutler |
node |
Node.js | Installs Node.js from the official nodejs.org prebuilt binaries, with optional npm and pnpm pinning. NVM-free. |
ghcr.io/synergy-shock/devcontainer-features/node |
opencode |
OpenCode | Installs the OpenCode AI CLI. | ghcr.io/synergy-shock/devcontainer-features/opencode |
rtk |
rtk (Rust Token Killer) | Installs the rtk CLI proxy. Pairs with claude and/or opencode. |
ghcr.io/synergy-shock/devcontainer-features/rtk |
Reference a feature from any devcontainer.json by its GHCR URL and version tag:
Version tags follow the MAJOR, MAJOR.MINOR, and MAJOR.MINOR.PATCH aliases that devcontainers/action publishes automatically.
These features are intentionally narrow — one upstream CLI each. For everything else (a shell, language runtimes, git, GitHub auth), reach for Microsoft's official base images and the devcontainers/features catalog before writing your own. Both are first-party, versioned, and reviewed.
Start from an official base image. Pick the one that already ships the runtime you need so you don't reinstall it as a feature:
mcr.microsoft.com/devcontainers/base:<distro>— minimal Debian/Ubuntu withvscodeuser, sudo, common utilities. The right default for ourclaude/gitbutler/opencode/rtkfeatures, none of which need Node.mcr.microsoft.com/devcontainers/typescript-node:<node>-<distro>— base + Node.js +pnpmandyarnvia corepack, ships with anodeuser. Use this when you actually need a JS toolchain; it removes the need for any pnpm feature.
Layer official features for shared tooling. From ghcr.io/devcontainers/features:
node:1— install Node + npm, and pnpm via itspnpmVersionoption (and yarn viainstallYarnUsingApt). NVM-based. Use it if you want NVM's multi-version management; reach for this repo'snodefeature instead if you want a single, NVM-free Node install (officialnodejs.orgprebuilt binaries straight into/usr/local).common-utils:2— sudo, curl/wget, useful shells, the canonical non-root user setup. Add it when you start from a non-devcontainers/baseimage. Required before this repo'snodefeature.git:1— newer git than what's in older distros.git-lfs:1— Git LFS, if your repo uses it.github-cli:1—ghfor PR/issue workflows andgh authagainst the host.
A typical Node-flavored stack ends up looking like:
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/git-lfs:1": {},
"ghcr.io/synergy-shock/devcontainer-features/claude:2": {},
"ghcr.io/synergy-shock/devcontainer-features/gitbutler:0": {}
}
}If you don't need Node at all, drop the image down to mcr.microsoft.com/devcontainers/base:trixie and skip the node feature entirely.
The features install binaries; the mounts and containerEnv in this repo's own .devcontainer/devcontainer.json are a worked example of how to thread host credentials, caches, and services through so those binaries have something to talk to.
- Bind host config, not container config. Treat the container as disposable. Anything you would lose on rebuild (auth tokens, AI session state, shell history for the tools you care about) should live on the host and be mounted in.
- Named volumes for caches. Bind mounts share the host filesystem's permissions and inode layout, which is fine for config but slow and fragile for large package caches. Use a Docker named volume for anything write-heavy (a shared pnpm/yarn store, build caches, Docker layer caches inside the container).
readonlyfor credentials you only need to read..npmrcand the 1Password signing helper are mounted readonly so a misbehaving tool inside the container cannot corrupt host state.- Pair every socket bind with an env var. A forwarded socket with no
SSH_AUTH_SOCK(or equivalent) pointing at it is just a file.
On macOS, Docker Desktop forwards the host SSH agent socket to /run/host-services/ssh-auth.sock. Bind it into the container and point SSH_AUTH_SOCK at it so ssh and git can use keys held by 1Password without copying them into the container. The op-ssh-sign helper is bind-mounted readonly so commits can be signed against a 1Password-managed key — configure git to invoke it via gpg.ssh.program.
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie",
"features": {
"ghcr.io/synergy-shock/devcontainer-features/claude:2": {}
},
"mounts": [
"source=/run/host-services/ssh-auth.sock,target=/agent.sock,type=bind",
"source=/Applications/1Password.app/Contents/MacOS/op-ssh-sign,target=/op-ssh-sign,type=bind,readonly"
],
"containerEnv": {
"SSH_AUTH_SOCK": "/agent.sock"
}
}This repo's own .devcontainer/devcontainer.json composes features from ../src/<id> so contributors can iterate on the install scripts in-place. Open the repo in VS Code Dev Containers, or run:
devcontainer up --workspace-folder .Then inside the container verify the binaries you touched:
claude --version
but --version
node --version
opencode --version
pnpm --version
rtk --versionTests use the official @devcontainers/cli. Install it once:
npm install -g @devcontainers/cliThen from the repo root:
# Autogenerated checks for a single feature
devcontainer features test -f claude \
-i mcr.microsoft.com/devcontainers/base:trixie \
-p .
# Custom scenarios for a single feature
devcontainer features test -f opencode --skip-autogenerated -p .
# Cross-feature combinations
devcontainer features test --global-scenarios-only -p .Each per-feature test lives at test/<id>/test.sh; scenario tests at test/<id>/<scenario>.sh with the matching test/<id>/scenarios.json; cross-feature tests in test/_global/.
CI runs all three of these in .github/workflows/test.yaml on every push and PR.
- Bump
versioninsrc/<id>/devcontainer-feature.jsonfor any feature you changed. Follow SemVer. - Merge the PR to
main. - From the Actions tab, run the Release features workflow (
workflow_dispatch). It will:- publish each feature to
ghcr.io/synergy-shock/devcontainer-features/<id>with the new version tag - open a follow-up PR with auto-generated
src/<id>/README.mdupdates
- publish each feature to
- First-time only, visit https://github.com/orgs/Synergy-Shock/packages (or your account
?tab=packagesif published under a user namespace), open each new package, and change its visibility to public. GHCR publishes private by default.
- Author hand-written context in
src/<id>/NOTES.md— it gets concatenated into the auto-generatedREADME.mdso consumers see it. - Don't edit
src/<id>/README.mdby hand; the release workflow regenerates it. - Run the relevant
devcontainer features testlocally before opening a PR.
MIT — see LICENSE.
{ "image": "mcr.microsoft.com/devcontainers/base:trixie", "features": { "ghcr.io/synergy-shock/devcontainer-features/claude:2": {}, "ghcr.io/synergy-shock/devcontainer-features/opencode:1": {}, "ghcr.io/synergy-shock/devcontainer-features/rtk:0": {}, "ghcr.io/synergy-shock/devcontainer-features/gitbutler:0": {} } }