Seven scripts. One runs on the Bazzite host. One runs inside each supported development environment. None of them writes a credential.
| Script | Where it runs |
|---|---|
bootstrap/host.sh |
The Bazzite host |
bootstrap/web-dev.sh |
Inside web-dev |
bootstrap/python-dev.sh |
Inside python-dev |
bootstrap/rust-dev.sh |
Inside rust-dev |
bootstrap/golang-dev.sh |
Inside golang-dev |
bootstrap/dotnet-dev.sh |
Inside dotnet-dev |
bootstrap/android-dev.sh |
Inside android-dev |
bootstrap/godot-dev.sh |
Inside godot-dev |
Together, these scripts converge the whole machine. To install one part, use the component installer in components.md:
./install.sh --components devboxThe host bootstrap and the environment bootstraps converge the same state,
because each step is one function in bootstrap/lib/, and the callers use it:
| Library | Step |
|---|---|
lib/common.sh |
The shared idempotency, backup and dry-run helpers |
lib/component.sh |
The preamble every component operation script uses |
lib/platform.sh |
The platform adapter lookup, which answers how this machine supplies a capability |
lib/host-packages.sh |
Homebrew formulae and casks, Flatpak applications |
lib/devbox.sh |
The router, the agent host shims, the router configuration |
lib/environments.sh |
Creation of one Distrobox development environment |
lib/agent-home.sh |
The host agent policy, status line and preferences |
lib/sandcastle.sh |
The agentbox CLI and its credential file location |
lib/agentq.sh |
The agentq host shim and the container-side runtime |
Both scripts use bootstrap/lib/common.sh, which gives them the same
guarantees:
- Idempotent. Each step checks the current state and changes only what does not match.
- Non-destructive. Anything a script replaces is copied into
~/.agents/backups/<timestamp>/first. - Additive for configuration this repository does not own. Claude and Codex defaults add missing keys and preserve existing values. Codex also migrates the previous toolkit policy when both old values are present. user hooks and state that a client wrote for itself survive.
--dry-run. Prints what a run would change, and writes nothing.
Both scripts print a summary at the end: how many steps changed something, how many were already correct, where the backups are, and which manual steps remain.
bootstrap/host.sh [--dry-run] [--skip-brew] [--skip-flatpak]
It refuses to run inside a container.
| Step | Idempotency check |
|---|---|
| Directories | test -d |
| Homebrew taps, formulae, casks | compared against brew list and brew tap |
| Flatpak applications | compared against flatpak list --app |
| Router, verify suite, wrappers | symlink already points at the checkout |
claude and codex shims |
generated by devbox new-shim only when absent |
grok shim |
generated by devbox new-shim; a non-shim file at that path is replaced |
agentq shim |
generated by devbox new-shim --env --map-path, and refreshed when the text differs |
| Router configuration | symlink already points at the checkout |
repos.tsv |
installed from the template only when the file is absent |
| The container of every supported environment module | podman container exists; an existing container is never touched |
| Host Codex policy and status line | symlink and item-list comparison |
The shims are generated by the router itself rather than copied from this repository. There is then one source of truth for the shim, and it cannot drift from the router that consumes it.
Two of them are treated differently, on purpose:
claudeandcodexcarry nothing but a tool name, so the bootstrap creates one only when it is absent and never touches a hand edit.grokis the same kind of shim. A file that already carries the shim header is left alone. A file that does not is the upstream installer link, and the bootstrap replaces it. The copy goes to the backup directory. The bootstrap also removes the upstream PATH block from the host shell rc. That block would run the host binary instead of the shim. The host binary under~/.grok/binstays on disk.agentqcarries the environment that owns the runtime, read fromAGENTQUEUE_ENVIRONMENTinmanifests/agentqueue.env. That is configuration, and stale configuration must not survive a bootstrap, so the file is compared and rewritten when it differs. The copy it replaces goes to the backup directory first.
devbox new-shim <tool> --env <environment> writes a pinned shim:
exec "$router" exec <environment> --cwd "$PWD" -- <tool> "$@"
Use it for a command that is installed in exactly one environment. The working
directory then decides which repository the command works on, and never where
the command runs. devbox new-shim <tool> --print writes the same text to
standard output, which is how a bootstrap compares before it writes.
--map-path <option> adds one more thing to the route line:
exec "$router" exec <environment> --cwd "$PWD" --map-path --repo -- <tool> "$@"
The user types a host path in that option, and the tool reads it inside the
container. The router then translates the value the way it translates the
working directory. An absolute value maps directly, a relative one resolves
against the host working directory first, and a leading ~ expands against the
host home. The option is repeatable, and it needs --env, because only a pinned
shim knows the destination.
bootstrap/web-dev.sh [--dry-run] [--skip-skills]
It refuses to run on the host.
| Step | Idempotency check |
|---|---|
| Distribution packages | rpm -q, from manifests/web-dev-packages.txt |
| nvm | test -s $NVM_DIR/nvm.sh |
| Shell integration | skipped when ~/.bashrc already loads nvm |
| Node | nvm version <v> |
| Corepack, pnpm | version comparison against manifests/toolchain.env |
| Claude Code, Codex, Grok | test -x on the binary. Grok's binary is ~/.grok/bin/grok |
agentq runtime |
symlink target comparison |
~/.agents, ~/.claude, ~/.codex wiring |
symlink target comparison |
| Skills | bin/install-skills compares content against each source |
| Client preferences | absent keys only |
| Status line | install.sh, which compares before it writes |
The stock Fedora ~/.bashrc already sources ~/.bashrc.d/*. The script
installs 10-nvm.sh there, which is the tidy place for it. If ~/.bashrc
already loads nvm inline, as it does on the machine this repository was captured
from, the script leaves it alone. nvm is then never loaded twice, and an
existing hand edit survives.
The router runs commands through the box login shell, because
environments.d/web-dev.env sets login_shell = 1. That is what makes node
and pnpm resolve for every routed command, with no init_file and no
toolchain knowledge in the router.
Claude Code, Codex and Grok are native installers, not npm packages, and each
one self-updates. manifests/toolchain.env therefore records the verified version
rather than a hard pin, and the bootstrap installs a CLI only when it is absent.
It never downgrades one that is already there. Grok's installer writes
~/.grok/bin/grok. The bootstrap then links ~/.local/bin/grok to that binary
inside the environment. The host path of the same name is the shim.
bootstrap/python-dev.sh [--dry-run] [--skip-skills]
It refuses to run on the host.
| Step | Idempotency check |
|---|---|
| Distribution packages | rpm -q, from manifests/python-dev-packages.txt |
| Shell integration | config/python-dev/bashrc.d/10-python-dev.sh, compared before it is written |
| uv | version comparison against manifests/python-dev.env |
| Python ownership | the manifest is refused when it pins a PYTHON_VERSION |
| Claude Code, Codex, Grok | test -x on the binary. Grok's binary is ~/.grok/bin/grok |
~/.agents, ~/.claude, ~/.codex wiring |
symlink target comparison |
| Skills | bin/install-skills compares content against each source |
| Client preferences | absent keys only |
| Status line | install.sh, which compares before it writes |
The environment pins uv and no interpreter. A repository owns its Python
version through .python-version or requires-python, and uv installs that
interpreter into the isolated container HOME. docs/python-dev.md says why.
bootstrap/rust-dev.sh [--dry-run] [--skip-skills]
It refuses to run on the host.
| Step | Idempotency check |
|---|---|
| Distribution packages | rpm -q, from manifests/rust-dev-packages.txt |
| Shell integration | config/rust-dev/bashrc.d/10-rust-dev.sh, compared before it is written |
| rustup | test -x $CARGO_HOME/bin/rustup. An existing rustup is never reinstalled |
| Baseline toolchain | compared against rustup toolchain list |
| Claude Code, Codex, Grok | test -x on the binary. Grok's binary is ~/.grok/bin/grok |
~/.agents, ~/.claude, ~/.codex wiring |
symlink target comparison |
| Skills | bin/install-skills compares content against each source |
| Client preferences | absent keys only |
| Status line | install.sh, which compares before it writes |
manifests/rust-dev.env pins the toolchain as a baseline, not as a
ceiling. It is what a repository gets when it pins nothing, and a repository
that carries rust-toolchain.toml overrides it. docs/rust-dev.md says why.
bootstrap/golang-dev.sh, bootstrap/dotnet-dev.sh, bootstrap/android-dev.sh and bootstrap/godot-dev.sh
Each script runs inside its matching container and accepts --dry-run and
--skip-skills.
| Script | Toolchain state | Verification |
|---|---|---|
golang-dev.sh |
Go from the distribution package; dependencies stay in go.mod and go.sum |
./verify.sh --only 28 |
dotnet-dev.sh |
The .NET SDK pinned in dotnet-sdk.env; target frameworks stay in project files and global.json |
./verify.sh --only 29 |
android-dev.sh |
JDK 25, Android command-line tools, Android Emulator and the SDK packages in android-dev.env |
./verify.sh --only 27 |
godot-dev.sh |
The Godot engine .NET build pinned in godot-dev.env, plus the .NET SDK pinned in dotnet-sdk.env; project settings stay in project.godot and the project files |
./verify.sh --only 21 |
All four also install the agent CLIs, shared agent configuration, third-party skills, client preferences and status line. The environment-specific manifests remain the source of truth.
dotnet-dev.sh and godot-dev.sh install the same .NET SDK, from
manifests/dotnet-sdk.env, through ensure_dotnet_sdk in
bootstrap/lib/dotnet.sh. The distribution package carries feature band 1xx
only, and no global.json rollForward policy moves down a band, so a
repository that pins another band needs the upstream SDK.
docs/dotnet-dev.md says why.
bootstrap/web-dev.sh installs the agentq runtime. The other environment
bootstraps do not. The coordinator is installed in one environment, which
manifests/agentqueue.env names. docs/agentq.md says why.
An environment is a module under components/. The router, the manifests and
the host bootstrap are already generic: they discover the modules, and they
name no environment. docs/environments.md holds the full procedure, and
dotnet-dev already exists as a module.
To make it real:
# 1. Describe the container.
cp distrobox/web-dev.ini distrobox/dotnet-dev.ini # then edit image, home, packages
distrobox assemble create --file distrobox/dotnet-dev.ini
# 2. Describe the environment for the router.
devbox new-env dotnet-dev --box dotnet-dev --workspace ~/projects:/workspace
mv <generated file> config/devbox-router/environments.d/dotnet-dev.env
# 3. Declare both in the module, and set "status": "supported".
$EDITOR components/dotnet-dev/component.json
# 4. Nothing else. components/dotnet-dev/inference.tsv already routes global.json.
./verify.sh --only 17bootstrap/host.sh then creates the container, and the router installation
links the environment file, because both discover what the modules declare.