Skip to content

Windows PowerShell Lane Refactor Plan

Status: draft for further broadening.

Summary

Refactor the Windows lane around three locked decisions:

  • PowerShell profiles must not live in OneDrive. Use Scoop-owned PowerShell's $PSHOME/persist profile files as the managed loader, and remove only repo-owned OneDrive CurrentUser stubs.
  • Use normal interactive mise activate pwsh, not --shims, with Scoop owning the mise binary and mise owning versioned developer and prompt tools.
  • Replace the current group-first windows/scoop/apps.json model with split, tool-first JSON inventory files that drive Windows bootstrap, upgrade, docs, and generated mise TOML.
  • Add a lane-aware daily update workflow that can be run from any directory and can cheaply prompt when the host has not checked updates recently.
  • Add managed DNS as an opt-in personal-machine baseline that applies during bootstrap when the machine profile declares it.
  • Add a personal desktop baseline for selected OS settings beyond classic dotfiles: file manager visibility/view mode, clipboard history, default app posture, 1Password GUI install, repo-managed startup entries, power settings, and search indexing exclusions.

Key Changes

PowerShell Startup

  • Add managed Windows-only files for ~/scoop/persist/pwsh/profile.ps1 and ~/scoop/persist/pwsh/Microsoft.PowerShell_profile.ps1; both load ~/.config/powershell/profile.ps1 through the existing global guard.
  • Stop installing CurrentUser $PROFILE stubs under Documents/OneDrive. Replace the current profile installer script with a cleanup script that:
    • deletes $PROFILE.CurrentUserAllHosts and $PROFILE.CurrentUserCurrentHost only when they exactly match the old repo stub,
    • writes timestamped backups for non-managed content,
    • warns when custom CurrentUser profile content remains.
  • Keep Windows Terminal and Zed on ordinary pwsh -NoLogo; do not use launcher-only profile loading as the primary path.
  • Update docs and decisions to state that Scoop pwsh is the Windows profile host and OneDrive-backed CurrentUser profiles are not repo-managed.

Mise And Prompt Performance

  • Change PowerShell startup from mise activate -q --shims pwsh to normal mise activate -q pwsh; never mix full activation and --shims in the profile.
  • Keep Scoop as owner of the mise executable; forbid mise as a mise-managed global tool in validation.
  • Remove manual PATH priority that prepends mise shims. Preserve ~/.local/bin, Scoop mise, Git for Windows, Scoop pwsh, and Scoop shims, while letting full mise activation expose active tool install bins.
  • Set MISE_BASH_PATH to standard Git for Windows Bash before activation and set MISE_NOT_FOUND_AUTO_INSTALL=false in the managed PowerShell profile unless a local override changes it.
  • Add a Windows dotfiles-refresh-powershell-init script that resolves real tool binaries through mise which, generates cached snippets for zoxide, starship, and atuin, validates/parses them, and atomically updates ~/.cache/dotfiles/powershell/init.d.
  • Make normal profile startup source cached snippets only. Failed or missing snippet generation must not block prompt display.
  • Add a Windows just refresh-shell-cache recipe and call the refresh script after Windows mise install/upgrade paths.

Daily Update Workflow

  • Add one repo-owned command, dotfiles-update, available in every lane and safe to run from any directory. It must locate the dotfiles source through chezmoi source-path first, then a documented local override if needed.
  • Support three modes:
    • dotfiles-update preview: show pending updates across the lane's package managers without applying them.
    • dotfiles-update apply: update all repo-managed unpinned tools and apps across the lane's package managers, then refresh shell caches and run the lane's fast smoke check.
    • dotfiles-update maybe: perform no network or package-manager work; read the local update timestamp and print a concise prompt only when the last successful preview/apply is older than 24 hours.
  • Store update state under ${XDG_STATE_HOME:-~/.local/state}/dotfiles on Unix-like lanes and ~\.local\state\dotfiles on native Windows.
  • Wire the shell startup prompt as a cheap state-file check only. Shell startup must never run package-manager, network, mise outdated, or winget work.
  • Reuse the existing lane upgrade scripts as the execution backend rather than creating a second updater. The daily command is the user-facing wrapper around preview/apply behavior.
  • Treat Codex and other high-churn tools as eager-latest mise tools unless the inventory marks them pinned or blocked.

Managed DNS Baseline

  • Add DNS machine config, for example:
    • dns.manage = true
    • dns.provider = "cloudflare"
    • optional lane-specific adapter or connection allowlists.
  • Treat DNS as part of this personal workstation baseline when enabled. It is not silent generic-dotfiles behavior; it only applies when the profile opts in.
  • Define Cloudflare provider data in repo source:
    • IPv4: 1.1.1.1, 1.0.0.1
    • IPv6: 2606:4700:4700::1111, 2606:4700:4700::1001
  • Windows behavior:
    • apply only to selected active physical adapters by default;
    • skip VPN, virtual, Hyper-V, WSL, Docker, loopback, tunnel, and disconnected adapters unless explicitly configured;
    • reset must restore DHCP/automatic DNS with Set-DnsClientServerAddress -ResetServerAddresses;
    • missing admin rights must fail cleanly and print the exact elevated command.
  • WSL behavior:
    • do not independently manage /etc/resolv.conf by default;
    • prefer generated/inherited/tunnelled Windows DNS;
    • provide status and doctor guidance when WSL DNS does not reflect the Windows baseline.
  • CachyOS/Linux behavior:
    • use NetworkManager connection-profile DNS settings when NetworkManager owns the active connection;
    • reset must restore automatic DNS for the managed connection;
    • if NetworkManager is absent or does not own the connection, report unsupported/manual rather than guessing.
  • DNS changes must be idempotent. If current DNS already matches the configured provider, no-op.
  • Validate target selection before changing anything. If verification fails after a change, attempt to reset/restore the affected target and report the exact remaining state.

Personal Desktop Baseline

  • Keep personal desktop baseline configuration in the machine-specific chezmoi data, not a separate config file. The source template remains home/.chezmoi.toml.tmpl, and each host's effective config remains ~/.config/chezmoi/chezmoi.toml.
  • Add baseline config, for example:
    • desktop_baseline.manage = true
    • desktop_baseline.browser = "chrome"
    • desktop_baseline.browser_backup = "firefox"
    • per-area booleans for file_manager, clipboard_history, default_apps, password_manager, startup_entries, power, and search_index.
  • File manager baseline:
    • Windows: show file extensions, show hidden files, and set Explorer's default folder view to details where stable registry-backed settings exist.
    • CachyOS KDE: manage Dolphin to show hidden files and use details view if the relevant dolphinrc keys are stable; otherwise report drift through doctor.
    • WSL: no GUI file-manager mutation by default.
  • Clipboard history:
    • Windows: enable clipboard history when configured.
    • CachyOS/Linux: status-only for now unless a specific KDE clipboard-history setting proves stable.
    • WSL: no independent clipboard-history management.
  • Default apps:
    • Set Chrome as the preferred browser and Firefox as backup in repo data.
    • CachyOS: apply browser, editor, file manager, PDF, text, and URL handler defaults through mimeapps.list; update the current Firefox default to Chrome when Chrome is installed.
    • Windows: install browsers and report default-app drift. Use SetUserFTA for repo-enabled current-user associations when the tool is installed and explicitly configured. Do not hand-write protected UserChoice registry hash data. Open the relevant Settings page and print manual instructions when Windows blocks reliable mutation.
  • Browser baseline:
    • Install Chrome as the primary browser and Firefox as backup on Windows and CachyOS.
    • Do not manage browser extensions; browser profiles and sync own extension state.
  • Password manager:
    • Windows: winget owns 1Password GUI via AgileBits.1Password; keep 1Password CLI as a separate CLI tool where needed.
    • CachyOS: use the Arch/AUR 1password package when an AUR helper is available; fall back to documented manual install if no AUR helper exists.
    • WSL: do not install a separate Linux GUI 1Password app. Keep WSL focused on CLI/status guidance and use the Windows GUI app as the desktop app.
    • Never configure vaults, accounts, credentials, or browser extension sign-in.
  • Startup app allow/deny:
    • Manage only startup entries created by this repo.
    • doctor may show independent startup entries in a separate informational section, but must not mutate them.
    • Provide preview before enabling/disabling any repo-managed startup entry.
  • Power settings:
    • Manage only explicit machine-profile settings such as AC/battery sleep, display timeout, hibernate, and lid behavior.
    • Windows uses powercfg/PowerShell helpers; CachyOS uses KDE/systemd/logind settings only where the owner is clear.
    • Missing elevation must fail cleanly with the exact rerun command.
  • Search indexing exclusions:
    • Windows: exclude known high-churn repo/dev/cache paths such as code roots, .git, node_modules, mise install/cache paths, Codex cache/session/log paths, and other configured paths when Windows Search indexing is present.
    • CachyOS KDE: manage Baloo exclusions only if Baloo is active and config keys are stable; otherwise report guidance through doctor.
    • WSL: no independent search-index management.
  • Backup/sync clients remain out of scope for automation in this phase. The repo may install a client later, but account sign-in, selected folders, bandwidth policy, and cloud credentials stay manual unless a concrete client has safe declarative config.

Windows Default Apps And Open With

  • Keep this as a Windows machine-baseline module, not normal shell/dotfile startup. Proposed source layout:
    • windows/default-apps/README.md
    • windows/default-apps/associations.json
    • windows/default-apps/open-with.json
    • scripts/windows/default-apps.ps1
    • scripts/windows/open-with.ps1
  • Recommended implementation strategy:
    • start with read-only status, export, and doctor commands before any mutation;
    • add allowlisted app registration/Open With cleanup next, because that is ordinary current-user registry state and does not need UserChoice hash writes;
    • add SetUserFTA-backed default association apply/reset only after target apps are installed, registered, backed up, and discoverable;
    • keep browser defaults as the last apply path because http, https, .htm, and .html are the most likely to be reset or blocked by Windows;
    • treat missing SetUserFTA, missing ProgIDs, and missing apps as warnings in bootstrap and hard failures only for direct default-apps apply --strict.
  • Recommended initial target policy:
    • browser: Chrome primary, Firefox backup;
    • PDF: SumatraPDF;
    • archives: 7-Zip;
    • images: IrfanView with Paint/Firefox/Chrome allowed in Open With;
    • media: VLC, with mpv available but not necessarily default;
    • text/dev/config/Markdown: configurable editor target, with Zed preferred when it has a valid ProgID and VS Code/Notepad++ as Open With fallbacks.
  • Model Windows association mechanisms explicitly:
    • UserChoice is the protected per-user default association. Read it for status, but never write ProgId or Hash manually.
    • ProgID names the registered handler that SetUserFTA or Windows Settings can select.
    • OpenWithProgids is the preferred per-extension way to make supported handlers appear in Open With.
    • OpenWithList is legacy MRU/noise state. Clean stale entries only when they point to missing repo-managed apps.
    • Applications\<exe> registers a desktop app executable, friendly name, icon, and open command for Open With.
    • SupportedTypes and NoOpenWith can constrain noisy apps, but only for repo-owned registrations.
    • App Paths helps ShellExecute resolve real executables without relying on PATH; use current-user App Paths for repo-owned portable/Scoop GUI apps where needed.
    • DISM default-app XML is useful for export/reference and future image or first-logon work, but it is not the primary current-user mutation path.
  • Use SetUserFTA as the default association backend when default_apps.manage = true and default_apps.tool = "setuserfta".
    • Store SetUserFTA as a repo-managed direct/manual Windows tool until its licensing/distribution story is explicit.
    • Generate a temporary SetUserFTA config from repo JSON and apply it in one invocation.
    • Use SetUserFTA get, query, find, browser, -cmdline, and -validonly for discovery and verification.
    • Use SetUserFTA browser mode for Chrome/Firefox where it resolves a stable browser registration; otherwise set http, https, .htm, and .html by discovered ProgID.
    • Use SetUserFTA del only as an explicit reset action for configured associations. Warn that protected types such as .pdf, media/image types, and browser protocols may reset to Windows defaults after deletion.
  • Discover valid ProgIDs after app install, before applying defaults:
    • query SetUserFTA for current/default associations;
    • enumerate HKCU and HKLM Software\RegisteredApplications capability entries for browsers and GUI apps;
    • inspect HKCU\Software\Classes, HKLM\Software\Classes, and Applications\<exe> for configured app handlers;
    • read repo inventory metadata first when a Scoop/winget package has known ProgIDs.
  • Export known-good state through two channels:
    • SetUserFTA export for the selected extension/protocol list;
    • DISM /Online /Export-DefaultAppAssociations as a reference snapshot only. Keep host exports under local state/cache by default, not committed, unless manually curated into windows/default-apps/associations.json.
  • Apply order:
    • install/register target apps first;
    • backup current selected associations and repo-owned Open With registry keys;
    • validate that requested ProgIDs exist;
    • apply available SetUserFTA associations;
    • apply repo-owned Open With registration/cleanup;
    • verify and report drift without hard-failing missing apps.
  • Scoop registry notes are not automatically honored today. The current Windows bootstrap only runs scoop install/scoop update; it does not parse or import post-install registry note files such as VS Code install-associations.reg, Notepad++ install-context.reg, 7-Zip shell integration, PowerShell context entries, Windows Terminal context entries, Python PEP 514, or Tailscale startup notes.
  • Do not blindly import every Scoop-generated .reg file. Add explicit tool-inventory metadata for allowed registry integrations:
    • context_menu: import generated context-menu registration for Code, Notepad++, Windows Terminal, PowerShell, and 7-Zip only when enabled.
    • associations: import registration-only files that create ProgIDs and OpenWithProgids, but do not set protected UserChoice defaults.
    • startup: managed separately by the startup allow/deny module.
    • pep514: Python registration is disabled unless a Windows-native Python owner is intentionally added back to the inventory.
    • blocked: OneCommander file-manager takeover, global shell hijacks, and broad machine-wide imports stay manual.
  • Open With cleanup is current-user and repo-scoped:
    • register approved Scoop GUI apps under HKCU\Software\Classes\Applications\<exe> using real current app paths;
    • add configured extension links under OpenWithProgids;
    • remove dead Applications\<exe> entries and per-extension OpenWithList/OpenWithProgids references only when they match repo-managed apps whose executable no longer exists;
    • optionally add SupportedTypes for repo-registered apps to reduce noisy suggestions;
    • avoid mutating independent user-installed apps.
  • The existing local C:\Users\alvaro\Fix-OpenWithScoop.ps1 is a useful prototype for dry-run cleanup, dead application detection, and Scoop shortcut registration, but the repo implementation should be driven by explicit config instead of a broad shortcut scan.
  • Current observed Windows drift to address in this module:
    • http, https, and .html currently point at Firefox, while the desired baseline is Chrome-first;
    • some Scoop registry note effects are already present on the machine outside repo ownership, including Code, Notepad++, and 7-Zip context/Open With registration. The module should make that state declarative and reversible.

Tool Inventory Refactor

  • Create split JSON inventory files under tools/, each with a tools object keyed by canonical tool name.
  • Use this schema shape for every entry:
    • common metadata: description, tags, optional notes;
    • per-lane metadata: windows, wsl, cachyos;
    • per-lane fields: owner, package, version, groups, bucket, install, upgrade, detect, state, update_policy.
  • Owners must be explicit: scoop, winget, direct, mise, apt, pacman, aur, manual, or absent.
  • Update policies must be explicit where the default is not enough: latest, pinned, manual, blocked, or absent.
  • Delete windows/scoop/apps.json as an obsolete source path. Update bootstrap, upgrade, smoke tests, and docs to read the new tool-first JSON directly.
  • Generate or validate mise TOML from the JSON inventory so home/dot_config/mise/conf.d/*.toml is treated as derived, not hand-authored source.
  • Encode Windows bootstrap groups as metadata on tool entries: bootstrap_auth, core, runtime_build, gui, fonts, and optional.
  • Encode winget/direct special cases in the same inventory: Git for Windows, Visual Studio Build Tools, Zed, and Logi Options+ offline.
  • Encode Windows registration metadata on tool entries when needed: progids, app_paths, open_with_exe, scoop_registry_notes, context_menu, association_reg_files, startup_reg_files, and registration_policy. registration_policy must be one of apply, preview, manual, blocked, or absent.

Interfaces

  • scripts/bootstrap-windows.ps1 keeps the existing bootstrap intent but reads tool inventory JSON. Remove -RepairMiseShims; bootstrap should install required groups and refresh PowerShell init cache when mise/tooling is available.
  • Windows bootstrap should run the default-app/Open With module only after GUI apps are installed and only when desktop_baseline.default_apps or open_with.manage is enabled. Missing SetUserFTA or missing target ProgIDs warn and continue; they must not break core bootstrap.
  • scripts/upgrade-windows.ps1 reads the same inventory for managed Scoop/winget upgrades and de-owned/absent Scoop apps.
  • Add cross-lane dotfiles-update launchers under ~/.local/bin:
    • Unix-like lanes use an executable shell helper.
    • Native Windows uses .cmd and .ps1 wrappers.
    • All wrappers delegate to lane-specific preview/apply scripts and update the shared timestamp only after successful preview/apply.
  • Add cross-lane dotfiles-dns launchers under ~/.local/bin with:
    • dotfiles-dns status
    • dotfiles-dns preview
    • dotfiles-dns set
    • dotfiles-dns reset
    • dotfiles-dns doctor
  • Bootstrap scripts must call dotfiles-dns set when DNS management is enabled in the machine profile. If elevation is missing, bootstrap should print the exact elevated command and continue with a warning; dotfiles-dns set itself should fail hard.
  • Add cross-lane dotfiles-baseline launchers under ~/.local/bin with:
    • dotfiles-baseline status
    • dotfiles-baseline preview
    • dotfiles-baseline apply
    • dotfiles-baseline reset <area>
    • dotfiles-baseline doctor
  • Add Windows-only default-app/Open With commands under dotfiles-baseline:
    • dotfiles-baseline default-apps status
    • dotfiles-baseline default-apps export
    • dotfiles-baseline default-apps apply
    • dotfiles-baseline default-apps reset
    • dotfiles-baseline open-with status
    • dotfiles-baseline open-with backup
    • dotfiles-baseline open-with apply
    • dotfiles-baseline open-with reset
  • Bootstrap scripts should apply safe enabled baseline areas by default for opted-in personal profiles. Areas that are doctor-only in this phase must report guidance without mutating state.
  • Add scripts/validate-tool-inventory.py and scripts/generate-tool-inventory.py using only Python standard library.
  • Add or update just recipes:
    • refresh-shell-cache works on Windows and Unix-like lanes.
    • mise-policy-check also validates generated mise TOML sync.
    • upgrade remains the script-facing backend.
    • check includes tool inventory validation.

Test Plan

  • Source checks:
    • python3 scripts/validate-tool-inventory.py
    • python3 scripts/generate-tool-inventory.py --check
    • scripts/smoke-test.sh --source-only
    • just check
  • Daily update checks:
    • dotfiles-update maybe from $HOME, from the dotfiles repo, and from an unrelated directory.
    • dotfiles-update preview in each lane where the required package managers are available.
    • dotfiles-update apply on at least one host per lane before final rollout.
    • Verify pinned/manual/blocked tools are reported but not updated.
  • DNS checks:
    • dotfiles-dns status, preview, and doctor on each lane.
    • Windows: dry-run adapter selection excludes VPN, virtual, Hyper-V, WSL, Docker, loopback, tunnel, and disconnected adapters; elevated set/reset works on active physical adapters.
    • WSL: status reports generated /etc/resolv.conf and inherited DNS; set does not mutate WSL DNS by default.
    • CachyOS: NetworkManager preview names the connection profile before mutation; set/reset works on a NetworkManager-owned connection.
    • Missing admin/root privileges print exact rerun commands.
  • Desktop baseline checks:
    • dotfiles-baseline status, preview, and doctor on each GUI-capable lane.
    • Windows: Explorer hidden-file/file-extension/details-view settings are idempotent; clipboard history applies; 1Password GUI resolves from winget; startup mutation touches only repo-managed entries; search-index exclusions are previewed before apply.
    • CachyOS: mimeapps.list renders Chrome-first with Firefox backup; Dolphin, power, and Baloo settings apply only when stable owners/keys are verified.
    • WSL: GUI baseline reports inherited/host-owned behavior and does not install GUI 1Password or mutate Linux GUI settings by default.
    • Windows default-app drift for Chrome is reported with manual instructions if Windows blocks reliable programmatic mutation.
  • Windows default-app/Open With checks:
    • Fresh Windows install after GUI bootstrap: status reports missing SetUserFTA or missing ProgIDs as warnings; apply does not mutate until target apps are installed and registered.
    • Known-good export: SetUserFTA selected association export and DISM XML reference export both complete and are written to local state/cache.
    • Apply with SetUserFTA: configured unpinned/current-user associations apply for .pdf, text/dev/config files, archives, images, media, http, https, .htm, and .html; protected types verify by reading UserChoice rather than by assuming registry writes succeeded.
    • Browser defaults: Chrome becomes the target for http, https, .htm, and .html when a valid Chrome registration is discovered; Firefox remains installed as backup.
    • Scoop registry notes: explicitly allowlisted generated .reg files import cleanly after Scoop install/update, and blocked files are reported but not imported.
    • Open With cleanup: dead repo-managed Scoop app references are removed from Applications\<exe>, OpenWithList, and OpenWithProgids; independent user-installed app entries are only reported.
    • App update/path change: registrations point to Scoop current paths where safe and are refreshed after scoop update.
    • Missing app: missing ProgIDs, missing .reg files, and missing executables warn and continue without hard failure.
    • Reset: SetUserFTA reset uses explicit configured del actions only, and Open With reset restores the registry backup for repo-owned keys.
  • Windows runtime checks:
    • .\scripts\smoke-test-windows.ps1
    • pwsh -NoLogo -Command '$PSVersionTable.PSEdition; "profile-ok"' with a hard timeout
    • where.exe mise, where.exe starship, where.exe atuin, where.exe zoxide, where.exe bash
    • mise activate -q pwsh
    • dotfiles-refresh-powershell-init
    • confirm no runaway mise.exe or bash.exe processes after startup and refresh tests.
  • Smoke assertions must reject:
    • mise activate ... --shims pwsh in PowerShell sources,
    • mise shim directories being manually prepended,
    • repo-managed CurrentUser profile stubs under OneDrive,
    • stale references to windows/scoop/apps.json.
    • startup code that performs network or package-manager update work.
    • DNS mutation from shell startup or unmanaged profiles.
    • desktop baseline mutation from shell startup.

Assumptions

  • The Windows lane standardizes on Scoop-installed PowerShell 7; non-Scoop PowerShell hosts are not a compatibility target.
  • Existing OneDrive CurrentUser profile files may be removed only when they exactly match the repo-owned stub; custom profile content is backed up and left for manual review.
  • JSON is the canonical inventory format because PowerShell can parse it during fresh bootstrap without extra modules.
  • Daily update prompting is opt-out through local state/config, but real update execution is explicit; shell startup only prints a reminder.
  • DNS is managed by default only for personal profiles that opt in through repo config. Router-level DNS, encrypted DNS, DoH/DoT policy, and per-network split-DNS are out of scope for this phase.
  • Windows default-app association mutation depends on SetUserFTA being present and locally acceptable to use. Without it, the repo reports drift and opens Windows Settings guidance rather than spoofing UserChoice hashes.
  • Scoop-generated registry files are treated as upstream registration hints, not trusted policy. Only explicitly allowlisted current-user integrations are imported by repo code.
  • Browser extensions, backup/sync account configuration, mouse acceleration, and non-repo startup entries are out of scope for mutation in this phase.