Workstation Setup tutorial series — Part 2: Set up the basics
WSL2, Docker Desktop, VS Code with the right extensions, Node, PHP, Codeberg auth (SSH key + tea CLI) plus the GitHub CLI, the OpenSpec CLI, and the Playwright browser. The longest part of the series — most of the time goes into downloads while you do something else. After this, you have a working dev workstation, just without Claude Code wired up yet.
This is the part with the most install commands. WSL2, Docker Desktop, VS Code with the right extensions, the language runtimes (Node, PHP, Composer), the CLIs (gh, OpenSpec), and the Playwright Chromium binary. By the end of Part 2 you'll have a working dev workstation — just without Claude Code wired up yet. That's Part 3.
A note on macOS and Linux
This part assumes Windows 11. If you're on macOS or Linux:
- macOS — skip step 1 (WSL2) entirely. Replace
apt installwithbrew installin the rest. Install Docker Desktop natively. The order and the verification commands stay the same. - Linux native — skip step 1 (WSL2). Install Docker Engine natively (not Docker Desktop). The order stays the same.
Our canonical workstation-setup.md documents the Windows + WSL2 path; the macOS and Linux variants above are the translation most teams use. If you hit something that doesn't translate cleanly, send a note via Email us at the bottom.
Step 1: install WSL2 (Windows only)
Open PowerShell as Administrator — right-click the Start button, pick "Terminal (Admin)" or "PowerShell (Admin)" — then:
wsl --install -d Ubuntu-24.04
Restart your machine when prompted. After the reboot, Ubuntu finishes its first-time setup and asks you to create a Linux username and password. Pick a username you'll remember; the password is the sudo password for your Ubuntu install.
Verify in PowerShell:
wsl --version
wsl -l -v
You should see Ubuntu-24.04 running on WSL version 2.
No reboot? Restart Ubuntu via
wsl --shutdownin PowerShell, then re-open the Ubuntu app. That clears nine out of ten "it just won't start" issues.
Step 2: install Docker Desktop
Download Docker Desktop for Windows from docker.com/products/docker-desktop and install it. After installation:
- Open Docker Desktop > Settings > Resources > WSL Integration.
- Enable integration with your Ubuntu-24.04 distro.
- Click Apply & Restart.
Open your Ubuntu terminal (Start menu → Ubuntu) and verify:
docker --version
docker compose version
Both should return a version string. If docker --version reports "command not found" inside WSL, the WSL integration didn't apply — quit Docker Desktop fully (right-click tray icon → Quit), then start it again.
Step 3: install VS Code
Download Visual Studio Code from code.visualstudio.com and install it on Windows (not inside WSL).
Then connect it to WSL:
- Open VS Code.
Ctrl+Shift+Xto open Extensions, install WSL (ms-vscode-remote.remote-wsl) on the Windows side. This one really must be on Windows, not WSL.Ctrl+Shift+P> "WSL: Connect to WSL".- Pick Ubuntu-24.04.
VS Code installs its server component inside WSL automatically. From now on, every VS Code terminal lives inside WSL.
Step 4: install VS Code extensions
Inside the WSL-connected VS Code window, install these. Easiest is from the WSL terminal:
code --install-extension anthropic.claude-code
code --install-extension ms-azuretools.vscode-docker
code --install-extension bmewburn.vscode-intelephense
code --install-extension vue.volar
code --install-extension dbaeumer.vscode-eslint
code --install-extension ms-python.python
code --install-extension eamodio.gitlens
code --install-extension redhat.vscode-yaml
code --install-extension github.vscode-github-actions
That covers the required + recommended set for a Conduction project. See workstation-setup.md for the optional ones.
Step 5: install Node (via nvm)
All Node work happens inside WSL, never on the Windows side. Use nvm so you can switch versions per project without sudo:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 20
nvm alias default 20
Verify:
node --version # v20.x
npm --version # 10.x
Why Node 20, not the latest? Docusaurus 3.x and the OpenSpec CLI both want Node 20+, and our canonical workstation-setup.md installs Node 20 specifically. Stick to 20 LTS until you have a reason not to.
Step 6: install PHP 8.1+ and Composer
sudo apt update
sudo apt install -y php8.1-cli php8.1-xml php8.1-mbstring php8.1-curl php8.1-zip unzip
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
Verify:
php --version # 8.1+
composer --version # 2.x
Use phpcs v3, not v4, when a project asks for it. The CI is pinned to v3 and v4 breaks the shared
phpcs.xmlconfig. If acomposer installlater tries to upgrade, override withcomposer require --dev "squizlabs/php_codesniffer:^3.9".
Step 7: Codeberg auth (SSH + tea)
Conduction migrated from github.com/ConductionNL to codeberg.org/Conduction in May 2026 — Codeberg is the primary git host now, GitHub stays as a secondary mirror. This step gets two things working: SSH (for git clone/push/pull) and the tea CLI (for PRs, issues, labels via Codeberg's REST API). Both stick around across reboots once configured.
This is the condensed walkthrough. The full version, with every troubleshooting case and a deeper rationale, lives in Codeberg Authentication Setup in the .github repo. Read that if anything below misbehaves.
7a. Generate an SSH key and add it to Codeberg
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_codeberg
Enter a passphrase when prompted — keychain (next step) makes the day-to-day cost almost zero, and a passphraseless key on disk is a stand-alone credential to anyone who reads the file.
Copy the public key (the .pub file) to Codeberg:
cat ~/.ssh/id_ed25519_codeberg.pub
# or pipe directly to the Windows clipboard from WSL:
cat ~/.ssh/id_ed25519_codeberg.pub | clip.exe
Go to codeberg.org/user/settings/keys → Add Key, paste the whole line, give it a recognisable name (e.g. WSL Ubuntu — <laptop>), click Add Key.
7b. Point SSH at the right key for Codeberg
cat >> ~/.ssh/config <<'EOF'
Host codeberg.org
HostName codeberg.org
User git
IdentityFile ~/.ssh/id_ed25519_codeberg
IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
chmod 600 is required — SSH silently ignores a config file with looser permissions.
Verify:
ssh -T [email protected]
# Hi there, <YourCodebergUsername>! You've successfully authenticated...
7c. Keep the key loaded across shells with keychain
Without keychain, every git push asks for the passphrase. With it, you enter the passphrase once per WSL boot and every subsequent shell (including Claude Code's Bash tool) inherits the unlocked agent.
sudo apt install -y keychain
cat >> ~/.bashrc <<'EOF'
# Keep an ssh-agent alive across shells, with the Codeberg key loaded
eval $(keychain --eval --quiet --agents ssh ~/.ssh/id_ed25519_codeberg)
EOF
Open a new terminal — keychain prompts for the passphrase once. After that:
ssh-add -l
# 256 SHA256:... [email protected] (ED25519)
7d. Install the tea CLI for PRs/issues/labels
SSH covers the git protocol. Everything else (creating a PR, listing issues, applying labels) goes through Codeberg's REST API. tea is Gitea's official CLI — think of it as gh for Codeberg.
sudo wget -O /usr/local/bin/tea https://dl.gitea.com/tea/0.11.0/tea-0.11.0-linux-amd64
sudo chmod +x /usr/local/bin/tea
tea --version
7e. Create a Codeberg API token and tell tea about it
Go to codeberg.org/user/settings/applications → Generate New Token with these scopes (tick write where applicable): repository, issue, user, organization. Set a 90-day expiry; rotate quarterly. Copy the token immediately — Codeberg shows it once.
tea login add --name codeberg --url https://codeberg.org --token <paste-token-here>
tea login list
# NAME=codeberg URL=https://codeberg.org USER=<YourCodebergUsername>
The token sits in ~/.config/tea/config.yml from then on; every tea invocation reuses it.
7f. Install the GitHub CLI (secondary, still useful)
A handful of repos still live on GitHub and Hydra's cron scripts haven't migrated yet, so gh stays in the toolbelt:
sudo apt install -y gh
gh auth login # GitHub.com → HTTPS → Login with a web browser
If your work GitHub uses SSO, make sure your default browser is Edge or Chrome — Firefox SSO callbacks sometimes don't complete cleanly.
Verify all three:
ssh -T [email protected] # → "Hi there, <YourCodebergUsername>!"
tea login list # → row with NAME=codeberg
gh auth status # → green checkmark + your GitHub username
Step 8: install the OpenSpec CLI
Used by all /opsx-* skills for spec-driven development:
npm install -g @fission-ai/openspec
openspec --version
Do NOT run
openspec initinside a Conduction project — those projects already have a customisedopenspec/directory. Runninginitoverwrites them.
Step 9: install the Playwright Chromium binary
The Playwright MCP browsers (used by our testing skills) need Chromium installed once on this machine:
npx playwright install chromium
The download is ~300 MB. Run it on a stable connection — not on the train.
Step 10: verify everything
A clean dev workstation passes this checklist with no errors:
node --version # v20.x
npm --version # 10.x
php --version # 8.1+
composer --version # 2.x
docker --version # 24+
docker compose version # 2.x
ssh -T [email protected] # "Hi there, <YourCodebergUsername>!"
tea --version # 0.11+
tea login list # row with NAME=codeberg
gh --version # 2.x
gh auth status # green checkmark + your GitHub username
openspec --version # 1.x
npx playwright --version # 1.x
If every command returns a real version, ssh -T greets you by your Codeberg username, tea login list shows the codeberg row, and gh auth status is green, you're done with Part 2. Otherwise, jump to Troubleshooting below.
Troubleshooting
WSL2 won't install: 'WSL is not supported on this version of Windows'You're probably on Windows 10 without the latest cumulative update, or on Home edition without virtualisation enabled. Update Windows, then enable Virtual Machine Platform and Windows Subsystem for Linux via Turn Windows features on or off. Reboot and retry.
docker command not found inside WSL after Docker Desktop installThe WSL integration didn't take. Open Docker Desktop > Settings > Resources > WSL Integration, toggle Ubuntu-24.04 off and on again, click Apply & Restart. Then re-open the Ubuntu terminal.
composer install fails with 'vendor/ is owned by root'Happens after the first Docker run inside a project — Docker created vendor/ as root. Two fixes: (1) sudo chown -R $USER:$USER vendor/ and rerun, or (2) install phpcs globally with composer global require "squizlabs/php_codesniffer:^3.9" and skip composer install for now.
gh auth login opens the wrong browser and SSO never completesIf your work GitHub uses SSO and Firefox is the default browser, the callback sometimes doesn't come back cleanly. Set Edge or Chrome as the default browser and rerun gh auth login --web.
ssh -T [email protected] returns 'Permission denied (publickey)'Either the public key wasn't pasted into Codeberg, or ~/.ssh/config doesn't have chmod 600. Recopy ~/.ssh/id_ed25519_codeberg.pub to codeberg.org/user/settings/keys and run chmod 600 ~/.ssh/config. Then retry.
Every git push asks for the SSH passphrasekeychain isn't installed, or the eval $(keychain ...) line isn't in ~/.bashrc. Open a fresh terminal after editing ~/.bashrc — keychain only runs on shell start. Verify with ssh-add -l; you should see the Codeberg key listed.
tea pulls create returns 401 UnauthorizedThe token in ~/.config/tea/config.yml doesn't have write:repository or write:issue. Regenerate the token at codeberg.org/user/settings/applications with the right scopes (see Step 7e) and re-run tea login add.
npx playwright install chromium hangs or times outThe download is ~300 MB. On a slow connection it can look stuck. Wait it out, or rerun on a faster network. If it really refuses, set HTTPS_PROXY and try again from a network where the proxy resolves.
Test yourself
Five short questions to check that this part landed. Stuck? Click Hint. Curious about the answer? Click Answer.
1. Why do we install Node, PHP and the rest inside WSL instead of on the Windows side?
Hint
Think about which file system the tooling reads and writes, and which shell scripts later steps will run.
Answer
Because every later step — Composer, the OpenSpec CLI, the gh CLI, the global Claude safety hooks (Part 3) — assumes a POSIX shell and Linux file paths.
Installing Node or PHP on Windows leads to two trips through path translation (\\wsl$\... ↔ C:\Users\...), inconsistent line endings, and bash hooks that just don't run. The rule of thumb: anything you'll run from a shell goes inside WSL; editor and Docker Desktop run on Windows.
2. The Docker command works in Windows PowerShell but not inside WSL. What's the most likely cause?
Hint
Docker Desktop runs on Windows. It needs an explicit step to be reachable from WSL.
Answer
The WSL Integration wasn't enabled (or didn't apply) for your Ubuntu distro. Open Docker Desktop > Settings > Resources > WSL Integration, toggle your Ubuntu distro off and on, click Apply & Restart. Then re-open the Ubuntu terminal.
If that still doesn't help, quit Docker Desktop fully (right-click tray icon → Quit) and start it again. That clears the last 10% of cases.
3. Why do we install the WSL extension for VS Code on the Windows side, and the rest on the WSL side?
Hint
The WSL extension does one specific thing the others don't.
Answer
The WSL extension is what connects VS Code on Windows to a remote VS Code Server inside WSL — that step has to live on Windows. Once that connection is up, the other extensions (Claude Code, Volar, ESLint, Intelephense, Docker) need access to your code, your node_modules, and your shell — so they install on the WSL side, against the same file system as your project.
Installing language extensions on Windows when the code lives in WSL gives you broken IntelliSense, slow file watchers, and confusing path errors. Keep the rule simple: WSL extension on Windows, everything else inside WSL.
4. Your git push to a Codeberg repo hangs silently for two minutes, then errors out with "Permission denied (publickey)". What are the two most likely causes, and how do you tell them apart?
Hint
One is about whether the key is known to Codeberg. The other is about whether the key is unlocked on your machine. Two separate failure modes; two separate checks.
Answer
The two failure modes are:
- The public key isn't paired with your Codeberg account. Diagnose:
cat ~/.ssh/id_ed25519_codeberg.puband compare against the keys listed at codeberg.org/user/settings/keys. If it's missing, paste it in. - The key is on Codeberg, but no ssh-agent has it loaded on this machine. Diagnose:
ssh-add -l. If the output is "The agent has no identities",keychaineither isn't installed or isn't running in this shell. Runkeychain ~/.ssh/id_ed25519_codebergonce, then open a fresh terminal.
In either case, after fixing, ssh -T [email protected] should reply "Hi there, <your username>!" — the surest single confirmation that auth works.
5. After the first docker compose up -d of a Conduction project, composer install on the host suddenly fails with Permission denied on vendor/. What happened, why is it specifically the first Docker run, and what's the fastest fix?
Hint
Docker containers run as root by default. When a container writes into a bind-mounted directory, the files inherit that ownership on the host.
Answer
When Docker first starts a Conduction project, the PHP container writes into the bind-mounted vendor/ directory — but the container runs as root, so the resulting files are owned by root:root on the host file system. Your host user ($USER) can't write to them, and composer install from the host fails.
Why specifically the first run: before that, vendor/ either didn't exist or was clean. Subsequent runs reuse the already-root-owned files, so the error persists until you fix the ownership.
Fastest fix:
sudo chown -R $USER:$USER vendor/
composer install
Alternative if you don't want to use sudo: install phpcs globally (composer global require "squizlabs/php_codesniffer:^3.9") and skip the local composer install for the moment. The CI still uses the project's composer.json, so this only affects local tooling.
Next step
With the runtimes installed, it's time to wire up Claude Code itself — and, just as importantly, the global safety hooks.