Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Validate Dev Container Features

on:
pull_request:
workflow_dispatch:

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate metadata and shell script syntax
run: bash test/validate.sh
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ agent-specific repository. It provides the following features:
- Roo Code
- Playwright Agent CLI
- GitLab CLI
- tmux
- Vim

**Claude Code** has an [official Dev Container Feature](https://github.com/anthropics/devcontainer-features). This repository provides a companion Feature that adds persistence on top of the official installation.

Expand All @@ -35,7 +37,9 @@ This keeps the setting out of each project's `.devcontainer/devcontainer.json`.
"ghcr.io/temeteke/devcontainer-features/cline:0": {},
"ghcr.io/temeteke/devcontainer-features/roo-code:0": {},
"ghcr.io/temeteke/devcontainer-features/playwright:0": {},
"ghcr.io/temeteke/devcontainer-features/gitlab-cli:0": {}
"ghcr.io/temeteke/devcontainer-features/gitlab-cli:0": {},
"ghcr.io/temeteke/devcontainer-features/tmux:0": {},
"ghcr.io/temeteke/devcontainer-features/vim:0": {}
}
}
```
Expand Down Expand Up @@ -87,6 +91,8 @@ other sensitive data. Since they are stored directly in your host home directory
| `playwright` | AI agent tool: Playwright Agent CLI and browser runtime only | none |
| `gitlab-cli` | Coding tool: `glab` CLI from GitLab releases | `~/.config/glab-cli` |
| `agents` | Shared feature for agent skills/state only | `~/.agents` |
| `tmux` | `tmux` via apt (Debian/Ubuntu) | read-only `~/.tmux.conf` |
| `vim` | `vim` via apt (Debian/Ubuntu) | read-only `~/.vimrc`, read-write `~/.vim` |

## Publishing

Expand All @@ -99,3 +105,22 @@ fork the repository.
This repository is an unofficial collection of Dev Container Features.
It is not affiliated with, endorsed by, or maintained by OpenAI, Anthropic,
Roo Code, Cline, OpenCode, or GitLab.

## Terminal Features

The `tmux` Feature installs tmux and shares the host's `~/.tmux.conf`
file read-only. The `vim` Feature installs Vim and shares `~/.vimrc`
read-only and `~/.vim/` read-write. This supports standard dotfile layouts,
including Vim configurations that install plugins or write undo data under
`~/.vim/`.

Before starting a Dev Container, ensure the corresponding host paths exist.
A missing bind-mount source may prevent the container from starting.
These Features do not mount the entire host HOME directory.

The writable `~/.vim/` mount allows processes in the container to modify
host Vim configuration, plugins and state. Review this security tradeoff
before using these Features with untrusted code.

See [`src/tmux/README.md`](src/tmux/README.md) and
[`src/vim/README.md`](src/vim/README.md) for details.
28 changes: 28 additions & 0 deletions src/tmux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# tmux Feature

Installs tmux on Debian/Ubuntu and exposes the **host** `~/.tmux.conf`
file in the container, read-only. A symlink at the container user's
`~/.tmux.conf` points to the mounted file.

## Host configuration

The Feature uses tmux's conventional `~/.tmux.conf` configuration file.
It does not mount `~/.config/tmux/` or other host configuration paths.

Before building/starting the Dev Container, make sure `~/.tmux.conf`
is a regular file or a resolvable symlink to a file on the host. A missing
file bind mount cannot be skipped dynamically; the container may fail to start.

## Usage

```json
{
"features": {
"ghcr.io/temeteke/devcontainer-features/tmux:0": {}
}
}
```

Only the configuration file is shared; tmux server/socket/session state
belongs to the temporary container. The container startup script refuses
to overwrite an existing `~/.tmux.conf` file or unrelated symlink.
15 changes: 15 additions & 0 deletions src/tmux/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "tmux",
"version": "0.1.0",
"name": "tmux",
"description": "Installs tmux and shares the host ~/.tmux.conf read-only.",
"mounts": [
{
"type": "bind",
"source": "${localEnv:HOME}/.tmux.conf",
"target": "/mnt/devcontainer-features/tmux/tmux.conf",
"readonly": true
}
],
"postCreateCommand": "/usr/local/share/devcontainer-features/tmux/setup-persistence.sh"
}
15 changes: 15 additions & 0 deletions src/tmux/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail

if ! command -v apt-get >/dev/null 2>&1; then
echo "[tmux] Debian/Ubuntu (apt-get) is required." >&2
exit 1
fi

export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends tmux
rm -rf /var/lib/apt/lists/*

mkdir -p "/usr/local/share/devcontainer-features/tmux"
install -m 0755 "${0%/*}/setup-persistence.sh" "/usr/local/share/devcontainer-features/tmux/setup-persistence.sh"
25 changes: 25 additions & 0 deletions src/tmux/setup-persistence.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail

SOURCE="/mnt/devcontainer-features/tmux/tmux.conf"
: "${HOME:?HOME must be set}"
TARGET="$HOME/.tmux.conf"

if [[ ! -f "$SOURCE" ]]; then
echo "[tmux] Host ~/.tmux.conf mount is missing or is not a file: $SOURCE" >&2
exit 1
fi

if [[ -L "$TARGET" ]]; then
if [[ "$(readlink "$TARGET")" == "$SOURCE" ]]; then
exit 0
fi
echo "[tmux] Refusing to replace existing symlink: $TARGET" >&2
exit 1
elif [[ -e "$TARGET" ]]; then
echo "[tmux] Refusing to overwrite existing container file: $TARGET" >&2
exit 1
fi

ln -s "$SOURCE" "$TARGET"
echo "[tmux] Linked $TARGET -> $SOURCE"
37 changes: 37 additions & 0 deletions src/vim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Vim Feature

Installs the full `vim` package on Debian/Ubuntu. Exposes the **host**
`~/.vimrc` **read-only** and `~/.vim/` **read-write** and links both
paths into the container user's HOME.

## Host configuration

The Feature uses Vim's conventional `~/.vimrc` and `~/.vim/` paths.
Configurations that use different locations must arrange those paths on
the host before starting the container.

Make sure both `~/.vimrc` (file) and `~/.vim/` (directory) exist on the
host before starting a container. Missing bind sources may prevent startup.

**Why is `~/.vim` writable?** Some Vim configurations install or update
plugins and store undo or other runtime data inside `~/.vim/`. A read-only
mount would prevent these operations. This also means processes running
inside the container can modify host Vim configuration, plugins and state.
Review this exception to workspace isolation before using the Feature
with untrusted code.

The optional `~/.vimrc.local` is not mounted, so Vim skips it unless
another mount provides it.

## Usage

```json
{
"features": {
"ghcr.io/temeteke/devcontainer-features/vim:0": {}
}
}
```

The setup script refuses to overwrite existing nonempty container-side
configuration. Use a base image with an empty/default Vim HOME.
21 changes: 21 additions & 0 deletions src/vim/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "vim",
"version": "0.1.0",
"name": "Vim",
"description": "Installs Vim and shares host ~/.vimrc (read-only) and ~/.vim (read-write).",
"mounts": [
{
"type": "bind",
"source": "${localEnv:HOME}/.vimrc",
"target": "/mnt/devcontainer-features/vim/vimrc",
"readonly": true
},
{
"type": "bind",
"source": "${localEnv:HOME}/.vim",
"target": "/mnt/devcontainer-features/vim/vim",
"readonly": false
}
],
"postCreateCommand": "/usr/local/share/devcontainer-features/vim/setup-persistence.sh"
}
15 changes: 15 additions & 0 deletions src/vim/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail

if ! command -v apt-get >/dev/null 2>&1; then
echo "[vim] Debian/Ubuntu (apt-get) is required." >&2
exit 1
fi

export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends vim
rm -rf /var/lib/apt/lists/*

mkdir -p "/usr/local/share/devcontainer-features/vim"
install -m 0755 "${0%/*}/setup-persistence.sh" "/usr/local/share/devcontainer-features/vim/setup-persistence.sh"
58 changes: 58 additions & 0 deletions src/vim/setup-persistence.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail

CONFIG_FILE="/mnt/devcontainer-features/vim/vimrc"
CONFIG_DIR="/mnt/devcontainer-features/vim/vim"
: "${HOME:?HOME must be set}"

if [[ ! -f "$CONFIG_FILE" ]]; then
echo "[vim] Host ~/.vimrc mount is missing or is not a file: $CONFIG_FILE" >&2
exit 1
fi
if [[ ! -d "$CONFIG_DIR" ]]; then
echo "[vim] Host ~/.vim mount is missing or is not a directory: $CONFIG_DIR" >&2
exit 1
fi

link_config() {
local source="$1" target="$2" kind="$3"

if [[ -L "$target" ]]; then
if [[ "$(readlink "$target")" == "$source" ]]; then
return 0
fi
echo "[vim] Refusing to replace existing symlink: $target" >&2
return 1
fi

if [[ "$kind" == dir && -d "$target" ]]; then
if [[ -n "$(ls -A "$target")" ]]; then
echo "[vim] Refusing to replace nonempty directory: $target" >&2
return 1
fi
rmdir "$target"
elif [[ -e "$target" ]]; then
echo "[vim] Refusing to overwrite existing container path: $target" >&2
return 1
fi

ln -s "$source" "$target"
echo "[vim] Linked $target -> $source"
}

# Check both destinations before modifying either to avoid partial migration.
for dest in "$HOME/.vimrc" "$HOME/.vim"; do
case "$dest" in
"$HOME/.vimrc") src="$CONFIG_FILE"; kind=file ;;
"$HOME/.vim") src="$CONFIG_DIR"; kind=dir ;;
esac
if [[ -L "$dest" && "$(readlink "$dest")" != "$src" ]] ||
[[ ! -L "$dest" && -e "$dest" &&
( "$kind" == file || ! -d "$dest" || -n "$(ls -A "$dest")" ) ]]; then
echo "[vim] Refusing to replace pre-existing container configuration: $dest" >&2
exit 1
fi
done

link_config "$CONFIG_FILE" "$HOME/.vimrc" file
link_config "$CONFIG_DIR" "$HOME/.vim" dir
Empty file modified test/validate.sh
100644 → 100755
Empty file.
Loading