Skip to content

fix: hide the Windows console for the tool-update service - #14344

Merged
jdx merged 1 commit into
mainfrom
claude/github-issue-14313-7e25f6
Oct 12, 2026
Merged

jdx merged 1 commit into
mainfrom
claude/github-issue-14313-7e25f6

Conversation

@jdx

@jdx jdx commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

https://entire.io/gh/jdx/mise/trails/668

On Windows, the built-in tool-update service opened a terminal window that stayed on screen, and closing it stopped the updater. The Scheduled Task runs mise.exe __tool-update --watch, which was never classified as unattended, so it kept the console Windows hands a task's console program.

[bootstrap.services.mise-tool-update]
builtin = "tool-update"

tool-update --watch is now treated like the dotfiles watcher, so its console is hidden right after parsing. mise tool-update <tool> still keeps its console so errors can be shown.

I only verified the classification on Linux (extended only_a_starting_watcher_runs_unattended); I haven't confirmed the window disappears on Windows 11. Discussion #13426 reports the history watcher still showing a window on build 26200.9457, so that may need separate work.

Fixes #14313

AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5; version: 2.1.295.

🤖 Generated with Claude Code


Note

Low Risk
Small CLI classification change on Windows console behavior; interactive tool-update and non-watch paths are explicitly excluded by tests.

Overview
On Windows, the mise __tool-update --watch bootstrap/Scheduled Task path is now classified as unattended, so the CLI hides its console right after parse (same policy as the dotfiles watcher and bootstrap service launcher). That stops the updater window from staying on screen and from being closed in a way that kills the service.

ToolUpdate::is_watch() exposes whether --watch is set, and Commands::runs_unattended delegates to it. Interactive __tool-update <tool> invocations are unchanged and still keep a console for errors. Tests lock in that only --watch is unattended.

Reviewed by Cursor Bugbot for commit 6487ecb. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Tool updates started in watch mode are now treated as unattended; regular tool updates remain interactive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4S20NQP34BZPMSK8VW7DRBX
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f174a95b-c00f-4d0d-a560-a7b64f9f8a59
📥 Commits

Reviewing files that changed from the base of the PR and between 623b640 and 6487ecb.

📒 Files selected for processing (2)
  • src/cli/mod.rs
  • src/cli/tool_update.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

📝 Walkthrough

Walkthrough

Watched ToolUpdate commands are now classified as unattended. The test covers both __tool-update --watch and __tool-update node.

Changes

Tool-update unattended classification

Layer / File(s) Summary
Watch-mode classification and coverage
src/cli/tool_update.rs, src/cli/mod.rs
ToolUpdate::is_watch returns the watch setting. runs_unattended uses it to classify tool-update commands. The test checks that watch mode is unattended and a non-watch command is not.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~5 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: jambalaya56562

Merge Risk: ⚪ Minimal · up to 6487e

No actionable issue remains in the reviewed change. Windows 11 Scheduled Task console behavior still needs validation.

Pre-merge checks · Passed 5
Passed Passed checks (5 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: hiding the Windows console for the unattended tool-update service.
Linked Issues check Passed Issue #14313 requires the Windows tool-update service action, mise.exe __tool-update --watch, to run unattended. Commands::runs_unattended now delegates ToolUpdate classification to `ToolUpdat…
Out of Scope Changes check Passed The pull request changes only unattended classification, the ToolUpdate::is_watch() helper, and focused tests. These changes support issue #14313. The history watcher is not changed, which matches t…
Docstring Coverage Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Treats __tool-update --watch as unattended so the Windows updater service uses the existing console-hiding path.

  • The tool-update watcher now uses the unattended console path.

Reviews (1) · Last reviewed commit: "fix(task): hide the Windows console for ..." · Reviewed by Greptile

@jdx
jdx merged commit 1882c24 into main Oct 12, 2026
54 of 57 checks passed
@jdx
jdx deleted the claude/github-issue-14313-7e25f6 branch October 12, 2026 22:58
jdx pushed a commit that referenced this pull request Oct 13, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/640
<!-- entire-trail-link-end -->

### 🚀 Features

- **(bootstrap)** add apt repositories and PPAs with
[bootstrap.apt.sources] by @jdx in
[#14306](#14306)
- **(bootstrap)** embed apt source signing keys inline in .sources files
by @jdx in [#14348](#14348)
- **(bootstrap)** render templates in [bootstrap.repos] checkout paths
by @jdx in [#14359](#14359)
- **(bootstrap)** declare macOS login items by @jdx in
[#14354](#14354)
- **(bootstrap)** layer dnf packages with rpm-ostree on Fedora Atomic
hosts by @jdx in [#14357](#14357)
- **(bootstrap)** support run0 and doas for privilege elevation by @jdx
in [#14351](#14351)
- **(dotfiles)** show tracked entries that no variant selects in status
by @jdx in [#14266](#14266)
- **(dotfiles)** group mise dot status into sections by @jdx in
[#14361](#14361)
- **(github)** add oauth_copy_code to copy the device code to the
clipboard by @jdx in [#14269](#14269)
- **(github)** keep the native OAuth refresh token in a password manager
or keychain by @jdx in [#14360](#14360)
- **(http)** add libc() and avx2() template functions by @jdx in
[#14298](#14298)
- **(pypi)** read upload times from PEP 691 simple indexes by @jdx in
[#14352](#14352)
- **(rust)** download rustup-init from RUSTUP_UPDATE_ROOT by @jdx in
[#14356](#14356)

### 🐛 Bug Fixes

- **(aqua)** warn once for packages with no repo to list versions from
by @jdx in [#14336](#14336)
- **(backend)** keep decoded download file names inside the download
directory by @JamBalaya56562 in
[#14274](#14274)
- **(bootstrap)** support dnf package groups by @jdx in
[#14347](#14347)
- **(bootstrap)** don't print a stdin-only helper as a manual command by
@jdx in [#14350](#14350)
- **(bootstrap)** explain why dnf packages cannot install on ostree
hosts by @jdx in [#14353](#14353)
- **(brew)** install bottle etc files and run post_install_steps when
pouring by @jdx in [#14272](#14272)
- **(brew)** refuse to chown a prefix owned by another user by @jdx in
[#14345](#14345)
- **(brew)** preserve protected post-install copy destinations by @jdx
in [#14311](#14311)
- **(brew-cask)** support generate completions in evaluated casks by
@pmmmwh in [#14255](#14255)
- **(docker)** install libatomic1 in the Debian image so Node.js 26 runs
by @jdx in [#14303](#14303)
- **(dotfiles)** keep existing ciphertext when an encrypted file is
unchanged by @jdx in [#14300](#14300)
- **(dotfiles)** stop a newly selected profile from deleting its shared
file by @jdx in [#14275](#14275)
- **(dotfiles)** restore config entries when adopting under a symlinked
home by @jdx in [#14315](#14315)
- **(dotfiles)** show per-file diffs for directory copy entries in dot
diff by @jdx in [#14333](#14333)
- **(dotfiles)** apply mise's own config before planning the other
entries by @jdx in [#14339](#14339)
- **(go)** read an exact go line in go.mod as the toolchain version by
@jdx in [#14308](#14308)
- **(http)** retry artifact downloads started by mise exec and shims by
@jdx in [#14342](#14342)
- **(npm)** resolve unlocked latest to the npm version of a graph-locked
install by @jdx in [#14307](#14307)
- **(npm)** give unlocked requests their own install under the identity
layout by @jdx in [#14309](#14309)
- **(oci)** ignore host apt hooks when installing apt bootstrap packages
by @jdx in [#14296](#14296)
- **(packslip)** let a workflow list carry the signer pin across a
workflow move by @jdx in
[#14338](#14338)
- **(packslip)** keep a renamed project's lockfile signer under packslip
1.7 by @jdx in [#14362](#14362)
- **(prune)** keep a uv-locked pipx tool's installs for every Python by
@jdx in [#14335](#14335)
- **(prune)** keep versions of task-scoped tools by @jdx in
[#14334](#14334)
- **(schema)** accept [bootstrap.secrets] by @JamBalaya56562 in
[#14322](#14322)
- **(schema)** accept [bootstrap.groups] and [bootstrap.users] by
@JamBalaya56562 in [#14340](#14340)
- **(schema)** accept table forms for vars._.file and vars._.source by
@JamBalaya56562 in [#14383](#14383)
- **(shim)** keep other projects' command wrappers on reshim by @jdx in
[#14355](#14355)
- **(task)** bump gix so remote tasks work in git hooks by @any-victor
in [#14316](#14316)
- hide the Windows console for the tool-update service by @jdx in
[#14344](#14344)

### 🚜 Refactor

- replace serde_yaml with serde-saphyr by @jdx in
[#14328](#14328)

### 📚 Documentation

- **(bootstrap)** explain when to use --from versus --adopt by @jdx in
[#14301](#14301)
- **(bootstrap)** recommend fish = "auto" for fish shell activation by
@jdx in [#14304](#14304)
- **(dotfiles)** explain when to use sync, pull, and apply by @jdx in
[#14343](#14343)
- **(shims)** explain how to check whether a tool is installed by @jdx
in [#14337](#14337)
- switch body text to DM Sans and tighten the releases page by @jdx in
[#14302](#14302)
- self-host DM Sans and JetBrains Mono so they load on mise.jdx.dev by
@jdx in [#14310](#14310)

### 🧪 Testing

- **(bootstrap)** don't depend on bc being absent in the apt sources
test by @jdx in [#14371](#14371)

### 📦️ Dependency Updates

- install Homebrew uses_from_macos dependencies on Linux by @itskyf in
[#14288](#14288)
- bump packslip to 1.7.0 by @jdx in
[#14363](#14363)
- lock file maintenance by @renovate[bot] in
[#14320](#14320)
- update github actions by @renovate[bot] in
[#14380](#14380)
- update dependency python to v3.14.8 by @renovate[bot] in
[#14381](#14381)
- update rust crates by @renovate[bot] in
[#14382](#14382)
- update docker images by @renovate[bot] in
[#14379](#14379)

### 📦 Registry

- install opencode 2 from npm by @jdx in
[#14295](#14295)

### Chore

- **(ci)** switch to jactionlint v2 and drop zizmor by @jdx in
[#14256](#14256)
- fix CI for the aqua registry update and the unsafe-libyaml advisory by
@jdx in [#14326](#14326)

### Ci

- double the unit-macos job timeout by @jdx in
[#14346](#14346)

### Security

- **(config)** require trust for path: tool versions by @jdx in
[#14358](#14358)

### New Contributors

- @pmmmwh made their first contribution in
[#14255](#14255)
- @itskyf made their first contribution in
[#14288](#14288)

## 📦 Aqua Registry Updates

### New Packages (3)

- [`Alice39s/xmtr`](https://github.com/Alice39s/xmtr)
-
[`bufbuild/protoschema-plugins/protoc-gen-jsonschema`](https://github.com/bufbuild/protoschema-plugins)
- [`takumin/qemu-bin`](https://github.com/takumin/qemu-bin)

### Updated Packages (3)

- [`bootandy/dust`](https://github.com/bootandy/dust)
-
[`connectrpc/connect-go/protoc-gen-connect-go`](https://github.com/connectrpc/connect-go)
- [`sharkdp/hyperfine`](https://github.com/sharkdp/hyperfine)
jdx pushed a commit that referenced this pull request Nov 2, 2026
…5265)

mise's built-in Windows background services and environment-service
launcher should not depend on keeping a Windows Terminal window open. On
Windows with `AllocConsoleWithOptions`, this change releases their
private Terminal console after startup and keeps the service running in
an inheritable windowless console. Shared interactive shells are left
alone.

Fixes #14313 on the supported API path; older-Windows limitations are
described below.

## Reproduction and root cause

With Windows Terminal as the default terminal, declare the updater in
your global mise config and run `mise bootstrap services apply`:

```toml
[bootstrap.services.mise-tool-update]
builtin = "tool-update"
```

Task Scheduler starts the persistent `mise __tool-update --watch`
process in its own console. Before this change, its Terminal window
remains open, and closing it terminates the watcher.

For a pseudoconsole,
[`GetConsoleWindow`](https://learn.microsoft.com/en-us/windows/console/getconsolewindow)
returns a message-only HWND, **not the visible Terminal window**.
`ShowWindow(..., SW_HIDE)` therefore hides the wrong window.

- #14344 (`1882c24b1`) correctly classified `__tool-update --watch` as
unattended, but retained that existing hiding mechanism.
- The related #14995 report, moved to [discussion
#14997](#14997), concerned
prompt-started update passes. #15038 (`6b5e5a870`) adds
`CREATE_NO_WINDOW` for those child passes. It does not retroactively
change the console already owned by the persistent watcher or
environment-service launcher.

## Fix

Keep the existing unattended-startup hook and sole-console-client guard.
For a private pseudoconsole, dynamically resolve
[`AllocConsoleWithOptions`](https://learn.microsoft.com/en-us/windows/console/allocconsolewithoptions),
detach from the old console, and allocate an inheritable windowless
replacement **in the same process**. Ordinary console children can
inherit it rather than opening another window.

Preserve file/pipe redirection and console code pages, replace
invalidated console standard handles with NUL, and perform console
replacement before registering signal listeners. Propagate
allocation/restoration failures instead of continuing without a
replacement console. Task Scheduler still tracks the original PID;
service job supervision and full-width exit status are unchanged.

## Compatibility and design tradeoff

- The replacement API requires Windows 11 24H2 / Windows Server 2025 or
later. Dynamic lookup preserves startup compatibility; unavailable-API
systems retain the old hiding behavior, which can still leave Windows
Terminal visible. Classic Console Host continues using `ShowWindow`.
- **This is not a zero-startup-window fix.** A window can appear before
mise reaches the existing hook. In earlier debug-build stress testing,
desktop window-state sampling observed one scheduled start visible for
about 27 seconds under compilation load at the default low service
priority. That is not an optimized-release measurement or a duration
bound.
- Terminal's own keep-open policy can retain a disconnected tab. The
desktop regression requires automatic tab closing and application-title
changes; it does not claim policy-independent UI disappearance.

This intentionally changes the existing launch/supervision architecture
as little as possible. Eliminating startup visibility would instead
require changing the initial launch, for example a GUI-subsystem
launcher that creates mise with `CREATE_NO_WINDOW`. **If maintainers
prefer that broader rewrite, please feel free to reject this PR in its
favor.** I hope the diagnosis and reproducible regression here can help
a replacement PR meet that stricter goal.

No new user flags, settings, or migration steps are required. The
services guide documents startup visibility, retained tabs, and the
older-Windows fallback.

## CI prerequisite

The base lockfile also contained `pulldown-cmark 0.13.5`, which fails
`cargo deny` for
[RUSTSEC-2026-0433](https://rustsec.org/advisories/RUSTSEC-2026-0433). A
separate commit updates only that locked package and its checksum to
`0.13.6`; no manifest requirements or other dependencies change.

## Validation

- Strict workspace/all-features/all-targets Clippy and default-feature
Windows Clippy passed without lint exclusions; canonical `mise run
lint-fix` and scoped formatting/lint checks passed.
- Two Rust console unit tests and seven repository Windows Pester cases
passed, with no skips locally. The desktop regression starts real mise
suspended in an already visible private console, releases it after the
helper detaches, and checks real desktop windows while the watcher
remains alive. The release baseline 2026.10.24 failed the
persistent-visibility assertion; the patched debug build passes.
- Seven additional isolated lifecycle smoke cases passed:
Ctrl-C/Ctrl-Break, UTF-8 code pages, redirected JSON/stderr, scheduled
update passes and descendants, and service stop/restart/full-width exit
status. Hostile logging settings and post-Pester environment restoration
were also checked. No test tasks or watchers remained.
- Unavailable-export observer gating was exercised with a genuine failed
lookup; this is **not an older-Windows production-fallback run**. Older
Windows, Windows Server, classic Console Host, alternate architectures,
third-party hosts, and `closeOnExit: "never"` were not runtime-tested
locally.
- Documentation build (1,535 tests, 423 pages, links), rendered-page
inspection, and generated-index drift checks passed.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* On supported Windows versions, unattended services can replace a
private Windows Terminal console with a windowless console. Ordinary
console children inherit the replacement instead of opening additional
windows; an initial startup window can still appear.
* Shared interactive consoles remain visible, and Classic Console Host
windows continue to be hidden directly.
* **Bug Fixes**
* Console allocation and handle/code-page restoration failures are
propagated instead of continuing without a replacement console.
* **Documentation**
* Added details about Windows version support and cases where a terminal
window or disconnected tab may remain visible.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

*AI-assisted — Tool: oh-my-pi (omp); model: openai-codex/gpt-6.1-sol;
version: 18.15.0.*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: tool-update service opens a terminal window and stops when it is closed

1 participant