# `dev-toolkit` kit A Docker Sandboxes **v2 mixin kit** that provisions a general-purpose development toolkit: a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills covering code quality, documentation, and a controlled feature-development workflow. | Tool | Version | Installed at | Notes | | --- | --- | --- | --- | | qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics engine, for any language. | | Go | 1.26.5 | `/usr/local/go` | Symlinked into `/usr/local/bin`, shadowing the template's own `/usr/bin/go`. | | TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` set; for `wasm`, `wasip1`, `wasip2` targets. | | Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup, `minimal` profile + `clippy` + `rustfmt`, with the `wasm32-wasip2` target. | | Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the template's own `/usr/bin/node` (22.x). | | pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Plus bundled `corepack`, for projects that pin a version via `packageManager`. | | vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions to the Marketplace / Open VSX. | | gcc, g++ | Ubuntu's | `/usr/bin` | The base template has **no** C compiler; Rust and node-gyp both need one. | | Skill | Purpose | | --- | --- | | `quality` | Configures qlty, measures the project, writes a report, tracks progression, refactors until the gate passes. | | `diataxis-doc` | Writes or restructures documentation with the Diátaxis four-quadrant method. | | `methodical-dev` | Orchestrator. Drives a feature through requirements → plan → implementation → quality gate → bilingual docs → project memory, stopping for approval at every step. Invokes the other two. | Plus a `~/.claude/CLAUDE.md`, loaded at the start of every session, which brackets the work at both ends: **read** the project's committed `.memory/` record — summary, history, latest handoff — before planning or asking the user anything, and **update all three** before handing control back, unconditionally, however small the session. `summary.md` is edited in place and never regenerated from one session's partial view; `history.md` gets exactly one appended entry; `handoffs/` gets today's file. `methodical-dev` (Phase 7) holds the full templates — `CLAUDE.md` is what makes the same discipline apply to ad-hoc work that never invokes the skill. **Only the toolchain binaries are language-specific.** qlty is not a Go tool: `qlty init` detects the languages present and enables the matching linters, and all three skills are language-agnostic. Drop the toolchain install steps from `spec.yaml` and the kit is a pure quality/docs/workflow mixin for any stack. The install steps are independent of each other, so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except the C-toolchain step, which Rust and native npm addons both depend on. Project-specific skills do **not** belong here. `builtin-parity`, which describes the GoloScript repository in particular, lives in the sibling [`golo-toolkit`](../golo-toolkit/) kit and is composed in only where it applies. ## Layout ``` kits/dev-toolkit/ ├── spec.yaml # schemaVersion "2", kind: mixin ├── README.md └── files/home/.claude/ ├── CLAUDE.md # loaded every session: read .memory/ first └── skills/ ├── quality/ │ ├── SKILL.md # the workflow the agent follows │ └── scripts/quality_report.py # measurement, report, history, gate verdict ├── diataxis-doc/SKILL.md └── methodical-dev/SKILL.md ``` `files/home/**` is copied into the agent's home at container start, so the skills land under `~/.claude/skills/` and Claude Code picks them up as personal skills, and `CLAUDE.md` lands at `~/.claude/CLAUDE.md` — the user-level memory file, prepended to **every** Claude Code session in the sandbox regardless of which project is open. **Why both a `CLAUDE.md` and a skill say to read `.memory/`.** `methodical-dev`'s Phase 0 only runs when that skill is invoked. `~/.claude/CLAUDE.md` is loaded unconditionally, so an ad-hoc request ("fix this bug") also starts by reading the project record instead of re-deriving state that is already written down. Keep the file short — it costs tokens on every single session; the detail belongs in the skills and in the project's own `.memory/README.md`. Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to the skills themselves — a stray `__pycache__/` would be shipped alongside them. The repository `.gitignore` guards against that. ## Usage ```bash # create the sandbox with the kit — `--kit` at create time, not `sbx kit add`, # so the cache volumes are attached (see Known limitations) sbx run claude --kit ./kits/dev-toolkit ``` Then, in the agent session: *"run a quality report"*, *"document this project with Diátaxis"*, or *"let's build this feature methodically"*. ### Editor extensions The two ecosystems use different toolchains, and both are provisioned: | | VS Code | Zed | | --- | --- | --- | | Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` + `Cargo.toml` | | Build | the project's own script (tsc, esbuild) | `cargo build --release --target wasm32-wasip2` | | Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | Publish | `vsce publish`, `ovsx publish` | a PR to `zed-industries/extensions` | `wasm32-wasip2` is preinstalled because that is the target Zed compiles extensions with; building for anything else produces an artifact it refuses. Both flows were verified inside the sandbox end to end — `npm install` → `tsc` → `vsce package` → `.vsix`, and `zed_extension_api` → `cargo build --target wasm32-wasip2` → a WASM **component** (layer `0100`, not a core module). What does **not** work here, by construction: no editor runs in the sandbox, so VS Code's F5 Extension Development Host and Zed's *Install Dev Extension* are host-side actions, and `@vscode/test-electron` needs an X server this kit does not install. Compile, unit-test, lint and package in the sandbox; drive the editor on the host. ## Design notes **Why a mixin and not a `kind: sandbox` kit.** The kit adds tooling to whatever agent you are already using rather than defining its own image, so it composes: `--kit` it onto `claude`, `codex`, `shell`, or anything else. Exactly one `sandbox` kit is allowed per composition; mixins stack freely. **Why pinned tarballs instead of the official installers.** `curl https://qlty.sh | sh` resolves `latest` (so two sandboxes built a day apart differ), POSTs an install event to `cdp.customer.io`, and rewrites the user's shell rc files. Every artifact here is pinned by version **and** SHA256, verified with `sha256sum -c` before it is unpacked — the same pattern the official `trivy`, `vale`, and `mise` kits use. qlty's own published `.sha256` files match the digests in `spec.yaml`. **Why Python unpacks the qlty archive.** qlty ships only `.tar.xz` and the sandbox template has no `xz` binary. Installing `xz-utils` would mean running `apt-get update`, which re-fetches *every* configured apt source — including the template's `download.docker.com` — and fails if any one of them is not allow-listed. `python3` is present, and `lzma` is in its standard library, so the archive is decompressed with no extra egress at all. **Toolchain pairing is load-bearing.** TinyGo 0.41.1 accepts Go 1.19–1.26 *and* refuses a GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. `GOTOOLCHAIN=local` prevents Go from silently downloading a different toolchain behind your back. **Why a C compiler is a hard requirement, not a nicety.** The base template ships `make` but no `cc`, `gcc`, `clang`, `ld` or libc headers. That produces a genuinely misleading failure: `cargo build --target wasm32-wasip2` **succeeds** without a C compiler, because rustc links wasm targets with its own bundled `rust-lld` — while `cargo test`, which builds for the host, dies with ``error: linker `cc` not found``. A Zed extension therefore appears to build fine and cannot be unit-tested, which is the worst possible shape for a missing dependency. The same gap breaks any crate with a C-compiling `build.rs`, and any npm package with a native addon built through node-gyp. `apt-get update` is deliberately *not* run: the template ships populated `/var/lib/apt/lists`, so a plain `apt-get install` works, whereas `update` re-fetches every configured source (Docker's included) and fails if one is not allow-listed. It stays only as a fallback for the day those lists go stale. **Why rustup rather than the standalone Rust tarballs.** A pinned `rustup-init` *binary* (digest-verified, not the `curl https://sh.rustup.rs | sh` bootstrapper, which resolves `latest` and edits shell rc files) buys a working `rustup target add` — which matters because Zed itself shells out to exactly that when an extension needs a target that is not installed. Rust lives under `/usr/local` rather than in the home directory, for the same reason Go does: the install runs once at container creation, and only paths baked into the image survive a restart. Both `RUSTUP_HOME` and `CARGO_HOME` are left world-writable — the layout the official `rust` Docker image uses — so `rustup target add`, `rustup component add` and `cargo install` work as the agent user without sudo. **Why the kit does *not* set npm's prefix.** The obvious move — `npm_config_prefix=/usr/local`, so global installs land in `/usr/local/bin` — is wrong here. The base template already exports `NPM_CONFIG_PREFIX=/usr/local/share/npm-global`, and that directory is **agent-owned and already on PATH ahead of `/usr/local/bin`**. Overriding it would move global installs into a root-owned tree and break `npm install -g` for the agent user, which currently works with no sudo. The install step passes `--prefix` explicitly (rather than trusting the variable to be exported into a root-run step) and then `chown`s the tree back to the agent. **`--allow-scripts` is not optional for vsce.** npm 11 skips unrecognised lifecycle scripts and merely warns. Two of vsce's dependencies need theirs: `keytar` builds the native addon that stores a publishing PAT, and `@vscode/vsce-sign` unpacks the signing binary. Skipped, they fail at `vsce publish` time — long after the install that caused it. **The cache volumes.** Three, all of them things that are otherwise re-downloaded on a cold start: `~/.qlty` (qlty provisions a JVM for `radarlint-go`, Node, and each linter binary on demand), `/usr/local/cargo/registry` (Cargo's `.crate` files and sparse index) and `~/.npm`. The Cargo one is deliberately the `registry` subdirectory rather than all of `CARGO_HOME`: mounting an empty volume over `CARGO_HOME` would shadow the rustup shims in its `bin`. Volumes are fixed at container creation, so `sbx kit add` warns and skips them — use `--kit` at create time to get the caches. ## Maintenance **Bumping a version.** Change the version *and* both per-arch `SHA256` values in the matching `setup.install` entry in `spec.yaml`. Sources: - Go — (`files[].sha256`) - TinyGo — compute from the release tarball; the project publishes no checksums file - qlty — the `.tar.xz.sha256` file next to each release asset - Node — `https://nodejs.org/dist/v/SHASUMS256.txt`. Pick from the **active LTS** line; `https://nodejs.org/dist/index.json` marks it with a non-`false` `lts` field. Note the `.tar.gz` digest, not `.tar.xz`: the template has no `xz`, and unlike qlty, Node publishes both — which is why this step needs no python-lzma workaround - rustup — `https://static.rust-lang.org/rustup/archive///rustup-init.sha256`, with the current version at `https://static.rust-lang.org/rustup/release-stable.toml` - Rust itself — no digest to update; rustup resolves and verifies the toolchain. The current stable version is the `[pkg.rust] version` in `https://static.rust-lang.org/dist/channel-rust-stable.toml` - vsce / ovsx / pnpm — `npm view version` **Before bumping the Zed target.** `wasm32-wasip2` is not a preference — it is read from Zed's own extension builder (`RUST_TARGET` in `crates/extension/src/extension_builder.rs`). Confirm there before changing it; an extension built for another target is one Zed refuses to load. **Validating a change.** ```bash sbx kit validate ./kits/dev-toolkit sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings' # expect null or [] ``` Empty warnings is the green light — a non-empty list means a v1 surface crept in that will stop loading at the spec's Phase 6 cutover. `Install commands completed` only means the commands exited `0`. Verify the outcome: ```bash sbx exec -- sh -lc ' go version && tinygo version && qlty --version && rustc --version && cargo --version && rustup target list --installed && node --version && npm --version && pnpm --version && vsce --version && ovsx --version && cc --version | head -1' ``` And verify the thing versions cannot tell you — that host-target Rust actually **links**, which is what the C-toolchain step exists for: ```bash sbx exec -- sh -lc ' cd "$(mktemp -d)" && cargo init --lib -q . && cargo test 2>&1 | tail -3' ``` **Extending the network allowlist.** `permissions.network.allow` covers the install downloads, qlty's plugin/linter resolution, the Go module proxy, Cargo's three hosts (the sparse index `index.crates.io`, the CDN `static.crates.io`, and the API `crates.io` — all three are needed), Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths. A linter for another language will need its registry added. Find out what is missing rather than guessing: ```bash sbx policy log ``` That prints what the proxy actually allowed and blocked. Add blocked hosts one at a time. ## Known limitations - **`sbx kit add` does not write mixin context.** The engine gates the kit-memory write on the artifact's own AI-profile filename, which mixins deliberately do not carry (`agentInstructions.filename` is ignored for a mixin), so adding this kit at runtime silently skips `kits-memory/dev-toolkit.md`. Create the sandbox with `--kit` instead. The same applies to the `~/.qlty` volume. - **The skills and `CLAUDE.md` are Claude Code-specific.** The toolchain is not. The kit sets no `requires.agent`, so it works with any agent; on non-Claude agents those files are inert and `agentInstructions` carries the same instructions — the `.memory/`-first rule and the `quality` script invocation. - **Only `linux/amd64` and `linux/arm64`** are supported. Other architectures fail the install with an explicit error rather than installing something wrong. - **No editor, and no display.** Nothing in this kit can run VS Code or Zed. The Extension Development Host (F5), Zed's *Install Dev Extension*, and any `@vscode/test-electron` run all need a GUI session on the user's machine; there is no X server here and the kit does not install `xvfb`. The sandbox covers compile, unit-test, lint and package — write extensions so their logic is testable without the `vscode` module and the split costs you nothing. - **Tree-sitter grammars in Zed extensions are not reproducible here.** Compiling a parser needs the wasi-sdk, which Zed downloads itself, on the host. The Rust half of such an extension builds fine; the grammar half does not happen in this sandbox. - **The C toolchain is the one unpinned install.** Go, TinyGo, qlty, Node and rustup are all version-and-digest pinned; `gcc` comes from Ubuntu's archives at whatever version they serve (15.2.0 at time of writing). Pinning it would mean pinning a `.deb` set and its transitive closure, which is a much larger commitment than it is worth for a linker driver — but it does mean two sandboxes built months apart can differ here.