Skip to content

Latest commit

 

History

History
323 lines (251 loc) · 14.9 KB

CLAUDE.md

File metadata and controls

323 lines (251 loc) · 14.9 KB

CLAUDE.md

Context for working in this repository. Read this before making changes.

What this repo is

A single source of truth for provisioning Craig's development environment across several targets. It started as one bash script and is being extended into multiple provisioning mechanisms that must all converge on the same toolchain.

Existing:

  • bootstrap.sh - imperative bash provisioner for Ubuntu 26.04 on WSL2. Working and verified. Treat it as the reference definition of "what a correctly set up machine looks like".

To build:

  • Nix flake plus home-manager configuration (the intended long-term replacement for bootstrap.sh)
  • Dockerfile and .devcontainer/ definition
  • Ansible role
  • Possibly a Vagrant box. See "Open questions" before starting this.

Environment

Primary target is Ubuntu 26.04 LTS (Resolute Raccoon) on WSL2, on a personal Windows 11 machine (craig-pc, user craig). An Arch WSL distro (archlinux) still exists and is the source system being migrated from.

There is also a work MacBook running corporate security agents, and a corporate Windows build with WSL running Oracle Linux 9 behind a CNTLM proxy. Anything written here should not assume unproxied egress or a trusted default CA bundle. bootstrap.sh handles this with PROXY and CORP_CA variables; new provisioners should expose equivalents.

Shell is bash. Locale is en_GB.UTF-8.

The toolchain (canonical list)

Anything you write must provision all of these. This list came from pacman -Qqe plus brew leaves on the Arch box.

From the Ubuntu archive: build-essential, git, git-lfs, openssh-client, gnupg2, 7zip, zip, unzip, xz-utils, dos2unix, bind9-dnsutils, iputils-ping, net-tools, traceroute, htop, ncdu, tree, less, lsof, strace, jq, nano, neovim, man-db, manpages, podman, podman-compose, slirp4netns, fuse-overlayfs, uidmap, python3, graphviz, poppler-utils, pandoc, texlive (latex-base, latex-recommended, latex-extra, fonts-recommended, fonts-extra, xetex), fonts-crosextra-carlito, fonts-dejavu, fonts-liberation

From vendor apt repos: azure-cli, powershell, gh, dotnet-sdk, pdk, cloudflared, trivy

powershell and azure-cli are not optional extras. Every provisioner you write, including the container image and the Ansible role, must install both plus the PowerShell modules below. A target without pwsh and az is not considered provisioned.

Standalone binaries: yq (mikefarah, not the Ubuntu package of the same name), tenv, glab, keybase, starship, uv, conftest, terraform-docs, just, shellcheck

shellcheck is here because "shellcheck clean at -S warning" is a hard constraint below, and a constraint you cannot check on the machine is one that only ever fails in CI. Its release asset is .tar.xz, not the .tar.gz that tar_install handles, which is why bootstrap.sh installs it longhand.

Toolchain managers: goenv, nvm, sdkman

Via uv: pre-commit

Homebrew: cloudflared, conftest, gh, graphviz, just, nvm, poppler, starship, terraform-docs, trivy. Brew is installed deliberately even though almost everything above duplicates it. Do not "optimise" it away without asking.

Python venv at ~/.venvs/developer, Python 3.12, containing ansible-core, ansible-lint, pre-commit, pyyaml, requests, azure-identity, azure-mgmt-resource, msgraph-sdk, pandas, rich. Activated by the dev shell function.

PowerShell modules (user scope, from PSGallery): Az, Microsoft.Graph, Pester, LibreDevOpsHelpers. Az and Microsoft.Graph are meta-modules that pull in dozens of sub-modules each, so a cold install is slow. This is expected, not a hang. LibreDevOpsHelpers is Craig's own module; if it is not resolvable from the public gallery, a feed needs registering with PSGALLERY_TOKEN first.

bootstrap.sh installs these in its psmodules section, which runs after repos because it needs a Linux pwsh to exist first. It trusts PSGallery once up front, since Install-Module otherwise stops on the untrusted repository prompt in a non-interactive shell. A failure to install LibreDevOpsHelpers warns and continues rather than aborting, because the private feed is the expected reason for it.

Deliberately excluded: firefox (GUI), yay (Arch-only), ex-vi-compat (superseded by vim), gcc as a brew formula (build-essential covers it), yt-dlp (dropped from the toolchain).

Hard constraints

No secrets in this repository, ever. Secrets live in ~/.config/shell/secrets.env at mode 0600, sourced from .bashrc, and are never committed. If you generate a template, it contains commented-out variable names only. .gitignore must cover secrets.env, *.key, *.asc, *.pem, .env.

Credentials that have appeared in this environment historically: ARM_TENANT_ID, ARM_SUBSCRIPTION_ID, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_TOKEN, PUPPET_FORGE_API_KEY, PSGALLERY_TOKEN, GITHUB_TOKEN, TF_VAR_github_token, SNOW_*, TF_VAR_SNOW_OAUTH_*. Reference them by name, never by value.

Work inside the WSL filesystem. Repos live under ~/src/. Never work under /mnt/c - the 9p bridge makes git and node operations pathologically slow, and it introduces CRLF line endings that break shebangs.

Everything must be idempotent. Re-running any provisioner on an already provisioned machine must be a no-op that exits zero. bootstrap.sh does this with per-item have/skip guards; keep that property.

Bash scripts: set -euo pipefail, shellcheck clean at -S warning, LF line endings.

No em dashes or en dashes in any generated documentation or comments.

Things already learned the hard way

Do not rediscover these.

Third-party apt repos lag a new LTS. Microsoft, HashiCorp, Puppet and others had not published a resolute suite for 26.04 at the time of writing. bootstrap.sh pins them to noble via REPO_SUITE, which works. Any new provisioner needs the same escape hatch. Set REPO_SUITE=auto once vendors catch up.

GitHub CLI's keyring is githubcli-archive-keyring.gpg, binary, with no .asc variant. Requesting .gpg.asc 404s.

Puppet's signing key filename contains a rotation date that changes. Use puppet-tools-release-<suite>.deb rather than fetching the key directly.

A failing repo key must not abort provisioning. Warn, name the repo and URL, and continue.

Ubuntu's yq package is a different project from mikefarah's yq and has incompatible syntax. Always install the GitHub release binary.

GPG import needs loopback pinentry on a fresh WSL instance. There is no TTY bound and no graphical pinentry, so gpg --import fails with error sending to agent: Bad secret key, which is misleading. Fix is pinentry-curses plus allow-loopback-pinentry in ~/.gnupg/gpg-agent.conf, and --pinentry-mode loopback on the import. GPG_TTY must be exported in .bashrc or commit signing fails.

Git credential helpers must use absolute paths that survive package changes. The Arch config pointed at /home/linuxbrew/.linuxbrew/bin/gh, which would break on any brew reinstall. Prefer /usr/bin/gh and /usr/bin/glab. Do not resolve with command -v at provision time, because the answer depends on which sections have already modified PATH.

PATH precedence is deliberate. Brew's shellenv is evaluated first so that apt, uv and direct binary installs shadow brew formulae of the same name. Several binaries exist twice (gh, starship, just, conftest, trivy, terraform-docs, cloudflared). This is intentional, not a bug.

shred does not work on /mnt/c. drvfs cannot do overwrite-in-place, and uutils shred panics partway, leaving the file under a randomised name. Use rm.

A presence check must reject /mnt/ resolutions. WSL appends the Windows PATH to the Linux one, so command -v az happily returns /mnt/c/Program Files/Microsoft SDKs/Azure/CLI2/wbin/az on a box with no Linux az installed at all. verify reported that as ok for as long as it used a bare have. A Windows binary is not a provisioned Linux toolchain. The same trap applies to pwsh, gh and git, and to any check written in any of the other provisioners.

Check for the binary, not the package. graphviz ships dot, neato and friends but never a binary called graphviz, so verifying graphviz by name reported it missing forever, including on machines where it was correctly installed. poppler-utils is the same shape, which is why the check is pdftotext.

gh_latest fails on an unauthenticated rate limit, not just when the network is down, and an empty tag silently produces a malformed release URL. Callers use "$(gh_latest owner/repo || true)" and then test for empty, because a bare assignment from a failing command substitution aborts the whole run under set -e. Resolve tags from the /releases/latest redirect rather than api.github.com: the API allows 60 unauthenticated requests an hour per IP and this script makes several.

glab is not released on GitHub. github.com/gitlab-org/cli is a read-only mirror that publishes no releases, so a GitHub tag lookup returns 404 and can never work. Resolve it from gitlab.com/gitlab-org/cli/-/releases/permalink/latest, which is what gl_latest does, and download the deb from GitLab too.

sudo -v prompts for a password even under NOPASSWD:ALL, because it matches no specific command. With no TTY it blocks forever instead of failing, so anything calling it hangs the run rather than erroring out of it. The starship installer does exactly this before writing to /usr/local/bin, which is why it is pointed at ~/.local/bin instead. Real commands under sudo are unaffected. Homebrew's installer is safe here only because NONINTERACTIVE=1 makes it use sudo -n.

azure-cli is not in Microsoft's prod feed for noble. It has its own repository at packages.microsoft.com/repos/azure-cli. Related and worse: never name several vendor packages in one apt-get install. A single unavailable package makes apt install none of them, so a missing azure-cli silently took powershell and gh down with it and the warning blamed all three. One package per call.

Microsoft retires dotnet SDK series per suite. dotnet-sdk-9.0 and dotnet-sdk-8.0 no longer resolve for noble; dotnet-sdk-10.0 does. Try newest first and take whichever resolves, rather than pinning a version that will age out.

How you are being run

Claude Code runs on the Windows host, not inside WSL, and reaches the Linux environment through pwsh invoking wsl.exe. This has consequences you must account for on every command.

Invoke Linux work as a login shell so that PATH, brew, goenv, nvm and sdkman are all initialised:

wsl -d Ubuntu-26.04 -- bash -lc "cd ~/src/dotfiles && <command>"

Without -l, none of the .bashrc PATH setup applies and half the toolchain will appear to be missing.

Rules that follow from this:

  • Never write files to the repo from the Windows side. Line endings and file modes both get mangled. Write them from inside WSL, or write them on Windows and immediately run dos2unix plus chmod +x in WSL.
  • Paths in committed files are Linux paths. ~/src/dotfiles, not C:\Users\craig\... and not /mnt/c/....
  • Do not assume the Windows and Linux toolchains are the same. pwsh exists on both; az, git and gh may exist on both with different versions and different credentials. Work in WSL unless there is a specific reason not to.
  • Quoting nests three deep here (PowerShell, then wsl, then bash -lc). When a command gets hairy, write a script file in WSL and execute that instead of fighting the escaping.
  • $var is expanded before it reaches WSL bash, even inside single quotes. $name, ${name} and $(cmd) are all consumed by a layer above the shell you think you are writing for, so for b in a b; do echo "$b"; done arrives with $b already empty and prints blank lines rather than failing. Write \$b to pass one through. This is silent, and it produces plausible-looking wrong output rather than an error, so anything with a variable in it needs a control case before you trust the result. For anything longer than one line, write the script to a file and run the file.
  • pkill -f matches your own bash -lc command line, because the pattern you are searching for is sitting in it. It will kill the shell running the pkill. Match on a pattern that cannot appear in your own invocation, or kill by PID.

Commit conventions

  • Commit as Craig. Craig Thacker <craigthackerx@gmail.com>, signed with 82D2F9D5BA2AE757. The repo's git config already does this.

  • No trailers or sidecars of any kind. No Co-Authored-By, no Generated-with, no tool attribution, no emoji markers. The commit body ends where the message ends.

  • English. British spelling to match the rest of the repo.

  • No em dashes or en dashes, in commit messages as in everything else.

  • Imperative mood in the subject line, wrapped at 72 characters. Body only when the change needs explaining.

  • Git user: Craig Thacker <craigthackerx@gmail.com>

  • GPG signing key: 82D2F9D5BA2AE757 (ed25519, created 2022-01-29)

  • commit.gpgsign = true is expected everywhere

  • Git aliases: a = add --all, c = commit, p = push

  • Editor: nano

Note: the signing key currently has no passphrase. If provisioning touches GPG, do not make that worse; ideally flag it.

Verifying your work

bootstrap.sh verify is the acceptance test. It checks every expected binary resolves, the developer venv exists, the PowerShell modules are installed, the GPG secret key is present, and reports shadowed duplicates. Any new provisioner should be checkable against the same criteria. Extending verify into a standalone script that all four mechanisms can share would be a reasonable early move.

verify exits non-zero when anything is missing, so it can gate. It is the only section allowed to do that: the driver runs it via || VERIFY_RC=1 so its result becomes the script's exit status without aborting any section queued behind it. A consequence worth knowing is that a full ./bootstrap.sh run on a machine that is not yet complete now exits 1, which is deliberate.

Smoke tests:

ssh -T git@github.com
echo test | gpg --clearsign > /dev/null
git -C ~/src/<repo> commit --allow-empty -m 'signing test'

Open questions

Is Vagrant worth building? WSL2 is itself the VM, so there is no nested hypervisor available and a box would have to be driven from Windows against Hyper-V. Given Nix and a devcontainer both cover reproducibility, Vagrant may be redundant. Confirm with Craig before investing in it.

Nix scope. home-manager alone (user packages and dotfiles, layered on Ubuntu) is a much smaller step than full NixOS-WSL, and would also work on the work MacBook. Establish which is wanted before writing a flake.

Convergence. Once Nix exists, bootstrap.sh becomes redundant for the personal machine but stays useful where Nix cannot be installed, which includes locked-down corporate builds. Plan for both to coexist rather than deleting the script.