Sharing VS Code Extensions and Settings Across a Team

New hires keep formatting files differently and linting locally with different rules because each developer installs their own extensions and tweaks their own settings. The fix is to pin the editor's extensions and settings in devcontainer.json so opening the project provisions an identical toolchain for everyone, following the same Devcontainer Configuration Standards that pin base images and features.

Diagnostic

Confirm the drift before standardizing. Each developer's extension list and effective settings differ:

#!/usr/bin/env bash
# audit-extensions.sh — list what each dev actually has installed
set -euo pipefail
code --list-extensions --show-versions
# BAD: two developers, two different toolchains
# Dev A
[email protected]
[email protected]
# Dev B
[email protected]
# (no prettier — formats with editor default, producing diff noise)

Workspace settings that are not committed live only in each user's profile, so editor.formatOnSave and editor.tabSize vary per machine.

Root cause

VS Code stores extensions and user settings in the per-user profile, not in the repository. Without a committed .devcontainer/devcontainer.json customizations.vscode block (or a .vscode/ folder for non-container setups), nothing forces consistency. The result is divergent linters, formatters, and editor behavior that surface as noisy diffs and inconsistent lint results.

Resolution

  1. Pin extensions and settings inside customizations.vscode in devcontainer.json. Use exact extension version pins to prevent silent breaking updates.
// .devcontainer/devcontainer.json
{
  "name": "Platform Baseline",
  "dockerComposeFile": ["../docker-compose.yml"],
  "service": "app",
  "workspaceFolder": "/app",
  "customizations": {
    "vscode": {
      "extensions": [
        "[email protected]",
        "[email protected]",
        "[email protected]"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" },
        "files.eol": "\n"
      }
    }
  },
  "postCreateCommand": "npm ci"
}
  1. Pin language runtimes and CLIs as features so the linters and formatters resolve the same binaries everywhere.
// .devcontainer/devcontainer.json (features excerpt)
{
  "features": {
    "ghcr.io/devcontainers/features/node:1": { "version": "20" },
    "ghcr.io/devcontainers/features/git:1": { "version": "latest" }
  }
}
  1. Recommend the same extensions for engineers who do not open the folder in a container by committing .vscode/extensions.json. VS Code prompts them to install the set.
// .vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "ms-azuretools.vscode-docker"
  ]
}
  1. Commit workspace settings in .vscode/settings.json for host-side editors, mirroring the container settings block so both paths converge.
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "files.eol": "\n"
}
  1. Reopen the folder in the container so VS Code installs the pinned extensions and applies the settings.
#!/usr/bin/env bash
# rebuild the devcontainer to apply pinned extensions
set -euo pipefail
devcontainer up --workspace-folder . --remove-existing-container

Expected output

After reopening in the container, the installed set matches the pin exactly on every machine:

[email protected]
[email protected]
[email protected]

Prevention

Gate the configuration in CI so a malformed or unpinned change cannot merge:

# .github/workflows/devcontainer-lint.yml
name: devcontainer-lint
on:
  pull_request:
    paths: [".devcontainer/**", ".vscode/**"]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Reject floating extension pins
        run: |
          ! grep -E '"[a-z0-9-]+\.[a-zA-Z0-9-]+"\s*[,\]]' .devcontainer/devcontainer.json \
            | grep -vqE '@[0-9]+\.[0-9]+\.[0-9]+'

macOS (Docker Desktop): extension install runs inside the Linux VM on first open; expect a one-time delay, not a recurring cost. WSL2: keep the repo on the Linux filesystem (~/code, not /mnt/c) so the Dev Containers extension resolves ${localWorkspaceFolder} to a native path. Apple Silicon (ARM64): a few extensions ship architecture-specific native binaries; verify they have arm64 builds or the language server may fail to start.

Rollback

#!/usr/bin/env bash
set -euo pipefail
git checkout -- .devcontainer/devcontainer.json .vscode/
devcontainer up --workspace-folder . --remove-existing-container