A robust bootstrapper and idempotent update utility for Fedora WSL environments. It orchestrates critical system installs and updates via DNF5 alongside optional updates for developer tools and AI CLIs: Volta/Node.js, uv (self-update and tool upgrades), GitHub CLI, Claude Code, and Codex.
Developer tooling and AI CLIs change frequently. Keeping everything current often resolves issues quickly and unlocks new features. On macOS, brew update && brew upgrade makes this easy. This project provides a similarly reliable workflow for Fedora on WSL.
- System Updates: Safe DNF5-based system package upgrades
- Baseline Bootstrap: Idempotent install of required Fedora packages
- Global Opt-In: All optional tools default to OFF; enable via profile, config, or CLI flags
- Code-First Tool Logic: Tool update behavior stays in code for clear review and predictable behavior
- Profiles + Selectors: Use
--profile,--only, and--skipto avoid skip-flag sprawl as tools grow - Persistent Defaults: Optional user config for default profile and
ENABLED_TOOLSallowlist orDISABLED_TOOLSdenylist - DNF Always-On:
dnfruns on every invocation unless explicitly--skip dnf - Idempotent: Safe to run repeatedly; no-op when already current
- Comprehensive Error Handling: Detailed exit codes, lockfile-based concurrency control, and remediation hints
- Dry-Run Mode: Preview commands before execution
- Strict Mode: Fail immediately on any optional step failure (CI-friendly)
- Detailed Logging: Timestamped logs with step-by-step progress tracking
This project is designed to be modified safely by AI agents. AGENTS.md gives an agent everything it needs to make correct changes without human supervision:
- Invariants — hard constraints that must never be violated (gate sequence, wrapper requirement, strict bash mode, etc.)
- Checklists — step-by-step task lists for the most common changes: add a tool, add a CLI flag, add a config key
- Decision criteria — rules for non-obvious choices (
step_skipvsstep_fail_optional, critical vs optional failure classification, profile membership) - Exit code reference — all defined exit codes to prevent collisions when adding new ones
- Verification commands — exact lint and smoke checks to run before committing
An AI agent with access to this repo can reliably add a new tool update function, add a CLI flag, or modify the config system — and verify the result — without asking for clarification.
./wsl-fedora-guest-tools [options]ln -sf "$(pwd)/wsl-fedora-guest-tools" "$HOME/.local/bin/wsl-fedora-guest-tools"Using $(pwd) ensures an absolute path in the symlink. After this, git pull automatically updates the installed command - no reinstall needed.
install -m 0755 ./wsl-fedora-guest-tools "$HOME/.local/bin/wsl-fedora-guest-tools"Copied files don't auto-update. After each git pull, re-run the install command above.
cd /path/to/wsl-fedora-guest-tools && git pullSymlink users (Option 2) are done - the command picks up changes immediately. Copy users (Option 3) must also re-run the install command.
# Run all updates (default profile: all)
./wsl-fedora-guest-tools
# Preview what would run (no changes made)
./wsl-fedora-guest-tools --dry-run
# Start from profile "dev" (dnf, volta, uv.self, uv.tools, gh)
./wsl-fedora-guest-tools --profile dev
# Run only specific tools
./wsl-fedora-guest-tools --only uv.self,claude
# Start from a profile, then remove selected tools
./wsl-fedora-guest-tools --profile all --skip codex,claude
# Show supported tool IDs and profile membership
./wsl-fedora-guest-tools --list-tools
# Fail on any error (useful for CI/CD)
./wsl-fedora-guest-tools --strict
# Override Fedora guard (use with caution)
./wsl-fedora-guest-tools --force| Flag | Description |
|---|---|
--dry-run |
Print commands that would execute without running them |
--force |
Skip Fedora OS guard (still logs detected OS info) |
--strict |
Treat optional tool failures as fatal |
--profile <name> |
Base selection profile: all, core, dev, or ai |
--only <tool_ids_csv> |
Replace the current selection with exactly these tool IDs |
--skip <tool_ids_csv> |
Remove these tool IDs from the current selection |
--list-tools |
Print supported tool IDs and profile membership, then exit |
--help, -h |
Display usage information |
| Tool ID | Description | Profiles |
|---|---|---|
dnf |
System update + baseline bootstrap via DNF | all, core, dev, ai |
volta |
Node.js update via Volta | all, dev |
uv.self |
uv self-update (uv self update) |
all, dev |
uv.tools |
uv tool upgrades (uv tool upgrade --all) |
all, dev |
gh |
GitHub CLI install/upgrade via DNF | all, dev |
claude |
Claude Code CLI update | all, ai |
codex |
Codex CLI update via Volta | all, ai |
| Profile | Tools |
|---|---|
all |
dnf,volta,uv.self,uv.tools,gh,claude,codex |
core |
dnf |
dev |
dnf,volta,uv.self,uv.tools,gh |
ai |
dnf,claude,codex |
Tool selection is resolved in this order:
- Start with profile from CLI
--profile; else configDEFAULT_PROFILE; elseall - Apply config:
ENABLED_TOOLS(allowlist: clear and enable only listed tools) orDISABLED_TOOLS(denylist: remove listed tools). These keys are mutually exclusive; setting both is a fatal error (exit 2). - If
--onlyis provided, replace selection with exactly--only - Apply CLI
--skip - Enforce DNF always-on:
dnfis re-added unless--skip dnfwas explicitly passed --only+--skipis valid; final set isonly - skip(plusdnfunless--skip dnf)
Notes:
- All optional tools default to OFF; a profile,
ENABLED_TOOLS,--profile, or--onlyis required to enable them dnfalways runs unless explicitly--skip dnf(infrastructure tool)- Unknown tool IDs in CLI args are fatal (
exit 2) - Unknown tool IDs in config are ignored with a warning
- Invalid config profile is ignored with a warning and defaults to
allunless CLI profile is provided
Optional user config path:
$XDG_CONFIG_HOME/wsl-fedora-guest-tools/config
If XDG_CONFIG_HOME is not set, the fallback path is:
$HOME/.config/wsl-fedora-guest-tools/config
If no config file exists and no CLI tool-selection flags (--profile, --only) are provided, the script prompts you interactively to choose a profile and writes a config file. This runs once (on first use) and only in a terminal session.
In non-interactive environments (CI, pipes), the script exits with an error and instructions to create the config file or pass CLI flags.
DEFAULT_PROFILE=all|core|dev|ai— base profile used when no--profileCLI flag is givenENABLED_TOOLS=tool_id_csv— allowlist: only the listed tool IDs are enabled (mutually exclusive withDISABLED_TOOLS)DISABLED_TOOLS=tool_id_csv— denylist: listed tools are removed from the profile selection (mutually exclusive withENABLED_TOOLS)
Setting both ENABLED_TOOLS and DISABLED_TOOLS in the same config file is a fatal error (exit 2).
Unknown keys are ignored with a warning.
# Opt-in allowlist: enable only specific tools
DEFAULT_PROFILE=all
ENABLED_TOOLS=volta,uv.self,uv.toolsWith this config, default runs select dnf,volta,uv.self,uv.tools (dnf is always-on).
# Denylist: start from dev profile, disable gh
DEFAULT_PROFILE=dev
DISABLED_TOOLS=ghWith this config, default runs select dnf,volta,uv.self,uv.tools.
- OS: Fedora (use
--forceto bypass on non-Fedora systems) - Privileges: Passwordless
sudorequired for DNF operations - Dependencies:
bashflock(fromutil-linux; used for lock-based concurrency control)dnf5
- Optional tools (for respective update steps):
volta(forvoltaandcodexsteps)uv(foruv.selfanduv.toolssteps)ghCLI (installed/managed by theghstep itself via DNF)claude(for theclaudestep)codex(for thecodexstep)
| Code | Meaning |
|---|---|
0 |
All steps completed successfully |
2 |
Invalid command-line arguments |
3 |
OS guard validation failed (not Fedora) |
4 |
Passwordless sudo not available |
5 |
Another instance is already running (lock file exists) |
6 |
Missing required command |
10 |
One or more optional steps failed (not in strict mode) |
The script runs these steps in order:
- Lock: Acquire lock file to prevent concurrent runs
- OS Guard: Validate Fedora base distro (skippable with
--force) - Sudo Check: Verify passwordless sudo is available
- System Update: DNF package upgrade (always-on unless
--skip dnf) - Baseline DNF Bootstrap: Install missing baseline packages (always-on unless
--skip dnf) - GitHub CLI Update: Install or upgrade
ghvia DNF with gh-cli repo setup (whenghis selected) - Volta Node Update: Optional Node.js update via Volta (when
voltais selected) - uv Self-Update: Optional uv self-update (when
uv.selfis selected) - uv Tool Upgrade: Optional uv tool upgrades (when
uv.toolsis selected) - Claude Update: Optional Claude Code CLI update (when
claudeis selected) - Codex Update: Optional Codex update via Volta (when
codexis selected)
- Safe to run multiple times
- Detects when tools are already up-to-date
- Does not reinstall unchanged versions
- Baseline DNF bootstrap installs only missing packages/repo configuration
- Optional tool failures do not stop execution unless
--strictis enabled
- Detailed error messages with remediation hints
- Captures command output for diagnosis
- Continues after optional tool failures in normal mode
- Prints a summary of all steps at completion
- Detects when uv self-updates are disabled (
uv.selfstep) - Provides guidance to upgrade uv via its original install method
uv.toolsstep runs independently regardless ofuv.selfoutcome
- Treats updater "up to date" output as success
- Falls back to version comparison checks
- Provides hints for common installation paths/workflows
- Requires both
voltaandcodexto be present - Skips update if either is missing
- Uses
volta install @openai/codex@latest - Compares before/after versions when available
The dnf tool is infrastructure and runs by default on every invocation. It can be disabled only by explicitly passing --skip dnf. Profile selection and ENABLED_TOOLS do not suppress dnf.
All optional tools default to OFF. To enable tools:
- Let the interactive first-run setup write a config file
- Create a config file manually with
DEFAULT_PROFILEand/orENABLED_TOOLS - Pass
--profileor--onlyon the command line
This ensures that users who configured the tool before new tools were added are never silently opted into tools they didn't request.
ENABLED_TOOLS is an allowlist: only the listed tool IDs run. DISABLED_TOOLS is a denylist: listed tools are removed from the base profile. These keys are mutually exclusive; using both is a fatal error (exit 2).
The gh tool manages the gh-cli DNF repository and installs or upgrades the gh package via dnf5. It is independent of the baseline bootstrap step. If gh is not selected, no DNF repo changes are made for gh-cli.
uv self-update (uv.self) and uv tool upgrades (uv.tools) are independent steps with separate tool IDs. This allows fine-grained control: for example, --only uv.tools runs only the tool upgrades without attempting the self-update.
This project intentionally keeps tool behavior in code. For any new tool PR:
- Add a canonical tool ID plus description/profile metadata in the script registry
- Implement idempotent update behavior (pre-check or post-check no-op safety)
- Ensure selector compatibility (
--profile,--only,--skip) - Keep dry-run output parity for the new step
- Classify failures correctly (
criticalvsoptional) and include remediation hints - Update README sections:
- Tool IDs and Profiles
- Examples (if applicable)
- Any behavior notes specific to the tool
GitHub Actions runs static quality checks and smoke tests on a matrix of supported Fedora versions (currently Fedora 42 and 43).
Triggers:
- Every PR
- Every push to
main - Weekly scheduled run (Monday 07:13 UTC)
- Manual runs via
workflow_dispatch
Checks:
bash -nsyntax validationshellchecklintingshfmt -dformat checks--help,--list-tools, and dry-run smoke scenarios
Practices:
- All third-party actions are pinned to SHA digests for supply-chain security
Automated failure alerts:
For scheduled runs, a notify-scheduled-failure job creates (or updates) a
GitHub issue titled Scheduled CI failure detected (labeled ci-scheduled)
when CI fails, then automatically closes it on the next successful scheduled
run.
Note: Fedora WSL images often configure this by default.
Configure passwordless sudo via sudo visudo:
your_username ALL=(ALL) NOPASSWD: ALLIf a previous run crashed, verify no active process is holding the lock and remove stale lock file if needed:
rm /tmp/wsl-fedora-guest-tools.lockThe lock is usually located at $XDG_RUNTIME_DIR/wsl-fedora-guest-tools.lock or /tmp/wsl-fedora-guest-tools.lock.
- In normal mode, optional failures do not prevent remaining steps from running
- Normal mode exits
10if one or more optional steps fail - Use
--strictto fail immediately on first optional failure - Check the final summary to identify failing steps and apply remediation hints