Conversation
Persists GitHub Copilot CLI settings and chat history across container rebuilds using a named Docker volume mounted at /copilot-data with a symlink from ~/.copilot. - Add install script with root/non-root user handling - Add devcontainer-feature.json with volume mount and env var - Add tests (default, debian, ubuntu scenarios) - Update root README with feature documentation - Disable generate-docs to preserve custom feature READMEs
There was a problem hiding this comment.
Pull request overview
Adds a new copilot-persistence devcontainer Feature intended to persist GitHub Copilot CLI state across container rebuilds by mounting a named Docker volume at /copilot-data and linking ~/.copilot to that volume.
Changes:
- Introduces the
copilot-persistenceFeature (feature metadata, install script, and README). - Adds automated feature tests (default + Debian/Ubuntu scenarios).
- Updates repository documentation and disables auto doc generation in the release workflow.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/copilot-persistence/devcontainer-feature.json |
Defines the feature metadata, volume mount to /copilot-data, and COPILOT_DATA_DIR env var. |
src/copilot-persistence/install.sh |
Creates /copilot-data, sets ownership/permissions, and creates the ~/.copilot symlink. |
src/copilot-persistence/README.md |
Documents how the feature works, what persists, and how to enable it. |
test/copilot-persistence/test.sh |
Default scenario verification for mount/env var/symlink behavior. |
test/copilot-persistence/debian.sh |
Debian scenario verification for mount/env var/symlink behavior. |
test/copilot-persistence/ubuntu.sh |
Ubuntu scenario verification for mount/env var/symlink behavior. |
test/copilot-persistence/scenarios.json |
Defines Debian/Ubuntu scenario images for feature testing. |
README.md |
Adds top-level documentation for the new feature. |
.github/workflows/release.yaml |
Disables generate-docs during publishing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Resolve home directory properly (root uses /root, others use /home/<user>) | ||
| if [ "$USERNAME" = "root" ]; then | ||
| USER_HOME="/root" | ||
| else | ||
| USER_HOME="/home/${USERNAME}" | ||
| fi |
There was a problem hiding this comment.
Home directory resolution assumes non-root users always live in /home/. This breaks for images/users with non-standard home paths (or when USERNAME doesn't exist). Resolve the home directory via the passwd database (e.g., getent/eval echo "~user") and fail fast if the user can't be resolved, rather than hardcoding /home.
| # Resolve home directory properly (root uses /root, others use /home/<user>) | |
| if [ "$USERNAME" = "root" ]; then | |
| USER_HOME="/root" | |
| else | |
| USER_HOME="/home/${USERNAME}" | |
| fi | |
| # Resolve home directory via the passwd database or shell expansion | |
| USER_HOME="" | |
| if command -v getent >/dev/null 2>&1; then | |
| USER_HOME="$(getent passwd "${USERNAME}" | cut -d: -f6)" | |
| fi | |
| if [ -z "${USER_HOME}" ]; then | |
| USER_HOME="$(eval echo "~${USERNAME}" 2>/dev/null || true)" | |
| fi | |
| if [ -z "${USER_HOME}" ]; then | |
| echo "Error: Unable to determine home directory for user '${USERNAME}'" >&2 | |
| exit 1 | |
| fi |
|
|
||
| # Fix ownership for the user | ||
| chown -R "${USER_UID}:${USER_GID}" /copilot-data | ||
| chmod 755 /copilot-data |
There was a problem hiding this comment.
/copilot-data is intended to store Copilot configuration and chat history, which can be sensitive. Setting permissions to 755 allows other users in the container to read it. Prefer 700 (or at least 750 with an appropriate group) after chowning to the target user.
| chmod 755 /copilot-data | |
| chmod 700 /copilot-data |
| if [ ! -L "${USER_HOME}/.copilot" ]; then | ||
| rm -rf "${USER_HOME}/.copilot" 2>/dev/null || true | ||
| ln -sf /copilot-data "${USER_HOME}/.copilot" | ||
| chown -h "${USER_UID}:${USER_GID}" "${USER_HOME}/.copilot" | ||
| fi |
There was a problem hiding this comment.
The symlink creation guard only checks whether ~/.copilot is a symlink, not whether it points to /copilot-data. If a symlink already exists but targets a different path, the feature will silently leave it in place. Update the condition to also validate the symlink target and replace it when it’s not /copilot-data.
| { | ||
| "id": "copilot-persistence", | ||
| "version": "1.0.0", | ||
| "name": "Copilot CLI Persistence", | ||
| "description": "Persists GitHub Copilot CLI settings and chat history across rebuilds", | ||
| "mounts": [ |
There was a problem hiding this comment.
All existing features in this repo include a documentationURL in devcontainer-feature.json, but this new feature does not. Add documentationURL pointing at src/copilot-persistence to match the repository convention and to improve discoverability in registries/tools.
| "mounts": [ | ||
| { | ||
| "source": "copilot-cli-data", | ||
| "target": "/copilot-data", | ||
| "type": "volume" | ||
| } | ||
| ], |
There was a problem hiding this comment.
The volume name is hard-coded to copilot-cli-data, which means containers from unrelated projects on the same Docker host will share the same Copilot data by default. Consider adding a feature option to override the volume name (defaulting to the current value) so users can avoid cross-project data mixing when desired.
|
|
||
| ```json | ||
| { | ||
| "features": { |
There was a problem hiding this comment.
The README usage example enables only copilot-persistence, but the text describes persisting GitHub Copilot CLI state. Since this feature is intended to be used alongside the Copilot CLI feature, update the example to include ghcr.io/devcontainers/features/copilot-cli:1 (or explicitly document that Copilot CLI must be installed separately).
| "features": { | |
| "features": { | |
| "ghcr.io/devcontainers/features/copilot-cli:1": {}, |
Instead of chmod 777, add a /etc/profile.d script that fixes ownership of /copilot-data on login when the volume mount overrides build-time permissions.
- Resolve home directory via getent/passwd instead of hardcoding /home/<user> - Use chmod 700 instead of 755 for sensitive copilot data - Fix symlink guard to also verify target path points to /copilot-data - Add documentationURL to devcontainer-feature.json - Include copilot-cli feature in README usage example Skipped configurable volume name - cross-project sharing is the intended behavior for copilot data persistence.
Matches the shell-history pattern from stuartleeks/dev-container-features. Each dev container gets its own isolated copilot data volume.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| echo "Error: Unable to determine home directory for user '${USERNAME}'" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
There was a problem hiding this comment.
USER_HOME fallback via eval echo "~${USERNAME}" can succeed even when the user doesn’t exist (it returns the literal string like ~someuser), so USER_HOME becomes a non-absolute path and the script may create /~someuser or similar and link config into the wrong place. Consider validating that the resolved home is an absolute existing directory (or that getent passwd/id succeeds for the user) and failing fast if the user cannot be resolved.
| # Validate that USER_HOME is an absolute path to avoid creating paths like /~someuser | |
| case "${USER_HOME}" in | |
| /*) | |
| # OK: absolute path | |
| ;; | |
| *) | |
| echo "Error: Resolved home directory '${USER_HOME}' for user '${USERNAME}' is not an absolute path" >&2 | |
| exit 1 | |
| ;; | |
| esac |
| #!/bin/bash | ||
| # Fix /copilot-data ownership if it exists and is not writable by current user | ||
| if [ -d /copilot-data ] && [ ! -w /copilot-data ]; then | ||
| sudo chown -R "$(id -u):$(id -g)" /copilot-data 2>/dev/null || true | ||
| fi |
There was a problem hiding this comment.
The generated init script runs sudo chown -R ... unconditionally and suppresses all errors. On images without sudo or where the user doesn’t have passwordless sudo, this will silently fail (or can block if sudo prompts), leaving /copilot-data non-writable and breaking the feature. Consider handling the cases explicitly: if running as root use chown directly; otherwise only run sudo if it’s present and non-interactive (e.g., sudo -n), and emit a clear warning/error when ownership cannot be fixed.
| # Create a symlink from the default location to our persistent volume | ||
| # This handles the case where Copilot doesn't use XDG_CONFIG_HOME correctly | ||
| mkdir -p "${USER_HOME}" | ||
| if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then |
There was a problem hiding this comment.
Replacing ~/.copilot via rm -rf can delete any pre-existing Copilot configuration/history that might already be present in the image or created earlier in the build. Consider migrating existing contents into /copilot-data (or at minimum backing them up and/or gating the removal behind an explicit check) before swapping to the symlink.
| if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then | |
| if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then | |
| # If there is pre-existing Copilot data, back it up into /copilot-data before replacing it | |
| if [ -e "${USER_HOME}/.copilot" ] && [ ! -L "${USER_HOME}/.copilot" ]; then | |
| BACKUP_DIR="/copilot-data/preexisting-copilot-$(date +%s)" | |
| mkdir -p "${BACKUP_DIR}" 2>/dev/null || true | |
| # Copy contents rather than move to avoid failures across filesystems | |
| cp -a "${USER_HOME}/.copilot/." "${BACKUP_DIR}/" 2>/dev/null || true | |
| fi |
| publish-features: "true" | ||
| base-path-to-features: "./src" | ||
| generate-docs: "true" | ||
| generate-docs: "false" |
There was a problem hiding this comment.
Disabling generate-docs changes the release workflow behavior for all features and will stop updating the existing auto-generated feature READMEs (which currently indicate they’re generated from devcontainer-feature.json). If the goal is to keep a custom README for this feature, consider keeping generate-docs: "true" and using the devcontainers-supported NOTES.md mechanism for custom content, or otherwise updating the repo’s documentation approach consistently across features.
| generate-docs: "false" | |
| generate-docs: "true" |
| 1. **Mounts a named volume** (scoped per dev container via `${devcontainerId}`) to `/copilot-data` | ||
| 2. **Creates a symlink** from `~/.copilot` → `/copilot-data` | ||
| 3. **Sets ownership** to the container user during installation (auto-detects from `$_REMOTE_USER`) | ||
|
|
There was a problem hiding this comment.
The README doesn’t mention that installation replaces ~/.copilot with a symlink (and may remove an existing directory). It would be helpful to document this explicitly (including any migration/backup behavior) so users aren’t surprised by potential data loss when enabling the feature.
Persists GitHub Copilot CLI settings and chat history across container rebuilds using a named Docker volume mounted at /copilot-data with a symlink from ~/.copilot.