installer/windows: the GUI installer does not report success when it installed nothing (#1181) - #1199
Conversation
…installed nothing (#1181) WairedSetup ran `waired-agent.exe install` as a [Run] entry, and Inno discards a [Run] entry's result. On a host where Smart App Control refused that program the Inno log recorded `CreateProcess failed; code 4551.` twice, Setup carried on, `waired.exe claude enable` ran anyway, and the wizard finished with exit code 0. The computer was left with no service and a Claude Code pointed at a gateway that would never listen: every turn failed with `API Error: Unable to connect to API (ConnectionRefused)`. The ruling this breaks is already on file — "インストールでも更新でも、 waired.exe か waired-agent.exe のどちらかがこの機で実行できないなら、 そこで止める" (docs/decisions/20260829/1730). install.ps1 implements it (#1153); the GUI installer never did, and update.mdx already promised "A fresh install behaves the same way". Where a failure can still fail an Inno installation turned out to be one place, not three. Measured against the Inno Setup 6 sources and on a Windows 11 host: [Run] results are discarded, ssPostInstall exceptions are handled and stepped over, and Before/AfterInstall exceptions are swallowed on purpose ("Don't allow exceptions raised by Before/AfterInstall functions to be propagated out"). Only PrepareToInstall can decline — exit code 7, before anything is created, stopped or replaced, and silent-safe. So everything that can fail now happens there: - the three programs are embedded once (dontcopy), extracted, and RUN from a staging directory under the install directory before anything is touched, with the same table install.ps1 uses; - waired-agent.exe is placed by Setup itself and its service registered, started and confirmed Running — that is the last moment Setup can still decline, so it cannot be left to [Files]; - an upgrade keeps the waired-agent.exe it replaces and puts it back, restarting the service, when the new one will not come up; - Claude Code is touched last, and only with a running daemon. SetupLogging=yes so a failed install always leaves a log — #1181 was only diagnosable because that run happened to have one. Verified on real hardware (Windows 11, Inno Setup 6.7.3), four paths: a fresh install whose agent will not start and one whose service will not come up both exit 7 with an empty %ProgramFiles%\Waired, no service, no registry key and Claude Code untouched; a good fresh install exits 0 with the service already Running when the installer returns; an upgrade whose new agent will not start exits 7 with the service Running and waired-agent.exe byte-for-byte the one that was there. Tests: scripts/install/waired_setup_iss_test.go is the first check that reads the .iss at all — it pins what Setup executes, that nothing hangs off a hook that cannot fail, and holds the pre-flight table against install.ps1's. The install test's ExeVariant leg gains the three behaviours above, driven by deliberately broken payloads because a policy refusal cannot be produced on demand. packaging/windows/ joins docs-surface-guard's list: the GUI installer was the one install path outside it. Fixes #1181 Refs waired-ai/waired#1312 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2 Signed-off-by: gen16k <gen16k@users.noreply.github.com>
|
📘 Docs preview — the preview channel for this PR has been deleted now that it is closed. |
… again - The install test compared managed-settings.json against a copy taken before the uninstall that removes it, so the two fresh-install cases would have failed on a correct installer. Re-read the baseline after the uninstall, where "unchanged" means something else. - The .iss guard read the call sites but not the wiring: dropping `SetUpTheService()` out of PrepareToInstall left the functions in the file, the script compiling, and every assertion green. Pin the two calls PrepareToInstall has to make. Refs #1181 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2 Signed-off-by: gen16k <gen16k@users.noreply.github.com>
…ith what start reported The Windows install test caught the gap on its first run. `waired-agent.exe start` waits for Running and exits non-zero otherwise, so its exit code should be the answer — but the installer does not know that the program it just placed is really waired-agent. With `where.exe` standing in for it, the runner's PATH had something matching `start`, `where.exe start` exited 0, and Setup reported a successful upgrade over a service that was Stopped. The same stand-in exits 1 on a developer machine: where.exe's exit code depends on what is on PATH. So ask the SCM instead of trusting the report: `sc.exe interrogate waired-agent` answers 0 when the service is running, 1062 when it is registered but stopped, 1060 when it is not registered — exit codes only, so nothing parses localised `sc query` output. Measured with a Go stub that exits 0 for every argument, so `install` and `start` both "succeed": Setup now stops with "the service is registered but is not running", exits 7, and puts the previous waired-agent.exe back with the service Running (hash unchanged). The remaining Windows failures in that run were downstream of this one: the upgrade left where.exe as waired-agent.exe, so the uninstaller's `waired-agent.exe uninstall` never deregistered the service, the Inno uninstall timed out waiting for it, and every later assert saw a service that should not have existed. main's Windows leg has no failures. Refs #1181 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2 Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Seventeen asserts that always run were added with the GUI installer's refusal cases, so the old floor of 139 no longer means what it says: a block could stop executing and the leg would still clear it. Measured, not estimated, from the green run 33786027609 of this exact configuration (-Tier 2 -Contract -ExeVariant), which executed 187 with no failures. 156 rather than 187 because the floor is a minimum every green run must clear, not a pin on today's total. Also record in the SAC signing inventory that the GUI installer now runs its programs from the staging directory too, and why that keeps the ledger's keys under ProgramFiles/ rather than growing a Temp/ row. Refs #1181 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2 Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Addendum — what CI's first run of this changed, and the measured floorCI found a hole in the change itself, and closing it made the installer better. The Windows leg's stand-in for a broken agent is The fix is the distinction #1087 is already about — a report is not a state. Proven on real hardware with a Go stub that exits 0 for every argument, so Declined, previous binary back byte-for-byte, service running again. Assert floor: raised 139 → 156, measured from the green run All seventeen pass on the runner, including Follow-up filed: #1202 — the setup program ships on every release and every |
… it (#1202) `WairedSetup-<version>-x64.exe` is attached to every release and every edge prerelease, and it is a full install path: it elevates, registers the service, starts it, and turns on the Claude Code integration by default. The site described one route, the `iwr … | iex` line, and did not name the setup program anywhere until #1199. The gap had already produced two wrong promises. packaging/windows/ sat outside docs-surface-guard's surface list until #1199 added it, so the GUI installer's behaviour could change with no docs page obliged to keep up — which is how getting-started/update.mdx came to promise a fresh-install behaviour only install.ps1 had (#1181). And every release's notes tell people to "see SmartScreen note in docs" for a note that did not exist; SmartScreen appeared nowhere under docs-site/. The new section follows the order a person downloading it actually meets: no `latest/download/…` shortcut, because the filename carries the version; "Windows protected your PC" and the way through it, which is why the page still leads with the PowerShell line; the permission prompt; a wizard that does not ask where to install and has one checkbox; that it does not run `waired init`, so signing in is still ahead of you; the log it always writes; in-place upgrade and uninstall, including that the state directory is kept by default; and the unattended switches with exit 0 / exit 7. The checkbox is described rather than quoted: its on-screen text still promises a transparent fallback to the Anthropic API, which #1198 is retiring, and a verbatim quote would go stale the day that lands. Also widen the "What gets installed" Start Menu row — both installers create **Waired** and **Waired (CLI)**, and the table listed one. Fixes #1202 Refs #1181, #1199, waired-ai/waired#1312 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2
…the install directory is empty (#1181) (#1216) The refusal cases #1199 added assert that a declined install left nothing in %ProgramFiles%\Waired. They run after the good install has been uninstalled, and that uninstall's completion signal is the SERVICE disappearing — but Inno removes unins000.exe and unins000.dat after that, from the _iu*.tmp copy it re-spawned as. The one-shot Remove-Item sweep between the two could land inside that window, fail silently on the two files because they were still mapped, and leave them for the next case to report: FAIL will-not-run : left behind unins000.dat, unins000.exe in C:\Program Files\Waired Seen once on run 33793355483 (on an unrelated PR), green on a re-run and green on main — a flake in the harness, not a defect in the installer. Wait for the uninstaller process to be gone, then sweep until the directory is, and fail loudly if thirty seconds is not enough. The alternative — allow-listing the two files — would leave the cases asserting on a directory they had not established was empty, which is most of what they are for. Floor 156 -> 157 for the one assert this adds. Reported by the L99 session, whose PR the flake landed on. Refs #1181 Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2 Signed-off-by: gen16k <gen16k@users.noreply.github.com> Co-authored-by: gen16k <gen16k@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
What this is about
On a Windows host where Smart App Control refused
waired-agent.exe,WairedSetup-0.0.3-rc5-x64.exe /VERYSILENTrecordedCreateProcess failed; code 4551.twice in its log, carried on, ranwaired.exe claude enable, and finished as a successful install with exitcode 0. The host was left with no service and a Claude Code pointed at a
gateway that would never listen — every turn failed with
API Error: Unable to connect to API (ConnectionRefused).The ruling this breaks is already on file:
docs/decisions/20260829/1730-installer-refuses-programs-that-cannot-run.md(owner, 2026-08-29) — 「インストールでも更新でも、
waired.exeかwaired-agent.exeのどちらかがこの機で実行できないなら、そこで止める」.install.ps1implements it (#1153). The GUI installer never did, andgetting-started/update.mdxalready promised "A fresh install behaves thesame way: it stops rather than leaving a computer with programs that cannot
run."
Fixes #1181
Refs waired-ai/waired#1312 (L96)
What I found first: an Inno installation can only be declined in one place
I went in expecting to raise from a
[Files] AfterInstall. That does not work,and neither do the other two candidates. Measured against the Inno Setup 6
sources (
jrsoftware/issrc, tagis-6_7_3) and confirmed on a Windows 11 hostwith Inno Setup 6.7.3:
PrepareToInstallreturns a message[Files], before the service is stopped, before Claude Code is looked atSetup.WizardForm.pasClickThroughPages[Files]AfterInstall/BeforeInstallraisesSetup.MainFunc.pasNotifyInstallEntry[Run]entry failsSetup.MainForm.pasProcessRunEntriesCurStepChanged(ssPostInstall)raisesSetStep(ssPostInstall, True)handles it and carries onSetup.MainForm.pasSetStep[Files]CheckraisesCalcFilesSize, thenCopyFiles)Setup.Install.HelperFunc.pasI measured the
AfterInstallrow rather than trusting the reading: a buildthat raised there exited 0 with everything installed. The whole design
follows from that row, and it is written down in
docs/knowledges/20260904/0210-inno-can-only-decline-before-it-installs.md.Also measured, and load-bearing: Inno routes
Application.HandleExceptionthrough a suppressible box, so an exception cannot hang a
/VERYSILENT /SUPPRESSMSGBOXESrun (the waired#760 hazard does not applyhere); and rollback keeps files that existed before the install
(
utDeleteFile_ExistedBeforeInstall), so a restored binary survives it.What changed
packaging/windows/waired-setup.iss— everything that can fail now happensin
PrepareToInstall:dontcopy, extracted, copiedinto
{app}\.waired-stagingand actually run there before anything onthe computer is created, stopped or replaced. The table of what to ask each
one is
install.ps1'sGet-StagedBinaryChecks, unchanged:waired.exe version --json(exit code read) andwaired-agent.exe -hare fatal, theWaired app only warns. Staging sits under the install directory for the same
reason
install.ps1puts it there. The two Inno installs areexternalentries reading those extracted copies, so the setup executable carries one
copy, not two.
waired-agent.exeis placed by Setup itself, and its service registered,started and confirmed Running — inside
PrepareToInstall, because that isthe last moment Setup can still decline.
[UninstallDelete]removes it.waired-agent.exe startalready waits for Running and exits non-zerootherwise, so its exit code is the answer and no localised
sc queryoutputis parsed.
waired-agent.exeit replaces and puts it back,restarting the service, when the new one will not come up — the rollback half
of the ruling, for the case the pre-flight cannot cover (the verdict moves on
its own).
claude enablestep moved out of[Run]intossPostInstallbehindgAgentRunning, so it cannot be reached by a computer with no service. Whenit fails on its own it is not fatal — Claude Code keeps talking to
api.anthropic.com, which is where it was — but it is no longer silent.
SetupLogging=yes. WairedSetup: the GUI installer reports success and enables the Claude Code integration although the agent service was never installed — leaving Claude Code broken #1181 was only diagnosable because that run happened tohave a log; a person hitting this never passes
/LOG.AllowCancelDuringInstall=no(by then the service is up and the remainingwork is a few seconds of copying), and the dead
WipeStatePagevariable isgone.
Tests.
scripts/install/waired_setup_iss_test.gois the first check in therepo that reads the
.issat all —install-script-lintis shellcheck,ps-script-lintcovers the.ps1files,ARGTESTisinstall.ps1-only. Itpins, by set equality: what Setup executes; that nothing hangs off a hook that
cannot fail; that the programs are embedded once and tried before they are
installed; that
claude enablesits behind the service check; and it holds thepre-flight table against
install.ps1's, because two copies of one tableforget different things. Each assertion was checked to fail on the defect it
names.
The install test's
-ExeVariantleg (every PR) gains three behaviours, drivenby deliberately broken payloads — a policy refusal cannot be produced on demand
— the same technique the #1087 asserts use for the update path:
waired-agent.exewill not run,where.exestand-in),waired-agent.exeis back byte-for-byte and the service Running.Those runs deliberately do not pass
/MERGETASKS=!claudeproxy, so "nomanaged-settings.jsonafterwards" means Setup stopped before the integrationrather than that the task was unchecked. They use a new build-time
/DNoCompressionswitch so a throwaway payload does not cost a minute oflzma2/ultra; shipping builds never pass it. Each run is bounded by
WaitForExit(180s)so a Setup stopped on a dialog fails this leg in threeminutes with a clear message instead of hanging it. The positive path now
asserts the service is already Running when the installer returns, instead
of starting it by hand — the harness comment claiming a fresh Inno install does
not start the service was stale.
scripts/ci/docs-surface-guard.sh:packaging/windows/joins the list.The GUI installer was the one install path outside it —
packaging/install/has been there from the start — and that gap is why
update.mdxcould promisea behaviour the GUI installer did not have with nothing failing.
Docs: a new "If Windows refuses to run a program" section on the Windows
install page (EN + ja), quoting the product output verbatim, which makes
update.mdx's "a fresh install behaves the same way" checkable.Verification on real hardware
Windows 11, Inno Setup 6.7.3, from a genuinely clean machine, all four paths:
waired-agent.exewill not run%ProgramFiles%\Wairedempty, no service, no registry key, no Start Menu, Claude Code untouchedwaired-agent.exeSetup had placedmanaged-settings.jsonwrittenwaired-agent.exebyte-for-byte the one that was there (9368638…),managed-settings.jsonunchanged, no.displaced-*leftTwo things worth recording from the same session:
unsigned
WairedSetup.exewas refused outright (CodeIntegrity 3077 + 3033 +3118, policy
{0283ac0f-…}), so the GUI installer would not start at allthere. That is why the functional verification ran on a host with SAC off and
why the CI cases use broken payloads. The event records went to installer(windows): find out what makes Smart App Control refuse a Waired program, and what makes the refusal lift #1191.
install.ps1'sChecking the new programs run on this computer before replacing anythingis now printed by the GUI installer too, so docs-site canquote one line for both.
Local:
gofmt,go build ./...,go test ./scripts/...,golangci-lint run --max-same-issues=0(0 issues),install-script-lint,docs-surface-guard(fires and is satisfied),
npm run i18n:check(32 pairs in sync).Known follow-up
The assert-count floor in
scripts/dev/installtest-windows.ps1(
$floor = if ($Contract) { 139 }) has to be re-measured from a green run ofthis branch rather than estimated; I will update it from the first CI run
before merging.
🤖 Generated with Claude Code
https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2