Scripts to set up a new GitHub Codespace with a full, opinionated development environment.
- Manual steps (do these yourself)
- Logs
- Getting started
- What it installs
- Structure
- Customisation
- Development
- GitHub CLI authentication
- Neovim configuration
- Related repositories
If you need to run the script manually:
git clone https://github.com/johnmathews/codespaces-setup.git ~/codespaces-setup
cd ~/codespaces-setup
bash setup.sh
exec zshThe script is idempotent – safe to run multiple times.
setup.sh automates everything it can, but two things cannot be scripted and must be done by hand in every
Codespace (any repo, not just this one). Do these and the environment is complete.
The container installs MesloLGS NF (the Nerd Font that gives Powerlevel10k its icons/glyphs), but a browser-based Codespace renders the VS Code terminal on your local machine, so the in-container font does nothing there. You must install the font locally and point VS Code at it:
-
Install the font on your local machine — download all four MesloLGS NF variants and install them: https://github.com/romkatv/powerlevel10k#meslo-nerd-font-patched-for-powerlevel10k
-
Tell VS Code to use it — open the Settings UI (
Cmd/Ctrl+,), search forterminal.integrated.fontFamily, and set it to:"MesloLGS NF"(Or add
"terminal.integrated.fontFamily": "MesloLGS NF"tosettings.json.)
This is a per-machine VS Code setting, so you only do it once per local machine, not once per Codespace. Without it,
the prompt shows tofu boxes (□) instead of icons. The in-container install (scripts/14-fonts.sh) is only used when you
access the Codespace over SSH. See also Neovim configuration.
Codespaces auto-injects a GITHUB_TOKEN that is scoped to the repo the Codespace was created from, and gh/git
prefer it — so you can't push to other repos until you log in as yourself. configs/.zshrc already runs
unset GITHUB_TOKEN GH_TOKEN for interactive shells; you just need to authenticate once per Codespace:
gh auth login # GitHub.com → HTTPS → paste a PAT or use the web flowAfter that, git push/pull work against any repo (via the gh credential helper wired up in
configs/.gitconfig_managed). Full details and the "give me the restricted token back" escape hatch are in
GitHub CLI authentication.
setup.sh mirrors all of its output to ~/.cache/codespaces-setup.log as well as the screen, so you can follow progress
from any other shell — or after the fact — even when it runs in the background (e.g. as the Codespaces
postCreateCommand):
tail -f ~/.cache/codespaces-setup.logThe Neovim plugin pre-load runs in the background and logs separately:
tail -f ~/.cache/nvim-setup.logIf a step fails, the error line names the failing step and script, e.g.
[setup] ERROR: Step failed: ... (current step: ...).
There are two independent ways this runs in Codespaces. They do not depend on each other, and the dotfiles route is the one that works across every repo.
| Mechanism | Scope | Runs when |
|---|---|---|
| Dotfiles (account setting) | Every Codespace, any repo | On each Codespace you create, regardless of repository |
devcontainer.json (in this repo) |
This repo only | Only when you open a Codespace on codespaces-setup (or a repo whose .devcontainer symlinks here) |
Set johnmathews/codespaces-setup as your dotfiles repository under GitHub → Settings → Codespaces → "Automatically
install dotfiles". GitHub then clones it into every new Codespace and runs the first install script it recognises, in
this order:
install.sh → install → bootstrap.sh → bootstrap → script/bootstrap → setup.sh → setup → script/setup
setup.sh is on that list, so it runs automatically. It derives its own location (so it works from wherever GitHub
clones the dotfiles repo) and is fully self-contained (installs everything via apt/curl — it does not rely on this
repo's devcontainer.json features or image).
Where the scripts live: GitHub clones your dotfiles repo into every new Codespace at
/workspaces/.codespaces/.persistedshare/dotfiles, then runs setup.sh from there. So this repo's contents (including
scripts/ and configs/) end up under that path — for example /workspaces/.codespaces/.persistedshare/dotfiles/setup.sh.
The tools it installs are placed elsewhere on the system (binaries under /opt/<tool>-<version> symlinked into
/usr/local/bin, and dotfiles copied into $HOME); the path above is just where the checkout of this repo lands.
Notes:
- Only one dotfiles repo can be designated per account.
- The target repo's own
devcontainer.jsonis not replaced — dotfiles run on top of whatever base image/features that repo defines.setup.shassumes a Debian/Ubuntu (apt) base, which covers the default image and most devcontainers, but not non-Debian images. - If the dotfiles install fails, the Codespace still starts; check the creation log (Command Palette → "Codespaces: View
Creation Log") or
~/.cache/codespaces-setup.log.
Opening a Codespace on this repository (or a repo whose .devcontainer symlinks here) runs the postCreateCommand in
.devcontainer/devcontainer.json, which calls .devcontainer/post-create.sh → setup.sh. This applies only to this
repo, not to others. (If you also have dotfiles enabled, setup.sh runs twice here — harmless, since it is idempotent.)
git clone https://github.com/johnmathews/codespaces-setup.git ~/codespaces-setup
cd ~/codespaces-setup
bash setup.sh
exec zshThe script is idempotent – safe to run multiple times.
| Tool | Version | Purpose |
|---|---|---|
| Neovim | v0.11.5 | Editor (config from johnmathews/neovim) |
| Node.js | 22 LTS | Required for Neovim Mason LSP servers / formatters |
| eza | v0.20.14 | Modern ls replacement |
| yazi | v0.4.2 | Terminal file manager |
| lazygit | v0.60.0 | Git TUI |
| atuin | latest | Shell history (local, no sync in Codespaces) |
| uv | latest | Python package manager |
| Claude Code | latest | AI coding assistant CLI |
| Claude skills/commands | — | Personal /engineering-team skill + /done, /merge-push, and /prompt slash commands, deployed into ~/.claude |
GitHub CLI (gh) |
v2.95.0 | GitHub from the terminal; also used as git's credential helper |
Azure CLI (az) |
v2.68.0 | Azure from the terminal; installed via uv (isolated env; works behind the TLS-intercepting proxy) |
| Zsh + Oh My Zsh | — | Shell |
| Powerlevel10k (lean) | — | Zsh theme |
| zsh-autosuggestions | — | Fish-style suggestions |
| zsh-syntax-highlighting | — | Command highlighting |
| Git identity | — | Sets global user.name and user.email for commits |
| Editor tooling | — | Formatters/linters on PATH (prettierd, biome, eslint_d, markdownlint, ruff, mypy, stylua, shfmt) + glow |
htop, ffmpeg, 7zip, jq, poppler-utils, fd-find, ripgrep, fzf, zoxide, imagemagick, tree, bat,
tmux, rsync, sqlite3
.devcontainer/
devcontainer.json # Codespace auto-setup (postCreateCommand: bash setup.sh)
configs/
.zshrc # Zsh configuration
.zsh_aliases # Shell aliases
.gitconfig_managed # Git aliases + portable settings (included from ~/.gitconfig)
claude/ # Personal Claude Code assets, deployed into ~/.claude
skills/engineering-team/ # /engineering-team skill (SKILL.md + phases/ + references/)
commands/done.md # /done slash command
commands/merge-push.md # /merge-push slash command
commands/prompt.md # /prompt slash command
scripts/
01-apt-packages.sh # Install CLI tools via apt
02-nodejs.sh # Install Node.js 22 (official tarball into /usr/local)
03-neovim.sh # Install Neovim AppImage (v0.11.5)
04-neovim-config.sh # Clone johnmathews/neovim → ~/.config/nvim
05-eza.sh # Install eza binary
06-yazi.sh # Install yazi binary
07-lazygit.sh # Install lazygit binary
08-atuin.sh # Install atuin (local history only)
09-uv.sh # Install uv Python package manager
10-zsh-setup.sh # Install Oh My Zsh, plugins, Powerlevel10k
11-dotfiles.sh # Deploy .zshrc, aliases, gitconfig; set default shell to zsh
12-claude-code.sh # Install Claude Code CLI
13-nvim-plugins.sh # Pre-load Neovim plugins headlessly (run in background)
14-fonts.sh # Install MesloLGS NF Nerd Font (in-container; SSH use)
15-dev-tools.sh # Install formatters/linters + glow that Neovim needs on PATH
16-gh.sh # Install GitHub CLI (gh) from release tarball
17-claude-skills.sh # Deploy configs/claude/ skills + commands into ~/.claude
18-azure-cli.sh # Install Azure CLI (az) via uv, symlinked into /usr/local/bin
ci/
lint-steps.sh # Assert every scripts/NN-*.sh is wired into setup.sh's STEPS
.github/workflows/
ci.yml # shellcheck + shfmt + lint-steps + skill gates on push/PR
docs/
development.md # CI checks, local lint commands, how to add a step
tests/
engineering-team-probes/ # Manual, LLM-in-the-loop regression probes for the skill router
engineering-team-triggering/ # Does the skill's description: fire on the right prompts? (LLM measure on-demand; parsers CI-checked)
engineering-team-drift/ # Rule-ownership drift-scan: index can't lie about a rule's home; worktree cmds keep --path-format=absolute (headless, in CI)
engineering-team-command-links/ # Link-check: the /done + /merge-push commands' cross-references into the skill tree still resolve (headless, in CI)
journal/ # Dated development-journal entries
setup.sh # Main entry point – runs the ordered STEPS array, then
# launches 13-nvim-plugins.sh in background
deploy-engineering-team-skill.sh # Standalone: overwrite ~/.claude engineering-team
# skill with this repo's copy (only the skill; see below)
Note:
setup.shruns an explicit, orderedSTEPSarray — not every file inscripts/. Filename number prefixes are labels, not run order, and13-nvim-plugins.shis deliberately excluded (launched in the background). Adding a script means also adding it toSTEPS;ci/lint-steps.shenforces this. See docs/development.md.
- Shell config: edit
configs/.zshrcthen re-runbash scripts/11-dotfiles.sh - Aliases: edit
configs/.zsh_aliasesthen re-runbash scripts/11-dotfiles.sh - Git aliases: edit
configs/.gitconfig_managedthen re-runbash scripts/11-dotfiles.sh - Git identity:
scripts/11-dotfiles.shsets global Git user details for the Codespace - Claude skills/commands: edit the files under
configs/claude/then re-runbash scripts/17-claude-skills.sh. To add another skill or command, drop it intoconfigs/claude/skills/<name>/orconfigs/claude/commands/<name>.mdand wire it into the deploy calls inscripts/17-claude-skills.sh. For the common case of just re-pushing theengineering-teamskill after an edit, runbash deploy-engineering-team-skill.sh— it deploys only that skill and overwrites the local copy unconditionally (no commands, no diff-skip) - Editor tooling:
scripts/15-dev-tools.shinstalls the formatters/linters (andglow) that Neovim expects onPATH..zshrcalso points Node/Python at the system CA bundle so Mason can install npm/pip tools behind a TLS-intercepting proxy (otherwise installs fail withSELF_SIGNED_CERT_IN_CHAIN) - Local overrides (not managed here):
~/.zshrc.localand~/.zsh_aliases.local - Prompt: run
p10k configureafter setup to customise the Powerlevel10k theme
There are no unit tests (the "product" is the provisioning scripts), but CI (.github/workflows/ci.yml) lints them on
every push to main and PR, and you can run the same checks locally:
shellcheck setup.sh deploy-engineering-team-skill.sh scripts/*.sh ci/*.sh # shell correctness
shfmt -i 2 -ci -kp -d setup.sh deploy-engineering-team-skill.sh scripts ci # formatting (-w to auto-fix)
bash ci/lint-steps.sh # every scripts/NN-*.sh is wired into STEPSFull details — the formatting convention, why -kp, and how to add a new provisioning step — are in
docs/development.md.
Codespaces auto-injects a GITHUB_TOKEN environment variable that is scoped to the repository the Codespace was
created from. gh (and git) prefer that token over any stored credentials, so gh auth login refuses to save your own
token while it is set, and you can't push to other repos.
To work around this, configs/.zshrc runs unset GITHUB_TOKEN GH_TOKEN for interactive shells. Once it's cleared,
authenticate as yourself once per Codespace:
gh auth login # choose GitHub.com → HTTPS → paste a PAT or use the web flowAfter that, git push/pull work against any repo because configs/.gitconfig_managed registers
gh auth git-credential as git's credential helper for github.com.
If you specifically need the original restricted token back in a shell (e.g. for headless automation), re-export it:
export GITHUB_TOKEN=....
The Neovim config is pulled directly from johnmathews/neovim into
~/.config/nvim. Plugins are managed by lazy.nvim and are pre-loaded in the
background during Codespace creation so that nvim is ready to use immediately. Progress is logged to
~/.cache/nvim-setup.log (see Logs).
If the background pre-load is still running when you first open nvim, plugins will already be partially or fully
installed – lazy.nvim will not re-download what it has already cached.
- johnmathews/neovim – Neovim configuration
- johnmathews/home-server – Ansible roles (particularly the
shell_environmentrole that this repo mirrors for Codespaces)