Cross-platform developer environment setup for machines without local admin rights.
Installs Git, Node.js, Prettier, Python, PHP, PowerShell, ShellCheck, ripgrep, Packer, and GPG per-user, plus BusyBox on Windows, then applies VS Code settings and portable Copilot MCP defaults. VS Code profile settings can follow you through Settings Sync; Copilot's separate global MCP config is not covered by Settings Sync. Every step auto-detects what is already present and skips it, reporting each decision in the terminal. Nothing requires WSL, sudo, or a GUI installer.
- Audit first.
setup.ps1 -Audit/setup.sh --auditreports what would change and writes nothing. - One config. Every tunable lives in
config/dev-setup.config.json. - Idempotent. Re-running is safe; values are compared before they are written.
- Color-coded. Each line is tagged
[found](green),[install](cyan),[warn](yellow), or[skip](grey) — identically across PowerShell, bash, and the Python VS Code applier. Colors are suppressed forNO_COLOR, non-TTY output, and are always on underGITHUB_ACTIONS.
Platform requirements:
- Windows: PowerShell 5.1+. WinGet for PHP, ShellCheck, ripgrep, BusyBox, and Packer.
Official portable ZIP fallbacks for Node.js and PowerShell;
uvfallback for Python. Prettier uses npm. - macOS: Xcode Command Line Tools for Git. Homebrew for Node.js, PHP, PowerShell, ShellCheck, ripgrep, Packer, and GPG. Prettier uses npm.
- Ubuntu / Debian: Homebrew is installed under
$HOMEautomatically if missing; used for Git, Node.js, PHP, PowerShell, ShellCheck, ripgrep, Packer, and GPG. Prettier uses npm.
No administrator or root access is required by the primary setup flow. WSL is never installed or invoked by setup.ps1.
- Windows WinGet installs always request user scope. Node.js and PowerShell 7 fall back to
official portable ZIPs if WinGet is missing, fails, or does not produce an executable.
ZIPs are SHA256-verified against official release metadata, staged, then installed under
%LOCALAPPDATA%\Programs; existing destination content is never overwritten. Python can fall back to user-localuv. Other unsupported packages report failure rather than retrying with machine scope or requesting elevation. WinGet uninstall is user-scoped too; portable ZIP fallbacks are deliberately left in place, not recursively removed. - macOS and Linux bootstrap Homebrew from its source archive into
$HOME/.linuxbrew. Homebrew documents this as its unsupported "untar anywhere" mode, so bottles may be unavailable and a formula may need to build from source. Setup never falls back to/opt/homebrew,/home/linuxbrew,sudo, or another system-owned prefix. - A new macOS installation still needs Xcode Command Line Tools for Git and source builds. Apple
provides no silent no-admin installation path, so run
xcode-select --installyourself when the tools are absent.
Windows — setup.cmd is the bootloader; it runs setup.ps1:
.\setup.cmdmacOS / Linux:
chmod +x setup.sh
./setup.shRun an audit first to see what would happen:
.\setup.ps1 -Audit./setup.sh --auditAudit mode installs nothing, writes no files, and does not touch git config or PATH.
Run setup from a normal, non-elevated account with a writable home directory. The Bash
entrypoint rejects non-audit runs as root; do not run it with sudo. Existing working
system-installed tools can be reused without elevation; they do not need to be reinstalled.
Setup never closes or reloads VS Code, terminates existing terminal sessions, or restarts WSL as part of its default flow. It prints manual restart guidance in the terminal after applying changes. Save work and restart only when you choose. There is no persistent "restart automatically" configuration; the optional WSL helper has a per-run override below.
setup-wsl.ps1 is a separate, opt-in helper for an existing WSL distribution that incorrectly starts as root.
It never installs WSL or a distribution. Audit first:
.\setup-wsl.ps1 -AuditTo create or configure a non-root default user without granting passwordless sudo, run:
.\setup-wsl.ps1 -Configure -UserName <linux-user>Pass -Distribution <name> to target a specific installed distribution. A non-root user with
a writable home and executable login shell is healthy; passwordless sudo is not required.
-Audit takes precedence over -Configure and never writes or restarts anything.
Configuration writes the default-user setting but leaves VS Code, terminals, and WSL running. The terminal reports restart pending; it does not claim that the new default has been verified. Save Linux work, manually terminate the selected distribution, then reopen it and run the audit again. To explicitly authorize termination and verification for one run:
.\setup-wsl.ps1 -Configure -UserName <linux-user> -Distribution <name> -RestartDistributionThis ends processes only in the named distribution, never all WSL distributions. Do not use
the override while it hosts VS Code, a remote workspace, or work you want to keep running.
Development tasks that genuinely require passwordless sudo, such as loop-device tests,
can separately opt in with -EnablePasswordlessSudo -Configure. Neither override is stored
as a default. The helper does not remove an existing sudo grant automatically.
The setup also installs the ShellCheck CLI by default, so the local Bash lint
checks used by the Blackout Secure repositories can run without a separate
tool installation. Set user.install.shellcheck to false to skip it.
ripgrep (rg) is installed by default for fast recursive repository searches.
Set user.install.ripgrep to false to skip it.
BusyBox is installed by default on Windows through WinGet (frippery.busybox-w32) to provide
its Unix utilities through one user-local executable. On macOS and Linux the setup detects an
existing busybox command but does not install or remove one because no supported user-local
package source is available. Set user.install.busybox to false to skip the check.
HashiCorp Packer is installed by default for building VM images such as the SkillForge lab
appliance OVA. Windows uses WinGet (Hashicorp.Packer); macOS and Linux use HashiCorp's own
Homebrew tap (hashicorp/tap/packer), because Packer is no longer in homebrew-core. The
hashicorp.hcl extension joins the synced VS Code baseline for .pkr.hcl syntax support.
Set user.install.packer to false to skip the CLI.
Prettier is installed by default as a user-level npm global so prettier --check .
works in any repository, including before a repository's own dependencies are installed.
The VS Code Prettier extension and default formatter setting are also part of the synced
baseline. Set user.install.prettier to false to skip the global CLI.
GPG is available by default too, so git commit/tag signing works without a separate
tool installation. Set user.install.gpg to false to skip its configuration. On Windows,
the setup uses the GPG executable bundled with user-local PortableGit, adds its directory to
the user PATH, and writes the resolved path to Git's global gpg.program; it does not install
the administrator-elevated Gpg4win package. macOS and Linux continue to install GPG through
the user-local Homebrew flow.
manage-gpg-key.ps1 / manage-gpg-key.sh are separate, opt-in helpers that generate, export, or
import a GPG key for git commit/tag signing. They're separate from setup.ps1/setup.sh because
they change real state: a new secret key in your keyring, and your global git signing config
(user.signingkey, commit.gpgsign, tag.gpgsign, and gpg.program). Nothing here runs as part of the main setup.
Audit first:
.\manage-gpg-key.ps1 -Audit./manage-gpg-key.sh --auditGenerate a new key (using user.git.userName / user.git.userEmail from config) and configure git
to sign with it:
.\manage-gpg-key.ps1 -Generate./manage-gpg-key.sh --generateBack up the configured signing key to a password-protected archive, and restore it on another machine. The archive is a plain zip/tarball encrypted with GPG's own AES-256 symmetric cipher, so "zipping it and putting it wherever" doesn't need a separate archiver:
.\manage-gpg-key.ps1 -Export C:\path\to\gpg-backup # -ZipPassword <SecureString>, or one is generated and shown once
.\manage-gpg-key.ps1 -Import C:\path\to\gpg-backup.gpg -ZipPassword <SecureString>./manage-gpg-key.sh --export /path/to/gpg-backup --zip-password '<password>' # omit to generate one, shown once
./manage-gpg-key.sh --import /path/to/gpg-backup.gpg --zip-password '<password>'-Generate/--generate and -Export/--export can be combined in one run. If a passphrase or
archive password isn't supplied, one is generated and printed once - save it, since it can't be
recovered afterward. -KeyPassphrase/--key-passphrase protects the private key itself (used by
gpg-agent when signing); -ZipPassword/--zip-password protects only the backup archive.
This kit installs machine and editor prerequisites; repositories keep their own pinned Node and Python tooling. From a repository root, use its declared check command. The common patterns in this organization are:
npm run checkpython -m pytest -qUse the repository's package.json, pyproject.toml, or contributor guide as the authority when
it differs. Do not install project linters or test runners globally. For Python repositories, create
or select the repository virtual environment first; this kit recommends .venv and configures VS
Code to discourage global package installs.
| Option | Applies to | Effect |
|---|---|---|
-Audit / --audit |
runners and every installer | Detect and report only. |
-SkipVSCodeSettings |
setup.ps1 |
Leave editor settings alone for one run. |
-GitInstallDir <path> |
setup.ps1 |
Override user.git.installDir for one run. |
-PythonVersion <x.y> |
setup.ps1 |
Override user.python.version for one run. |
-SkipUpgradeCheck |
setup.ps1 / --skip-upgrade-check |
Skip the upgrade check for one run (it runs by default). |
-CheckUpgradesOnly |
setup.ps1 / --check-upgrades-only |
Only check for newer versions; installs and applies nothing. |
-Uninstall |
setup.ps1 / --uninstall |
Remove tools this kit manages (winget/Homebrew). Combine with -Audit to preview. Git is never removed. |
Upgrade checks run by default (user.checkUpgrades, default true) alongside every audit, install, or
run-through, reporting newer versions with a new [update] tag and, where the package manager exposes
it, the installed and latest versions. They never apply upgrades automatically.
-CheckUpgradesOnly runs just that check without installing or changing anything. -Uninstall removes
Node.js, Prettier, Python, PHP, PowerShell, ShellCheck, ripgrep, and Packer via the same package manager that installed them (winget or npm
on Windows, Homebrew or npm on macOS/Linux). BusyBox is removed only on Windows, where the kit
manages it through WinGet; Unix installations are left untouched. Git is intentionally excluded since removing it would also need to
unwind the credential/identity config this kit writes.
Auditing (detect only - nothing will be installed or changed):
[found] Git git version 2.51.0 at C:\Program Files\Git\cmd\git.exe
[install] git cfg would set credential.helper, credential.credentialStore, credential.guiPrompt
[warn] identity not set; fill user.git.userName / user.git.userEmail
[found] Node.js v22.11.0 at C:\Program Files\nodejs\node.exe
[install] Python would install Python.Python.3.14 via winget
[install] vscode would set 5 setting(s) in <user-profile>\Code\User
- python.globalModuleInstallation
- dev.containers.defaultExtensions
- git.path
- python.defaultInterpreterPath
- python.terminal.activateEnvironment
[skip] validate dry-run; files were not changed
[found] VS Code User Sync GitHub Settings Sync already enabled (stable)
Audit complete. Nothing was installed or changed. Re-run without -Audit to apply.
Summary: 3 found, 2 would install, 1 skipped, 1 warning
Recommendation: 2 change(s) would be made if you run the real setup:
- git cfg: would set credential.helper, credential.credentialStore, credential.guiPrompt
- Python: would install Python.Python.3.14 via winget
Run this to apply them: .\setup.ps1
1 item(s) need attention regardless (see [warn] lines above).
The same vocabulary is used during a real run:
| Status | Meaning |
|---|---|
[found] |
Already present and correct. Nothing was done. |
[install] |
Something was changed or installed. |
[skip] |
Disabled in config, or not applicable on this platform. |
[warn] |
Continued, but the result is degraded or needs your attention. |
[update] |
A newer version is available. Reports installed/latest versions when known, but never applies automatically. |
[remove] |
Uninstalled. Only ever appears with -Uninstall / --uninstall. |
Every run ends with a one-line Summary: tally of these tags. -Audit runs add a
Recommendation: section: if anything is pending, it lists each planned change and the exact
command to apply it (.\setup.ps1 / ./setup.sh); if nothing is pending, it says so instead.
setup.cmd Windows bootloader -> setup.ps1
setup.ps1 Windows runner
setup.sh macOS / Linux runner
config/ the single config file you edit
src/ all implementation
src/scripts/ one installer per tool, each runnable on its own
.github/ repo-specific automation config, Dependabot, and CODEOWNERS
| Path | Contents |
|---|---|
| root | The entrypoints you actually type. Nothing else. |
config/dev-setup.config.json |
Single source of truth for every setting. Kept at the top level because it is the file you edit. |
src/config.ps1, src/config.sh |
Config loader, dotted-path lookup, status reporter, shared helpers. |
src/configure-vscode.py |
Cross-platform VS Code settings, extensions, and MCP applier. |
src/scripts/install-*.{ps1,sh} |
Per-tool install steps invoked by the runners. |
.github/bos-universal-config.json |
Repo-specific Blackout Secure automation config. |
.github/dependabot.yml |
Repo-specific dependency-update config. |
.github/CODEOWNERS |
Repo-specific review ownership. |
Every script resolves the repository root from its own location, so the runners and the individual installers work from any working directory.
config/dev-setup.config.json has two top-level sections, read by src/config.ps1 (PowerShell),
src/config.sh (bash), and src/configure-vscode.py (Python). A value is defined exactly once and
cannot drift between platforms.
Every lookup carries a built-in fallback. Deleting a key, leaving it "", or deleting the whole
config file degrades to the documented default rather than failing the run.
These are the supported knobs for routine use. They are safe to edit in a fork or personal copy.
| Setting | Default | Purpose |
|---|---|---|
user.install.git |
true |
Run the Git installer step. |
user.install.node |
true |
Run the Node.js/npm step. |
user.install.prettier |
true |
Run the Prettier CLI step. Uses user-level npm globals after Node.js/npm are available. |
user.install.python |
true |
Run the Python step. |
user.install.php |
true |
Run the PHP step. macOS/Linux use Homebrew; Windows uses WinGet. |
user.install.powershell |
true |
Run the PowerShell 7 step. macOS/Linux use Homebrew; Windows uses WinGet. |
user.install.shellcheck |
true |
Run the ShellCheck CLI step. macOS/Linux use Homebrew; Windows uses WinGet. |
user.install.ripgrep |
true |
Run the ripgrep (rg) CLI step. macOS/Linux use Homebrew; Windows uses WinGet. |
user.install.busybox |
true |
Install BusyBox through WinGet on Windows; detect an existing command on macOS/Linux. |
user.install.packer |
true |
Run the HashiCorp Packer CLI step. macOS/Linux use the hashicorp/tap Homebrew tap; Windows uses WinGet. |
user.install.gpg |
true |
Run the GPG step. macOS/Linux use Homebrew; Windows configures PortableGit's bundled GPG. |
user.install.vscodeSettings |
true |
Apply any VS Code settings at all. |
user.install.devcontainerDefaults |
true |
Include the Dev Containers keys and snippet. |
user.install.mcpServers |
true |
Reconcile MCP servers in mcp.json. |
user.checkUpgrades |
true |
Report newer versions ([update]) during every audit/install run, with installed/latest versions when known. -SkipUpgradeCheck / --skip-upgrade-check overrides for one run. |
user.git.installDir |
"" |
Windows PortableGit location. Empty uses advanced.git.defaultInstallDir. |
user.git.forcePortable |
false |
Install PortableGit even when a system Git is on PATH. Left false, an existing Git is detected and the download is skipped. |
user.git.userName |
"" |
Applied via git config --global user.name when non-empty. |
user.git.userEmail |
"" |
Applied via git config --global user.email when non-empty. |
user.python.version |
"3.14" |
Single source of truth for Windows winget and macOS/Linux uv. |
user.python.allowGlobalPackageInstalls |
false |
Written to VS Code as python.globalModuleInstallation; keep false to preserve the Python extension's virtual-environment warning. |
user.python.recommendedVirtualEnvironment |
".venv" |
Documented environment folder recommendation for project-level dependencies. |
user.vscode.profiles |
["stable","insiders"] |
Which VS Code profiles receive settings and extensions. |
user.vscode.settingsSync.syncAfterSetup |
true |
Request VS Code Settings Sync after successful validation when GitHub sync is already enabled. This may open VS Code. |
user.vscode.settingsSync.requiredProvider |
"github" |
Only request sync when the signed-in Settings Sync account provider matches this value. |
user.vscode.settings |
curated set, see below | Settings merged verbatim into settings.json. Wins over everything else. |
user.vscode.extensions.manage |
true |
Install/report extensions at all. |
user.vscode.extensions.install |
19 extensions | Installed if missing, via the VS Code CLI. |
user.vscode.extensions.block |
11 extensions | Never installed. Reported as [warn] if already present. |
user.vscode.extensions.uninstallBlocked |
true |
Remove blocked extensions that are installed instead of just reporting them. |
user.mcp.manage |
true |
Reconcile MCP servers at all. |
user.mcp.servers |
4 servers | Added to mcp.json if absent. Existing entries are never overwritten. |
user.mcp.inputs |
[] |
${input:id} definitions. Required by any server that references one. |
user.mcp.copilotConfig.manage |
true |
Also add portable servers to $COPILOT_HOME/mcp-config.json (or ~/.copilot/mcp-config.json). |
user.mcp.block |
[] |
Server IDs that should not be configured. Reported as [warn] if present. |
user.mcp.removeBlocked |
false |
When true, blocked servers are deleted from mcp.json instead of reported. |
user.devcontainers.dockerAccess |
"outside-of-docker" |
outside-of-docker, in-docker, or none. |
user.devcontainers.baseImage |
mcr.microsoft.com/devcontainers/base:ubuntu-24.04 |
Image used by the devcontainer snippet. |
user.devcontainers.remoteUser |
"vscode" |
remoteUser emitted by the snippet. |
user.devcontainers.features |
common-utils, git, github-cli | Features always installed. The Docker feature is added from dockerAccess. |
user.devcontainers.extensions |
Copilot, Copilot Chat, PR, Actions, GitLens, EditorConfig, ESLint, Prettier, Ruff, Python, Pylance, debugpy, Markdownlint, Code Spell Checker, Markdown All in One, YAML, ShellCheck | Extensions always installed in a container. |
user.devcontainers.settings |
copyGitConfig, gitCredentialHelperConfigLocation, cacheVolume, logLevel |
Remaining dev.containers.* keys. |
Shared, derived, or non-auto-detectable values: package identifiers, download URLs, version-pinned paths, state keys, and the setting keys the scripts own. These are public and secret-free, but they are not meant for day-to-day editing.
| Setting | Default | Purpose |
|---|---|---|
advanced.git.defaultInstallDir |
%USERPROFILE%\PortableGit |
Fallback when user.git.installDir is empty. |
advanced.git.portableReleaseApiUrl |
git-for-windows latest release API | Where the PortableGit build is discovered. |
advanced.git.portableAssetPattern |
PortableGit-*64-bit.7z.exe |
Which release asset to download. |
advanced.git.homebrewFormula |
git |
Formula used on macOS/Linux. |
advanced.git.homebrewInstallUrl |
Homebrew source archive | Archive extracted into $HOME/.linuxbrew when Homebrew is missing on macOS/Linux. |
advanced.git.linuxbrewShellenv |
$HOME/.linuxbrew/bin/brew |
User-local Homebrew executable loaded after install and persisted in .profile or .zprofile. |
advanced.git.credential.windows.helper |
manager |
Git Credential Manager. |
advanced.git.credential.windows.credentialStore |
wincredman |
Windows Credential Manager backing store. |
advanced.git.credential.windows.guiPrompt |
false |
Keeps GCM from opening blocking dialogs. |
advanced.git.credential.macos.helper |
osxkeychain |
Applied when the helper exists. |
advanced.git.credential.linux.helper |
manager |
Applied when the helper exists. |
advanced.gpg.portableGitRelativePath |
usr\bin\gpg.exe |
GPG executable within the user-local PortableGit directory on Windows. |
advanced.python.wingetPackageId |
Python.Python.{version} |
{version} is replaced by user.python.version. |
advanced.python.windowsInstallRoot |
%LOCALAPPDATA%\Programs\Python |
Where an existing interpreter is discovered. |
advanced.python.uvInstallUrl.windows |
https://astral.sh/uv/install.ps1 |
uv bootstrap, used only if winget is unavailable. |
advanced.python.uvInstallUrl.unix |
https://astral.sh/uv/install.sh |
uv bootstrap on macOS/Linux. |
advanced.python.uvWindowsPath |
%USERPROFILE%\.local\bin\uv.exe |
Where the Windows uv bootstrap lands. |
advanced.node.wingetPackageId |
OpenJS.NodeJS.LTS |
Also substituted into windowsSearchPaths. |
advanced.node.homebrewFormula |
node |
Formula used on macOS/Linux. |
advanced.node.windowsSearchPaths |
3 paths | Where node.exe is located after a winget install. {wingetPackageId} is substituted. |
advanced.prettier.npmPackage |
prettier |
Package name used for npm uninstall and upgrade checks. |
advanced.prettier.npmPackageSpec |
prettier@latest |
Package spec installed by npm. |
advanced.prettier.windowsGlobalPrefix |
%USERPROFILE%\.npm-global |
User-owned npm global prefix on Windows. |
advanced.prettier.unixGlobalPrefix |
$HOME/.npm-global |
User-owned npm global prefix on macOS/Linux. |
advanced.php.homebrewFormula |
php |
Formula used on macOS/Linux for PHP validation. |
advanced.php.wingetPackageId |
PHP.PHP |
Package used on Windows for PHP validation. |
advanced.powershell.homebrewFormula |
powershell |
Formula used on macOS/Linux for the PowerShell extension. |
advanced.powershell.wingetPackageId |
Microsoft.PowerShell |
Package used on Windows for the PowerShell extension. |
advanced.busybox.wingetPackageId |
frippery.busybox-w32 |
Package used on Windows. |
advanced.busybox.windowsSearchPaths |
WinGet Links and package folders |
Where an existing busybox.exe is discovered. |
advanced.packer.homebrewFormula |
hashicorp/tap/packer |
Tap-qualified formula used on macOS/Linux. |
advanced.packer.wingetPackageId |
Hashicorp.Packer |
Package used on Windows. |
advanced.packer.windowsSearchPaths |
WinGet Links and package folders |
Where an existing packer.exe is discovered. |
advanced.vscode.profileDirectories |
per-OS stable/insiders paths | Uses %VAR% on Windows and $HOME elsewhere. |
advanced.vscode.managedSettingKeys.gitPath |
git.path |
Setting written with the resolved Git path. |
advanced.vscode.managedSettingKeys.pythonInterpreter |
python.defaultInterpreterPath |
Setting written with the resolved interpreter. |
advanced.vscode.managedSettingKeys.phpValidator |
php.validate.executablePath |
Setting written with the resolved PHP executable. |
advanced.vscode.managedSettingKeys.powerShellAdditionalExePaths |
powershell.powerShellAdditionalExePaths |
Setting written with the resolved PowerShell executable. |
advanced.vscode.extensionCli |
stable: code, insiders: code-insiders |
CLI used to install extensions per profile. A profile is skipped when its CLI is not on PATH or a known install location. |
advanced.vscode.extensionCliFallbackPaths |
per-OS stable/insiders paths | Well-known install locations checked on macOS/Linux when the CLI isn't on PATH. |
advanced.vscode.mcpFileName |
mcp.json |
File written next to settings.json in each profile. |
advanced.vscode.settings |
azureFunctions.showProjectWarning: false |
Non-devcontainer settings the setup owns. |
advanced.devcontainers.dockerAccessFeatures |
map of 3 modes | Feature refs selected by user.devcontainers.dockerAccess. |
advanced.devcontainers.settingKeys.features |
dev.containers.defaultFeatures |
Key holding the feature map. |
advanced.devcontainers.settingKeys.extensions |
dev.containers.defaultExtensions |
Key holding the extension list. |
advanced.devcontainers.snippet.file |
jsonc.json |
Snippet file written under snippets/. |
advanced.devcontainers.snippet.name |
Dev container (Ubuntu base) |
Snippet entry name. |
advanced.devcontainers.snippet.prefix |
devcontainer |
Text typed to expand the snippet. |
For VS Code settings, later wins:
advanced.vscode.settings- dev container keys, when
user.install.devcontainerDefaultsistrue - managed Python global package install guidance
- resolved
git.path/python.defaultInterpreterPath user.vscode.settings
Command-line parameters override the config for a single run.
user.vscode.extensions.install is a lean, broadly useful baseline rather than an exhaustive list:
formatting and linting (editorconfig, prettier, eslint, ruff, markdownlint), Python
(python, pylance, debugpy), GitHub (vscode-github-actions, vscode-pull-request-github),
containers and remote (remote-containers, vscode-containers), plus powershell, yaml,
makefile-tools, code-spell-checker, markdown-all-in-one, and the Playwright Test extension
(ms-playwright.playwright) for running and debugging browser tests from VS Code.
The extension is installed into each configured VS Code profile, but Playwright itself remains a
per-project dependency. Use the extension's Test: Install Playwright command or
npm init playwright@latest in a project to create its test setup and install the selected browsers;
the setup kit does not install browser binaries globally.
Other stack-specific extensions are intentionally left out — Azure, Go, Swift, docs-authoring, and similar belong to individual workflows, not a shared baseline. Playwright is the exception because its extension adds editor support while leaving test dependencies and browser installs project-scoped. Add whatever else you need to the list.
user.vscode.extensions.block covers extensions that are deprecated or superseded by something
in the install list, so having both causes duplicate diagnostics or fighting formatters:
| Blocked | Superseded by |
|---|---|
ms-python.autopep8, black-formatter, flake8, isort, pylint |
charliermarsh.ruff |
ms-azuretools.vscode-docker |
ms-azuretools.vscode-containers |
ms-vscode.vscode-typescript-tslint-plugin |
dbaeumer.vscode-eslint (TSLint is deprecated) |
hookyqr.beautify |
esbenp.prettier-vscode (unmaintained) |
ms-vscode.powershell-preview |
ms-vscode.powershell |
eg2.vscode-npm-script, ms-vscode.node-debug2 |
Built into VS Code |
Blocked extensions are removed by default because uninstallBlocked is true; set it to false
to report them without removing them. An extension listed in both install and block is a
configuration error and fails the run.
Extension management needs the VS Code CLI (code) on PATH. Profiles whose CLI is missing are
reported as [skip] and everything else still runs.
user.mcp.servers is merged into each profile's mcp.json and into the Copilot user-level
mcp-config.json. The latter uses the directory in COPILOT_HOME, or ~/.copilot when that
variable is unset. The shipped default is four servers that need no authentication and no local
runtime beyond npx/uvx:
| Server | Transport | Why it is a default |
|---|---|---|
io.github.github/github-mcp-server |
http | Repos, issues, PRs. Useful in any repository. |
microsoftdocs/mcp |
http | Microsoft/Azure documentation lookup. No auth. |
microsoft/markitdown |
stdio (uvx) |
Converts PDFs/Office docs to Markdown. Language-agnostic. |
microsoft/playwright-mcp |
stdio (npx) |
Browser automation for front-end work. |
Servers that are not shipped as defaults, and why:
| Not included | Reason |
|---|---|
| Azure, Azure DevOps, NuGet, Terraform | Require a subscription, an organization name, a token, or a .NET/Terraform stack. Useful, but not universal. |
| Enterprise and Sentinel endpoints | Tenant-entitled URLs. They fail for anyone without the entitlement. |
| Next.js devtools, awesome-copilot | Framework-specific, or require a local Docker daemon. |
Add any of these to user.mcp.servers yourself — the merge handles them the same way.
Existing entries are never overwritten. Only missing servers are added, so a locally pinned
version or an added env block survives a re-run. Remove a server from mcp.json and it comes back
on the next run; add it to user.mcp.block to stop that.
Never hardcode secrets. Use VS Code's ${input:id} placeholders with matching entries in
user.mcp.inputs and mark secret prompts with "password": true. VS Code prompts for those values
locally instead of storing them in this repository.
A server that references an ${input:id} with no matching definition fails the run, as does a
server listed in both servers and block.
The Copilot config uses the portable mcpServers format and adds tools: ["*"] when a server
does not define a tool list. Existing entries are never overwritten. Servers that use VS Code-only
fields or variables (including ${input:id} secrets) are skipped for this destination and reported;
they remain available through the VS Code profile config. This is deliberate: the Copilot format
cannot prompt for VS Code input variables, and the kit will not copy resolved secrets into another
file.
VS Code Settings Sync does not sync $COPILOT_HOME/mcp-config.json. The setup writes the
portable config locally for the Copilot harness and Copilot CLI; it does not create a GitHub repo,
push user data, or store credentials. If you want this user-level config on other machines, use a
private repository or another secure sync method that you manage, and keep secret values in
environment variables rather than the tracked file.
user.vscode.settings holds portable editor, Git, terminal, and security preferences. Some
categories are intentionally not shipped here:
| Excluded | Why |
|---|---|
git.path, python.defaultInterpreterPath, php.validate.executablePath, powershell.powerShellAdditionalExePaths |
Machine-local; resolved and written by the installers. The PHP and PowerShell path settings are ignored by Settings Sync. |
azureResourceGroups.selectedSubscriptions, @azure.argTenant, chat.mcp.serverSampling |
Contain tenant/subscription identifiers. Never commit these to a shared repo. |
yaml.schemas |
Absolute paths containing a username. Already in settingsSync.ignoredSettings. |
terminal.integrated.cwd |
A personal folder convention. |
chat.tools.global.autoApprove, chat.agent.sandbox.*, chat.agent.networkFilter, chat.tools.*.autoApprove |
Agent security posture. Auto-approving tool calls and disabling the sandbox is a personal risk decision and must not be a shared default. Set them yourself if you want them. |
Other chat.* / github.copilot.* tuning |
Fast-moving setting names and highly personal. Add to user.vscode.settings if you want them synced. |
Anything in this list can still be added to user.vscode.settings on your own machine — the config
is yours to extend.
Config values may contain environment placeholders, expanded at read time:
| Form | Platform | Example |
|---|---|---|
%VAR% |
Windows | %APPDATA%\Code\User |
$HOME |
macOS / Linux | $HOME/.config/Code/User |
{version} |
any | Python.Python.{version} |
{wingetPackageId} |
any | ...\WinGet\Packages\{wingetPackageId}*\... |
| Name | Kind | Purpose |
|---|---|---|
$DevSetupRoot |
variable | Repository root, resolved from the file's own location. |
$DevSetupConfigPath |
variable | Default config path under config/. |
Get-DevSetupConfig [-Path] |
function | Load and parse the config; warns and returns empty if absent. |
Get-DevSetupValue $cfg <dotted.key> [$default] |
function | Safe lookup. null and "" fall back to $default. |
Expand-DevSetupPath <value> |
function | Expand %VAR% placeholders. |
ConvertTo-DevSetupHashtable <object> |
function | PSCustomObject to hashtable for iteration. |
Write-DevSetupStatus <state> <component> [detail] |
function | Emit one [found]/[install]/[skip]/[warn] line. |
Get-DevSetupVSCodeProfilePath $cfg [-Platform] |
function | Existing settings.json paths for configured profiles. |
Set-DevSetupVSCodeSetting $cfg <key> <value> |
function | Write one setting to every profile, skipping no-ops. |
Add-DevSetupUserPath <dir> [-Prepend] |
function | Add a directory to the user PATH once, comparing whole entries. |
Get-DevSetupGitCredentialSetting $cfg |
function | The git config keys/values this setup owns on Windows. |
Write-DevSetupGitIdentityStatus $cfg <git> [-Audit] |
function | Identity reporting shared by audit and apply. |
. (Join-Path $PSScriptRoot "..\config.ps1")
$config = Get-DevSetupConfig
$version = Get-DevSetupValue $config "user.python.version" "3.14"| Name | Kind | Purpose |
|---|---|---|
DEVSETUP_ROOT |
variable | Repository root, resolved from the file's own location. |
DEVSETUP_CONFIG_FILE |
variable | Config path. Override by exporting it before sourcing. |
DEVSETUP_READER |
variable | The JSON reader (python3, python, or jq) resolved once at source time. |
devsetup_config <dotted.key> [default] |
function | Safe lookup. Arrays print newline-separated. |
devsetup_enabled <dotted.key> [default] |
function | True when the value is true. |
devsetup_status <state> <component> [detail] |
function | Emit one status line. |
devsetup_git_identity_status [audit] |
function | Identity reporting shared by audit and apply. |
devsetup_config_reader |
function | Prints DEVSETUP_READER; fails if none was found. |
. "$(dirname "$0")/../config.sh"
version="$(devsetup_config user.python.version 3.14)"The config must be readable before Python is installed, so the reader is chosen from python3,
python, then jq, and every lookup falls back to the caller-supplied default if none exist.
Resolution happens at source time rather than per lookup: devsetup_config runs inside a $( )
subshell, so a value cached during a lookup would be discarded.
Each installer runs standalone and accepts -Audit / --audit.
.\src\scripts\install-portable-git.ps1 [-ConfigureVSCode] [-ForcePortable] [-Audit]./src/scripts/install-portable-git.sh [--audit]- Windows downloads the latest Git for Windows portable build
to the configured directory and adds it to the user
PATH. An existing Git onPATHis detected and the download skipped unlessuser.git.forcePortableistrue. - macOS reports and exits if Git and Xcode Command Line Tools are both missing; Apple offers no
silent install path, so run
xcode-select --installonce yourself. - Linux uses an existing Git or Homebrew, otherwise installs
Homebrew for Linux under
$HOME(no root) and installs Git through it. - Configures the credential helper and applies
user.git.userName/user.git.userEmailwhen set. Each value is compared first, so a second run reports[found]. -ConfigureVSCodepointsgit.pathat the resolved binary across every configured profile.
.\src\scripts\install-python.ps1 [-ConfigureVSCode] [-PythonVersion <x.y>] [-Audit]./src/scripts/install-python.sh [--print-path] [--audit]Checks for a working interpreter first. On Windows it installs per-user via winget when
available — winget packages go through a trusted pipeline and are not blocked by Defender
Application Control / Exploit Guard ASR policies common on managed corporate devices, whereas
executing a freshly downloaded uv.exe can be. If winget is unavailable, both platforms fall back
to uv in a user-local location.
The version comes from user.python.version on every platform. The PowerShell script emits the
resolved interpreter path as its final pipeline value; the shell script does the same under
--print-path, with progress on stderr.
.\src\scripts\install-node.ps1 [-Audit]./src/scripts/install-node.sh [--audit]Windows installs Node.js LTS per-user through winget; macOS/Linux use Homebrew. If PowerShell
execution policy blocks npm.ps1, use npm.cmd install / npm.cmd test — the same executable,
without changing machine security policy.
./src/scripts/install-php.sh [--print-path] [--audit]macOS/Linux use Homebrew and Windows uses WinGet to install PHP when it is missing. The resolved executable is written
to VS Code's php.validate.executablePath, which restores built-in PHP validation. That absolute
path is machine-local and explicitly excluded from Settings Sync.
.\src\scripts\install-powershell.ps1 [-Audit]./src/scripts/install-powershell.sh [--print-path] [--audit]macOS/Linux use Homebrew and Windows uses WinGet to install PowerShell 7 when it is missing. The
resolved pwsh path is written to powershell.powerShellAdditionalExePaths, restoring discovery
for the VS Code PowerShell extension. The path is machine-local and excluded from Settings Sync.
python .\src\configure-vscode.py [--dry-run] [--config <path>] [--git-path <p>] [--python-path <p>] [--php-path <p>] [--powershell-path <p>]python3 ./src/configure-vscode.py [--dry-run]One cross-platform script, so the settings payload has a single definition. It writes these files per configured profile, all covered by Settings Sync:
settings.json— the merged settings described in Precedence.snippets/jsonc.json— adevcontainersnippet. Typedevcontainerin a new.devcontainer/devcontainer.json.mcp.json— the configured MCP servers.prompts/terminal-execution.instructions.md— personal terminal and process guidance. Existing user-authored instructions are preserved and reported for manual comparison. Disable installation withuser.vscode.terminalInstructions.manage: false.
The baseline selects Agent, Copilot harness, Auto, the Standard (balance) tier, and queued
follow-up messages. Dictation and voice mode (including their buttons) are disabled.
Terminal output is shown in the Terminal panel where the harness supports it, shell integration
is enabled, and persistent process revival is disabled. Existing terminals are not closed.
These chat settings target recent VS Code/Copilot builds; preview features and model access depend on the installed version, account, and organization policy. The Windows Copilot SDK harness does not support the Agent Host integrated-terminal override in the currently reviewed build. Terminal settings cannot force SDK shell output into the main panel there; select the Local harness when integrated-terminal interaction is more important than the Copilot harness. Instructions require the agent to disclose this limitation.
Autopilot/Allow All and Fusion are explicit opt-ins for a personal copy of the config:
{
"user": {
"vscode": {
"copilot": {
"autopilot": true,
"fusion": true
}
}
}
}Merge those keys into the existing config; do not replace the file with this fragment.
Both default to false so the kit does not grant unattended permissions or enable
experimental model routing for every user. Fusion presents Auto as Fusion; balance is
its Standard tier. Changing Fusion settings may restart the Copilot runtime, not the
VS Code application. Settings under user.vscode.settings still win over these presets.
Autopilot/Allow All grants broad tool access; it is not permission to restart the editor
or terminate a user's terminals. New-conversation defaults do not rewrite a running chat.
The instruction file sets finite-command budgets (normally 120 seconds, or 600 for builds), foreground execution, bounded readiness checks for servers/watchers/debuggers, and cleanup of owned temporary helpers. Instructions guide the agent; they are not a guaranteed process watchdog. VS Code's 60-second silence threshold returns control without killing a process. The applier separately enforces CLI deadlines: 120 seconds for queries/sync and 600 seconds for extension changes, whose output remains visible. A CLI timeout is an error, not success.
It then reconciles extensions through the VS Code CLI: installs anything missing from
user.vscode.extensions.install, and reports (or removes) anything present from .block. See
Extensions.
Finally it merges user.mcp.servers into each profile's mcp.json and adds compatible entries
to the Copilot user-level MCP config. See MCP servers.
After writes finish, it validates the managed settings, snippet, extension list, and MCP entries by
rereading the files and VS Code CLI output. If validation passes, user.vscode.settingsSync allows
one final guarded sync request: the script checks VS Code's global user-data state for
sync.enable=true and the configured account provider, then runs the configured VS Code CLI sync
arguments. It skips sync when Settings Sync is off, the provider is not GitHub, or the CLI is absent.
The final request is enabled by default, including when the config key is omitted.
It also requires a nonempty cached Settings Sync account reference and a supported provider
(github or microsoft); requiredProvider defaults to github. To use an existing
Microsoft Settings Sync account, explicitly set requiredProvider to microsoft.
Missing/unreadable state, no cached account, or a provider mismatch produces an explicit
skip (and an unreadable database produces a warning). The database is opened read-only and
closed promptly. A cached account is not proof of a current OAuth session: VS Code itself
validates authentication and may pause uploads until the user signs in again.
To opt out, set user.vscode.settingsSync.syncAfterSetup to false, or skip just one run:
.\setup.ps1 -SkipSettingsSync
python .\src\configure-vscode.py --skip-settings-sync./setup.sh --skip-settings-syncThese options skip only the kit's final request; they do not disable VS Code's ongoing automatic sync or change Copilot session-sync preferences. Audit never makes a sync request. All settings, snippets, instructions, extensions, and MCP changes and their validation finish before the final request. Validation failure prevents that request. There is no blanket guarantee that every local file is cloud-synced: VS Code resource exclusions and policies apply, and the separate Copilot MCP file, tool installations, and credentials remain local.
An accepted --sync on request only confirms that sync was enabled, not that a cloud upload
finished. Check Settings Sync: Show Synced Data in VS Code for the remote settings and
instruction files. The script never edits the sync database, handles OAuth tokens, or
changes your public GitHub profile/bio.
Session history sync is separate from VS Code Settings Sync. The baseline enables
chat.sessionSync.enabled and its required dependency,
github.copilot.chat.localIndex.enabled. This allows Copilot session data to sync to the
signed-in GitHub.com account for cross-device history. It requires a supported Copilot
sign-in and can be blocked by organization policy; setup does not sign in, override policy,
or claim that all existing session history has finished uploading.
To disable session-history uploads, set
user.vscode.settings["chat.sessionSync.enabled"] to false. Session sync may upload
conversation content; review this privacy choice before using the baseline in sensitive
workspaces. The preferences themselves can roam through VS Code Settings Sync, but session
data is uploaded independently by Copilot. A successful settings upload is not proof of a
completed session-history upload.
The merge is additive and idempotent: existing settings and snippets are preserved, and JSONC
comments and trailing commas are tolerated on read. The file is rewritten as plain JSON, so comments
in settings.json are not preserved.
Settings Sync itself must still be enabled once from VS Code's Accounts menu — OAuth cannot be scripted.
Binaries, PATH entries, credentials, and git identity stay machine-local and are never uploaded.
advanced.node.portableBaseUrl / portableInstallDir and
advanced.powershell.portableReleaseApiUrl / portableInstallDir control the Windows ZIP
fallbacks. Keep sources official and destinations under the user's profile. Node chooses
the current LTS, not an obsolete release to accommodate an unsupported architecture;
PowerShell requires the release asset's SHA256 digest. Downloads have finite timeouts and
checksum failures leave no installation behind. These fallbacks do not enable automatic
runtime upgrades or change the existing opt-in uninstall behavior.
The repository is public and keeps only portable defaults in source control.
| Area | Current posture |
|---|---|
| Community defaults | Funding, issue templates, and PR templates are inherited from the org-level .github repo. |
| Org automation config | .github/bos-universal-config.json opts into common sync/lint settings only. |
| CODEOWNERS | .github/CODEOWNERS requests security review for every PR. |
| Issues | Enabled for support and bug reports. |
| Projects and Discussions | Disabled; this kit is small enough to keep planning in issues and PRs. |
| Wiki | Disabled; public docs live in README.md, SECURITY.md, and CONTRIBUTING.md. |
| Branch cleanup | Delete branch on merge is enabled in GitHub repository settings. |
| Secrets | No credentials, tokens, tenant IDs, subscription IDs, or private endpoints belong in this repo. |
| Workflows | No action/release workflow is copied because this repo has no action.yml or package manifest. |
Dev containers stay opt-in. Nothing here creates a .devcontainer folder or puts a workspace
into a container; VS Code only containerizes a repo that already has a .devcontainer/devcontainer.json.
These are the defaults applied if a container is built.
The default user.devcontainers.dockerAccess is outside-of-docker, which bind-mounts the host
Docker socket. The container shares the host image cache (no double-pull) and needs no --privileged
flag. Two consequences:
- Paths in nested
docker run -vcalls resolve against the host filesystem, not the container's. Use${localWorkspaceFolder}rather than$(pwd). This is the usual cause of confusing failures withactand nested Compose. - Socket access is root-equivalent on the host. Fine on a personal machine; not appropriate on shared or untrusted infrastructure.
Set dockerAccess to in-docker for true daemon isolation, accepting --privileged and a cold
image cache, or none to add no Docker feature at all.
Fill user.git.userName and user.git.userEmail in the config and the setup applies them.
Use a GitHub noreply address if you do not want to expose a personal email address.
The scripts update the user-level PATH, which existing shells do not inherit. Open a new
terminal, or reload VS Code.
If Docker Desktop shows a Filesharing dialog every time you run docker compose/docker run
for a project on a drive other than C:, this is Docker Desktop/Windows behavior, not something
VS Code or Settings Sync controls:
- With the Hyper-V backend, Settings → Resources → File sharing pre-authorizes a directory once.
- With the WSL 2 backend (the default), that persistent allow-list does not exist for drives WSL does not auto-mount, so Docker re-prompts per session by design. Updating Docker Desktop does not fix it.
Options: keep the project on C:, work inside the WSL distro's own filesystem and run Compose from
within WSL, switch to the Hyper-V backend, or click Yes each session.
Use npm.cmd instead of npm. Same executable, no security policy change.