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
37 changes: 10 additions & 27 deletions src/dotfiles-sync/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dotfiles Sync (dotfiles-sync)

Syncs local Git, SSH, GPG, npm, gh, cargo, pip, yarn/pnpm config files into the devcontainer. Optionally syncs cloud credentials (AWS, kube, Docker, gh OAuth token) — opt-in only. Works on macOS, Linux, Windows (WSL and native), GitHub Codespaces, Gitpod, and DevPod. Uses a **merge strategy** for established files and a **copy-if-absent** strategy for new ones — never overwrites existing values, safe alongside cloud platform native auth and GPG signing.
Syncs local Git, SSH, GPG, npm, and yarn config files into the devcontainer. Optionally syncs cloud credentials (AWS, kube, Docker) — opt-in only. Works on macOS, Linux, Windows (WSL and native), GitHub Codespaces, Gitpod, and DevPod. Uses a **merge strategy** for established files and a **copy-if-absent** strategy for new ones — never overwrites existing values, safe alongside cloud platform native auth and GPG signing.

## Usage

Expand Down Expand Up @@ -33,7 +33,6 @@ That's it. The feature auto-detects the environment and adapts its behavior.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `username` | string | `node` | Container username that receives synchronized config files |
| `syncGhAuth` | boolean | `false` | Copy `~/.config/gh/hosts.yml` (GitHub OAuth token used by `gh` CLI) into the container's `$HOME`. Skipped on cloud environments (Codespaces / Gitpod / DevPod inject their own token). When `false`, the file is bind-mounted into `/mnt/h4dotfiles` (Feature `mounts` cannot be conditional) but **never copied** to `$HOME` and never read by anything else. Prefer the [`github-dev`](../github-dev/) feature with `GH_TOKEN` for fine-grained PATs. |
| `syncAwsConfig` | boolean | `false` | Sync `~/.aws/config` (profiles only — `~/.aws/credentials` is **never** synced). |
| `syncKubeConfig` | boolean | `false` | Sync `~/.kube/config` (cluster credentials and tokens). Skipped on cloud environments. |
| `syncDockerConfig` | boolean | `false` | Sync `~/.docker/config.json` (registry auth tokens). Skipped on cloud environments. |
Expand All @@ -45,24 +44,23 @@ That's it. The feature auto-detects the environment and adapts its behavior.
| Local Path | Final Target | Strategy | Purpose |
|------------|--------------|----------|---------|
| `~/.gitconfig` | `~/.gitconfig` | Merge via `git config` | Git user configuration |
| `~/.gitignore_global` | `~/.gitignore_global` | Copy-if-absent | Personal global gitignore |
| `~/.config/git/ignore` | `~/.config/git/ignore` | Copy-if-absent | XDG global gitignore |
| `~/.config/git/attributes` | `~/.config/git/attributes` | Copy-if-absent | XDG global gitattributes |
| `~/.config/git/config-*` | `~/.config/git/config-*` | Copy-if-absent | Modular git includes |
| `~/.ssh` | `~/.ssh` | Per-file merge | SSH keys, config, known_hosts |
| `~/.gnupg` | `~/.gnupg` | Copy-if-absent (skipped on cloud) | GPG keys for commit signing |
| `~/.npmrc` | `~/.npmrc` | Merge line-by-line | npm registry auth |
| `~/.yarnrc.yml` | `~/.yarnrc.yml` | Copy-if-absent | yarn registries / settings |
| `~/.config/pnpm/rc` | `~/.config/pnpm/rc` | Copy-if-absent | pnpm settings |
| `~/.config/gh/config.yml` | `~/.config/gh/config.yml` | Copy-if-absent | gh CLI preferences (no token) |
| `~/.cargo/config.toml` | `~/.cargo/config.toml` | Copy-if-absent | Cargo registries / profiles |
| `~/.config/pip/pip.conf` | `~/.config/pip/pip.conf` | Copy-if-absent | pip index URLs |

### Opt-in (sensitive)

**Operational note — bind-mounts are unconditional:** DevContainer Feature `mounts` cannot be gated on option values. The files below are always bind-mounted into `/mnt/h4dotfiles` at container start, regardless of the option value. The option only controls whether `sync-files.sh` copies the staged file into `$HOME`. Consequences:

- **Startup failure risk** — Docker file bind-mounts fail hard if the source path does not exist on the host. If you don't have `~/.aws/config`, `~/.kube/config`, or `~/.docker/config.json`, the container will fail to start even if the corresponding option is `false`. Create the file (it can be empty) to unblock startup.
- **Data visible in staging** — even when the option is `false`, the host file is accessible inside the container at `/mnt/h4dotfiles/<path>`. Nothing reads that path unless the option is enabled, but if your threat model requires full isolation, do not use the feature for that credential.

| Local Path | Option | Notes |
|------------|--------|-------|
| `~/.config/gh/hosts.yml` | `syncGhAuth` | GitHub OAuth token used by `gh`. The file is bind-mounted into `/mnt/h4dotfiles` unconditionally (Feature `mounts` cannot be gated on options) but **only copied to `$HOME` when `syncGhAuth: true`**. Skipped on cloud environments. For fine-grained PATs, prefer [`github-dev`](../github-dev/) + `GH_TOKEN`. |
| `~/.aws/config` | `syncAwsConfig` | AWS profiles. `~/.aws/credentials` (long-lived access keys) is **not bind-mounted** and never synced. |
| `~/.kube/config` | `syncKubeConfig` | Kubernetes cluster credentials. Skipped on cloud environments. |
| `~/.docker/config.json` | `syncDockerConfig` | Docker registry auth tokens. Skipped on cloud environments. |
Comment on lines 55 to 66
Expand All @@ -74,7 +72,7 @@ That's it. The feature auto-detects the environment and adapts its behavior.

## GitHub authentication

`gh` CLI authentication is **off by default**. Pick whichever fits your workflow:
`gh` CLI authentication is not managed by this feature. Pick whichever fits your workflow:

1. **`github-dev` feature + `GH_TOKEN`** (recommended for fine-grained scope):

Expand All @@ -90,23 +88,7 @@ That's it. The feature auto-detects the environment and adapts its behavior.
}
```

2. **Sync your local `gh auth login` token** (`syncGhAuth: true`):

```jsonc
{
"features": {
"ghcr.io/helpers4/devcontainer/dotfiles-sync:1": {
"syncGhAuth": true
}
}
}
```

The token is copied with `chmod 600` and only if `~/.config/gh/hosts.yml` does not already exist in the container. Skipped on Codespaces / Gitpod / DevPod (the platform injects its own token).

**Security note** — because DevContainer Feature `mounts` cannot be conditional on options, `~/.config/gh/hosts.yml` is bind-mounted into `/mnt/h4dotfiles/.config/gh/hosts.yml` whether or not you opt in. Nothing reads that path unless `syncGhAuth: true`, but if your threat model considers any in-container exposure unacceptable, use approach 1 or 3 instead.

3. **`gh auth login` inside the container** — token stays in the container only.
2. **`gh auth login` inside the container** — token stays in the container only.

## Merge Strategy

Expand All @@ -118,7 +100,7 @@ That's it. The feature auto-detects the environment and adapts its behavior.
| `.ssh/known_hosts` | Appends host entries not already present |
| `.ssh` keys | Copies files only if destination does not exist |
| `.gnupg` | Copied on local/WSL; **skipped on cloud environments** (see below) |
| All other files (gitignore_global, gh/config.yml, cargo, pip, yarn, pnpm, …) | **Copy-if-absent** — never overwrites an existing target |
| All other files (git/ignore, git/attributes, yarnrc.yml, …) | **Copy-if-absent** — never overwrites an existing target |

### Cloud environment protection

Expand Down Expand Up @@ -253,6 +235,7 @@ ssh-add -l

## Version History

- **v1.0.4**: Removed bind-mounts for files that are frequently absent on host machines and have little value inside a devcontainer: `~/.gitignore_global` (redundant with `~/.config/git/` directory mount), `~/.config/pnpm/rc` (pnpm store-dir is counter-productive in a container), `~/.config/gh/config.yml` and `~/.config/gh/hosts.yml` (gh CLI auth managed separately), `~/.cargo/config.toml` (cargo not relevant in most containers), `~/.config/pip/pip.conf` (too environment-specific). Docker file bind-mounts fail hard if the source path doesn't exist on the host, which was causing containers to fail to start. The `syncGhAuth` option is removed.
- **v1.0.3**: Fixed incompatibility with `docker-in-docker` feature — staging directory moved from `/tmp/dotfiles-sync/` to `/mnt/h4dotfiles/` to avoid being hidden by the tmpfs that `docker-in-docker` mounts on `/tmp` at container start.
- **v1.0.2**: Added `syncGhAuth` opt-in to copy `~/.config/gh/hosts.yml` (GitHub OAuth token used by `gh` CLI) into `$HOME`. Default `false`, skipped on cloud environments. The file is bind-mounted into `/tmp/dotfiles-sync/` regardless (Feature `mounts` cannot be conditional) but only copied to `$HOME` when the option is enabled. For fine-grained PATs prefer the `github-dev` feature with `GH_TOKEN`.
- **v1.0.1**: Stop bind-mounting the `~/.config/gh` directory. Only `~/.config/gh/config.yml` (CLI preferences) is mounted. Added 3 opt-in booleans for sensitive files: `syncAwsConfig`, `syncKubeConfig`, `syncDockerConfig` — all default `false` and skipped on cloud environments. Added low-risk dotfiles (gitignore_global, git/ignore, git/attributes, yarnrc.yml, pnpm/rc, cargo/config.toml, pip/pip.conf) with copy-if-absent strategy. `~/.aws/credentials` is never bind-mounted.
Expand Down
39 changes: 2 additions & 37 deletions src/dotfiles-sync/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
{
"id": "dotfiles-sync",
"version": "1.0.3",
"version": "1.0.4",
"name": "Dotfiles Sync",
"description": "Syncs local Git, SSH, GPG, npm, gh CLI prefs, cargo, pip, pnpm/yarn config files into the devcontainer. Optionally syncs cloud credentials (AWS, kube, Docker, gh OAuth token) — opt-in only. Works on macOS, Linux, Windows (WSL), Codespaces, Gitpod, DevPod. Merges instead of overwriting.",
"description": "Syncs local Git, SSH, GPG, npm, yarn config files into the devcontainer. Optionally syncs cloud credentials (AWS, kube, Docker) — opt-in only. Works on macOS, Linux, Windows (WSL), Codespaces, Gitpod, DevPod. Merges instead of overwriting.",
"documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/dotfiles-sync",
"options": {
"username": {
"type": "string",
"default": "node",
"description": "Username in the container that should receive synchronized local config files"
},
"syncGhAuth": {
"type": "boolean",
"default": false,
"description": "Sync ~/.config/gh/hosts.yml (GitHub OAuth token used by gh CLI). Skipped on cloud environments (Codespaces/Gitpod/DevPod inject their own token). Prefer the github-dev feature with GH_TOKEN for fine-grained PATs."
},
"syncAwsConfig": {
"type": "boolean",
"default": false,
Expand All @@ -37,11 +32,6 @@
"target": "/mnt/h4dotfiles/.gitconfig",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.gitignore_global",
"target": "/mnt/h4dotfiles/.gitignore_global",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.config/git",
"target": "/mnt/h4dotfiles/.config/git",
Expand All @@ -67,31 +57,6 @@
"target": "/mnt/h4dotfiles/.yarnrc.yml",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.config/pnpm/rc",
"target": "/mnt/h4dotfiles/.config/pnpm/rc",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.config/gh/config.yml",
"target": "/mnt/h4dotfiles/.config/gh/config.yml",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.config/gh/hosts.yml",
"target": "/mnt/h4dotfiles/.config/gh/hosts.yml",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.cargo/config.toml",
"target": "/mnt/h4dotfiles/.cargo/config.toml",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.config/pip/pip.conf",
"target": "/mnt/h4dotfiles/.config/pip/pip.conf",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.aws/config",
"target": "/mnt/h4dotfiles/.aws/config",
Expand Down
15 changes: 1 addition & 14 deletions src/dotfiles-sync/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
set -e

USERNAME="${_BUILD_ARG_USERNAME:-"${USERNAME:-"node"}"}"
SYNC_GH_AUTH="${_BUILD_ARG_DOTFILES_SYNC_SYNCGHAUTH:-"${SYNCGHAUTH:-"false"}"}"
SYNC_AWS_CONFIG="${_BUILD_ARG_DOTFILES_SYNC_SYNCAWSCONFIG:-"${SYNCAWSCONFIG:-"false"}"}"
SYNC_KUBE_CONFIG="${_BUILD_ARG_DOTFILES_SYNC_SYNCKUBECONFIG:-"${SYNCKUBECONFIG:-"false"}"}"
SYNC_DOCKER_CONFIG="${_BUILD_ARG_DOTFILES_SYNC_SYNCDOCKERCONFIG:-"${SYNCDOCKERCONFIG:-"false"}"}"
Expand All @@ -28,7 +27,6 @@ echo "Setting up dotfiles-sync devcontainer feature..."
echo " Container user: ${USERNAME}"
echo " Home directory: ${TARGET_HOME}"
echo " Mount staging: ${SOURCE_HOME}"
echo " Sync gh auth: ${SYNC_GH_AUTH}"
echo " Sync AWS config: ${SYNC_AWS_CONFIG}"
echo " Sync kube config: ${SYNC_KUBE_CONFIG}"
echo " Sync Docker config: ${SYNC_DOCKER_CONFIG}"
Expand All @@ -42,10 +40,6 @@ mkdir -p \
"${TARGET_HOME}/.ssh" \
"${TARGET_HOME}/.gnupg" \
"${TARGET_HOME}/.config/git" \
"${TARGET_HOME}/.config/gh" \
"${TARGET_HOME}/.config/pip" \
"${TARGET_HOME}/.config/pnpm" \
"${TARGET_HOME}/.cargo" \
"${TARGET_HOME}/.aws" \
"${TARGET_HOME}/.kube" \
"${TARGET_HOME}/.docker" 2>/dev/null || true
Expand All @@ -57,7 +51,6 @@ if getent passwd "${USERNAME}" >/dev/null 2>&1; then
"${TARGET_HOME}/.ssh" \
"${TARGET_HOME}/.gnupg" \
"${TARGET_HOME}/.config" \
"${TARGET_HOME}/.cargo" \
"${TARGET_HOME}/.aws" \
"${TARGET_HOME}/.kube" \
"${TARGET_HOME}/.docker" \
Expand All @@ -77,7 +70,6 @@ cat > /usr/local/share/dotfiles-sync/config << CONF_EOF
DOTFILES_SYNC_USERNAME="${USERNAME}"
DOTFILES_SYNC_SOURCE="${SOURCE_HOME}"
DOTFILES_SYNC_TARGET="${TARGET_HOME}"
DOTFILES_SYNC_GH_AUTH="${SYNC_GH_AUTH}"
DOTFILES_SYNC_AWS_CONFIG="${SYNC_AWS_CONFIG}"
DOTFILES_SYNC_KUBE_CONFIG="${SYNC_KUBE_CONFIG}"
DOTFILES_SYNC_DOCKER_CONFIG="${SYNC_DOCKER_CONFIG}"
Expand Down Expand Up @@ -152,16 +144,11 @@ echo " Shell start -> SSH_AUTH_SOCK detection + sync fallback"
echo ""
echo "Targets:"
echo " Git config -> ${TARGET_HOME}/.gitconfig"
echo " Git ignore/attrs -> ${TARGET_HOME}/.gitignore_global, ${TARGET_HOME}/.config/git/"
echo " Git ignore/attrs -> ${TARGET_HOME}/.config/git/"
echo " SSH keys -> ${TARGET_HOME}/.ssh/"
echo " GPG keys -> ${TARGET_HOME}/.gnupg/"
echo " npm tokens -> ${TARGET_HOME}/.npmrc"
echo " yarn config -> ${TARGET_HOME}/.yarnrc.yml"
echo " pnpm config -> ${TARGET_HOME}/.config/pnpm/rc"
echo " gh CLI prefs -> ${TARGET_HOME}/.config/gh/config.yml"
echo " gh OAuth token -> ${TARGET_HOME}/.config/gh/hosts.yml [opt-in: ${SYNC_GH_AUTH}]"
echo " cargo config -> ${TARGET_HOME}/.cargo/config.toml"
echo " pip config -> ${TARGET_HOME}/.config/pip/pip.conf"
echo " AWS profiles -> ${TARGET_HOME}/.aws/config [opt-in: ${SYNC_AWS_CONFIG}]"
echo " kube config -> ${TARGET_HOME}/.kube/config [opt-in: ${SYNC_KUBE_CONFIG}]"
echo " Docker auth -> ${TARGET_HOME}/.docker/config.json [opt-in: ${SYNC_DOCKER_CONFIG}]"
44 changes: 1 addition & 43 deletions src/dotfiles-sync/sync-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,11 @@
# .gnupg -> skipped on cloud environments (GPG handled natively there).
# known_hosts -> merge line-by-line (append missing host entries).
# ── extra files (v1.0.1+) — copy-if-absent strategy:
# .gitignore_global, .config/git/{ignore,attributes,config-*}
# .yarnrc.yml, .config/pnpm/rc, .cargo/config.toml, .config/pip/pip.conf
# .config/gh/config.yml -> copy-if-absent (CLI preferences only)
# .config/gh/hosts.yml -> opt-in (DOTFILES_SYNC_GH_AUTH), skipped on cloud env
# .config/git/{ignore,attributes,config-*}, .yarnrc.yml
# .aws/config -> opt-in (DOTFILES_SYNC_AWS_CONFIG)
# .kube/config -> opt-in (DOTFILES_SYNC_KUBE_CONFIG)
# .docker/config.json -> opt-in (DOTFILES_SYNC_DOCKER_CONFIG)
#
# NOTE: ~/.config/gh/hosts.yml (GitHub OAuth token) is NOT synced by default.
# Enable `syncGhAuth: true` to opt in (skipped on Codespaces/Gitpod/DevPod
# which inject their own token). For fine-grained PATs, prefer the github-dev
# feature with GH_TOKEN, or run `gh auth login` once in the container.

# No set -e: sync as much as possible even if one part fails.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
Expand All @@ -48,7 +40,6 @@ fi
USERNAME="${DOTFILES_SYNC_USERNAME}"
SOURCE_HOME="${DOTFILES_SYNC_SOURCE}"
TARGET_HOME="${DOTFILES_SYNC_TARGET}"
SYNC_GH_AUTH="${DOTFILES_SYNC_GH_AUTH:-false}"
SYNC_AWS_CONFIG="${DOTFILES_SYNC_AWS_CONFIG:-false}"
SYNC_KUBE_CONFIG="${DOTFILES_SYNC_KUBE_CONFIG:-false}"
SYNC_DOCKER_CONFIG="${DOTFILES_SYNC_DOCKER_CONFIG:-false}"
Expand Down Expand Up @@ -332,8 +323,6 @@ _copy_if_absent() {
fi
}

# ── Sync .gitignore_global ────────────────────────────────────────────────────
_copy_if_absent ".gitignore_global" ".gitignore_global" "644"

# ── Sync ~/.config/git/{ignore,attributes,config-*} ───────────────────────────
if [ -d "${SOURCE_HOME}/.config/git" ]; then
Expand All @@ -353,35 +342,6 @@ fi
# ── Sync ~/.yarnrc.yml ────────────────────────────────────────────────────────
_copy_if_absent ".yarnrc.yml" ".yarnrc.yml" "644"

# ── Sync ~/.config/pnpm/rc ────────────────────────────────────────────────────
_copy_if_absent ".config/pnpm/rc" ".config/pnpm/rc" "644"

# ── Sync ~/.cargo/config.toml ─────────────────────────────────────────────────
_copy_if_absent ".cargo/config.toml" ".cargo/config.toml" "644"

# ── Sync ~/.config/pip/pip.conf ───────────────────────────────────────────────
_copy_if_absent ".config/pip/pip.conf" ".config/pip/pip.conf" "644"

# ── Sync ~/.config/gh/config.yml (CLI preferences only) ───────────────────
if [ -e "${SOURCE_HOME}/.config/gh/config.yml" ]; then
mkdir -p "${TARGET_HOME}/.config/gh"
_copy_if_absent ".config/gh/config.yml" ".config/gh/config.yml" "600"
else
echo " .config/gh/config.yml: not found in staging"
fi

# ── Sync ~/.config/gh/hosts.yml (opt-in: GitHub OAuth token) ───────────────
if [ "${SYNC_GH_AUTH}" = "true" ]; then
if [ "${IS_CLOUD_ENV}" = "true" ]; then
echo " .config/gh/hosts.yml: skipped (cloud env — platform manages GitHub auth)"
else
mkdir -p "${TARGET_HOME}/.config/gh"
_copy_if_absent ".config/gh/hosts.yml" ".config/gh/hosts.yml [GitHub OAuth token]" "600"
fi
else
echo " .config/gh/hosts.yml: skipped (opt-in: set 'syncGhAuth' to enable)"
fi

# ── Sync ~/.aws/config (opt-in) ───────────────────────────────────────────────
if [ "${SYNC_AWS_CONFIG}" = "true" ]; then
mkdir -p "${TARGET_HOME}/.aws"
Expand Down Expand Up @@ -422,10 +382,8 @@ if [ "$(id -u)" -eq 0 ] && getent passwd "${USERNAME}" >/dev/null 2>&1; then
"${TARGET_HOME}/.gnupg" \
"${TARGET_HOME}/.gitconfig" \
"${TARGET_HOME}/.npmrc" \
"${TARGET_HOME}/.gitignore_global" \
"${TARGET_HOME}/.yarnrc.yml" \
"${TARGET_HOME}/.config" \
"${TARGET_HOME}/.cargo" \
"${TARGET_HOME}/.aws" \
"${TARGET_HOME}/.kube" \
"${TARGET_HOME}/.docker" 2>/dev/null || true
Expand Down
Loading