Max Vilimpoc [Sun, 6 Sep 2026 08:14:46 +0000 (10:14 +0200)]
dotfiles: take the native arm64 rsync now that one is published
nuket/rsync-windows publishes an arm64 asset as of v3.5.0-gdeeda96f, so
ARM64 stops taking the x64 zip under emulation.
The rsync.exe was never the problem - a transfer is bounded by the
socket, not by emulated CPU. The ssh.exe beside it was: it links against
System32\libcrypto.dll, which on ARM64 is an ARM64 binary an x64 process
cannot load, so it died with 0xC0000135 (STATUS_DLL_NOT_FOUND) and the
verification step deleted it on every single run. Both binaries in the
arm64 zip are 0xAA64, and its ssh.exe reports
so the fast client survives the run now instead of being thrown away.
Checked the published .sha256 against the download the way the script
does; it matches.
Keep the run-it-to-verify check on every architecture. It is cheap, it
is what found the mismatch in the first place, and it still covers a
missing or too-old libcrypto.dll anywhere.
Drop rsync from the audit's accepted-emulation table and give it a
remedy instead: an x64 rsync.exe on an ARM64 box is now a leftover from
a run before this asset existed, not an exception to tolerate, so it
should warn and say to re-run the elevated half.
Also correct a stale README bullet claiming NASM is still installed on
ARM64 - it went into the x64-only set with the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt
Max Vilimpoc [Sun, 6 Sep 2026 06:32:10 +0000 (08:32 +0200)]
dotfiles: split provisioning between x64 and ARM64
The ARM64 machine is a small VM, and nearly everything it installed
without a native build ran under Prism emulation. Each exception was
defensible on its own; together they were a VM full of x64 binaries.
So the two architectures now provision different things on purpose. x64
is the full workstation. ARM64 gets MSVC and nothing that duplicates it
or exists only as an x64 binary: Brave, WinMerge, upstream LLVM, NASM,
Android GPU Inspector, OpenCppCoverage, BinSkim, Visual Studio's
clang-cl, and the WDK are all dropped there. :x64only in the batch file
is the single place the policy is applied, so every skip prints its
reason into the run log instead of leaving a silent gap.
Two of those are dropped by choice rather than limitation, and the
comments say so: a native ARM64 clang-cl is offered on Arm and worked
here before this change.
The WDK could not have installed anyway, which is worth recording. It
requires the matching Windows SDK revision, and revisions are not
side-by-side - Include\10.0.26100.0 is one directory whichever wrote it.
VS 2026 installs SDK 26100.7705; linkid=2335869 is WDK 26100.6584, the
kit paired with VS 2022. wdksetup.exe /quiet therefore aborts with exit
15605 and WER "WDK / 10.1.26100.6584 / Install / 0x80070642" - 0x642 is
1602, ERROR_INSTALL_USEREXIT, a silent install cancelling itself on a
failed prerequisite. Nothing to do with Arm; the same pairing fails on
x64.
Fix the architecture detection, which this change makes load-bearing.
Both sources the scripts trusted report the EMULATED architecture inside
an emulated x64 process - which is what `powershell` resolves to when
launched from an x64 shell on an ARM64 box. Measured there:
[RuntimeInformation]::OSArchitecture X64 wrong
%PROCESSOR_ARCHITECTURE% AMD64 wrong
HKLM\...\Session Manager\Environment ARM64 right
OSArchitecture is documented as the OS's architecture, and on .NET Core
it is; under .NET Framework on Prism it is not. All three scripts now
read the registry and keep the old sources as a fallback. This was a
mislabelled log line before; now it decides which packages install at
all. The batch parses reg.exe without piping to find/findstr - both are
shadowable, and a POSIX find turns the detection into a silent no-op
that leaves the emulated answer standing.
Stop invoking `py`, which failed with "'py' is not recognized" for two
independent reasons that look identical at the prompt. The session
inherits its PATH before winget installs Python, so nothing winget adds
is visible - the same staleness the DOTNET_EXE lookup already handles.
And the launcher may not exist: on this box every other Python component
registered while no launcher did, and the Launcher directory is on the
user PATH but absent. Resolve an interpreter by full path instead,
preferring the launcher only when it is real, and require python.exe
inside the directory - a half-removed version leaves the folder behind
with only Doc and Lib, which is what this box has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt
Max Vilimpoc [Sat, 5 Sep 2026 23:06:42 +0000 (01:06 +0200)]
dotfiles: detect that elevation is impossible instead of misreporting it
Running as a standard user on a box with UAC disabled produced no prompt
at all, and the script blamed a cancelled one.
The cause is that Start-Process -Verb RunAs does not report this. With
UAC off machine-wide (EnableLUA = 0) the RunAs verb is SILENTLY IGNORED:
no prompt appears, the child starts with the caller's own unelevated
token, and Start-Process returns exit code 0 as though it had worked.
Measured on the box: exit 0 from Start-Process, while the child reported
itself as the unelevated calling user. The elevated script then refuses
itself on its #Requires -RunAsAdministrator line, exits 1, and never
reaches Start-Transcript - so there is no log either. That is
indistinguishable from a declined prompt unless you know to look, which
is why the old message sent you hunting for a prompt that could never
have appeared.
So decide up front rather than trying and guessing afterwards. One check
at the top of setup-windows.bat classifies the account as ALREADY (run
the elevated half directly - asking to elevate what already is elevated
is pointless, and on a UAC-off box RunAs would be the no-op above),
PROMPT (request it, expect a consent or credential prompt), or NOLUA
(elevation is impossible; say so and skip). IsInRole(Administrator) is
false for an admin running unelevated under UAC, which is what makes
ALREADY mean actually elevated rather than merely capable.
The missing-log message now says which of the two things happened, using
that classification, rather than asserting a cancelled prompt.
Also stop running four installers that cannot succeed. The .NET SDK,
CMake, Android GPU Inspector and OpenCppCoverage sit in the "per-user"
section but are machine-wide installers, and on an account that cannot
elevate they failed on every run with codes that explain nothing - exit 5
(ERROR_ACCESS_DENIED), MSI 1603 twice, and exit 1. Grouped and skipped
with a single clear line when elevation is impossible. None of them
blocks a build: the .NET SDK is only here for the WiX MSI tooling, CMake
also ships inside Visual Studio, AGI profiles Android devices, and
OpenCppCoverage cannot instrument ARM64 binaries anyway.
A NOLUA run is now a success (exit 0) rather than a reported failure -
the per-user half did its job - and the closing ARM64 summary points at
the architecture audit for what actually landed, since that list
describes what the script provides rather than what this run achieved.
All three paths exercised: NOLUA skips and exits 0, ALREADY runs the
elevated half directly, PROMPT requests elevation and reports a declined
prompt correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt
Max Vilimpoc [Sat, 5 Sep 2026 22:48:57 +0000 (00:48 +0200)]
dotfiles: fix two architecture bugs a real ARM64 run exposed
Running setup-windows.bat end to end on an ARM64 box turned up two
packages that quietly did the wrong thing, and one bug in the audit that
was hiding a third.
WinMerge installed as x64 on ARM64. Upstream publishes the ARM64 build as
a MACHINE-scope installer and the x64 build as a PER-USER one, so an
unelevated winget lets its scope preference beat its architecture
preference and silently picks the emulated x64. Ask for --architecture
arm64 first and fall back to the default, so an administrator run gets the
native build and a standard-user run still gets a working diff tool.
LLVM does not fail when it cannot write to Program Files - it relocates.
The NSIS installer falls back to a per-user directory (observed at
%USERPROFILE%\Documents\LLVM) and winget still reports "Successfully
installed", so the compiler is genuinely present, genuinely native, and
findable by nothing. Its silent install also does not take the add-to-PATH
option. New LlvmPath step looks in that fallback as well as the usual
roots, reports the architecture, notes when the location means the install
was not machine-wide, and appends the directory to the user PATH.
Appended rather than prepended: this is a second compiler kept alongside
MSVC and should not quietly win a clang-cl meant for Visual Studio's copy.
The audit resolved labelled entries PATH-first, so with upstream LLVM on
the PATH the "clang-cl.exe (VS)" row answered with the upstream compiler
and reported Visual Studio's as present when it was absent. A labelled
entry names one specific copy and is now resolved only from its explicit
candidates; unlabelled entries still resolve PATH-first, because for those
the question is which binary a build would actually invoke.
Also record what a standard user cannot provision. The elevated half needs
an account that can elevate, and several packages in the non-elevated
section are per-user in name only: the .NET SDK (exit 5), CMake and
Android GPU Inspector (MSI 1603) and OpenCppCoverage (self-elevating) all
fail without an administrator.
Verified on ARM64: cmake, ninja, git, python, ssh, xperf, MSBuild and
cl.exe all native; both compilers build a native ARM64 binary through the
Ninja generator - MSVC 1950 and clang 22.1.8, each reporting target ARM64.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt
Max Vilimpoc [Sat, 5 Sep 2026 22:27:05 +0000 (00:27 +0200)]
dotfiles: pin the native ninja ahead of the one VS bundles
Visual Studio ships an x64 ninja.exe even on ARM64. That matters more
than a one-off emulated tool would, because ninja is re-invoked for every
edge in the build graph - it is the one place emulation is paid over and
over rather than once.
New NinjaPath step finds a native ninja (winget's package directory, or
one already on the PATH that is not the VS copy), checks its PE machine
type actually matches the host, and moves its directory to the FRONT of
the user PATH. Add-ToUserPath grew a -Prepend switch for it, which also
moves an entry already present further down; appending stays the default.
Corrects the previous commit, which claimed VsDevCmd prepends the VS
directories and that the emulated ninja therefore always won inside a
Developer Command Prompt. It does not. VS adds them from
Common7\Tools\vsdevcmd\ext\cmake.bat with
set "PATH=%PATH%;...\CMake\bin;...\CMake\Ninja"
which APPENDS, so they land at the very end of the composed PATH, behind
every machine and user entry - a native ninja anywhere on the user PATH
already beat it. Measured on an ARM64 box: the winget copy wins in a
fresh VsDevCmd session either way. This step is therefore insurance, not
the load-bearing fix: winget also appends its package directory, so
without it the margin rested on two append orders staying as they are,
one of them in a file Microsoft owns and revises.
The architecture audit now resolves against the PATH a NEW shell would
get - machine + user from the registry, then anything extra this process
carries (where a Developer prompt's VS directories live, kept last to
mirror how VsDevCmd appends them). Auditing $env:PATH reported state from
before the script ran, so it warned about a problem it had just fixed.
Verified end to end on ARM64: ninja resolves to the winget ARM64 build in
a fresh VsDevCmd session, CMake records it as CMAKE_MAKE_PROGRAM, and
`cmake -G Ninja` builds an ARM64 binary through the native ARM64 MSVC.
The audit now ends "Everything resolved to a native build, or to a listed
exception."
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt
Max Vilimpoc [Sat, 5 Sep 2026 22:01:55 +0000 (00:01 +0200)]
dotfiles: make the Windows setup work natively on ARM64
Run on Windows 11 on Arm as well as x64, preferring native ARM64 builds
everywhere one exists and saying plainly what is left emulated and why.
Two bugs found on an ARM64 box, both pre-existing:
- vswhere was queried unscoped, so `-products *` returned whichever VS
was newest and handed that path to a bootstrapper of a different
generation. On a machine with VS 2026 installed, a 17.x bootstrapper
was told to `modify --installPath <an 18.x install>` and every pass
targeted the wrong product. Get-VsInstallPath now scopes by version.
- the WDK probe read only the WOW6432Node hive. Which hive a kit lands
in has varied, and reading one made an installed WDK look absent,
costing a needless multi-GB reinstall on every run. Read both.
Visual Studio:
- $VsChannel splits by architecture - 17 (VS 2022) on x64, where the
v141/XP toolset is the reason to pin a generation, and 18 (VS 2026)
on ARM64, where that toolset cannot run at all.
- the aka.ms path segment differs per generation: /release/ for 17,
/stable/ for 18. This is not cosmetic - aka.ms/vs/18/release/... is
not a 404, it redirects to Bing and returns 200 with HTML, so a wrong
guess downloads a web page and fails later at Start-Process. The
download is now checked for an MZ header.
- name VC.Tools.x86.x64 and VC.Tools.ARM64 explicitly rather than
relying on --includeRecommended, which resolves differently per host:
on Arm the x64 cross-compiler is not implied. Same for ATL/Spectre.
- the v141/XP group is skipped on Arm; Clang/LLVM is installed on both.
- verification widened from "did v141 land" to which MSVC host
toolchains, whether clang-cl is native, and v141 on x64 only.
Compiler diversity: MSVC and clang-cl from Visual Studio, plus upstream
LLVM (resolves to LLVM-<ver>-woa64.exe on Arm). Two Clang majors on
independent schedules, all three native.
Native utilities, verified by reading PE COFF headers rather than
trusting package metadata:
- Visual Studio bundles an x64 ninja.exe even on ARM64, and VsDevCmd
prepends its directory, so the emulated one wins inside a Developer
prompt - the shell C++ builds run in. Ninja is re-invoked per edge in
the build graph, so this is the one place emulation is paid over and
over. Install the native build and document the PATH precedence.
- add standalone CMake: VS's copy is already ARM64 but only reaches the
PATH inside a Developer prompt, and the build line here assumes a
bare `cmake`.
- Sysinternals ships every architecture in one zip; the ARM64 build is
procexp64a.exe, not the default-named procexp.exe.
rsync: no ARM64 asset exists, so Arm takes the x64 zip (a transfer is
socket-bound, not CPU-bound). Its ssh.exe links against a System32
libcrypto.dll that is an ARM64 binary there, so it may not load at all -
and rsync PREFERS an ssh.exe in its own directory, meaning a broken one
does not degrade to the in-box client, it breaks rsync outright. Run it
after unpacking and delete it if it will not start.
VirtualBox is skipped on Arm (a hypervisor is kernel-mode; its x64 driver
cannot load on an ARM64 kernel), which also makes the Windows 7 x86 test
VM unreachable. VTune reports n/a rather than a download link.
setup-windows-no-uac.ps1 gains an ArchAudit step that reports the real
machine type of every provisioned tool, resolved PATH-first. Emulated
with a listed reason prints as expected; emulated without one is flagged
with a remedy. That is what found the ninja problem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt
Max Vilimpoc [Tue, 1 Sep 2026 11:01:04 +0000 (13:01 +0200)]
dotfiles: drop the non-elevated ETW tracing, it never worked
Putting an ordinary account into Performance Log Users was supposed to
buy user-mode ETW collection without a UAC prompt. Testing the actual
workflow says otherwise: xperf and VTune want an administrator account
and an elevated prompt, and everything short of that gets blocked
somewhere. The kernel logger was already known to be admin-only; the
rest turned out not to be worth the machinery either.
So the -TraceUser and -EtwRightsOnly parameters, the group membership
step, and the UAC launch line that existed only to carry an argument
across the elevation boundary all come out. The launch line goes back to
the single-argument form it had before.
Kept, because they are useful whether or not collection is elevated: the
Windows Performance Toolkit step that detects xperf/wpr/wpa, reports
their versions and puts them on the machine PATH, and the VTune step
that reports whether it is installed.
The README keeps the negative result rather than quietly losing it, and
gains the commands to undo what earlier revisions left on a box - the
group membership, the privilege, and the ACE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gCzbrN6p3emFUyufuJsyq
Max Vilimpoc [Sat, 29 Aug 2026 18:16:55 +0000 (20:16 +0200)]
dotfiles: keep the group membership, drop the grants that bought nothing
The NT Kernel Logger is reserved for Administrators and LocalSystem, and no
amount of permission granting reaches it. Measured on this box, all three in
place at once and across a reboot -- the account in Performance Log Users,
SeSystemProfilePrivilege granted to that group, and an explicit ACE giving the
group TRACELOG_ACCESS_KERNEL_LOGGER on SystemTraceControlGuid, confirmed present
in the descriptor afterwards:
xperf -on base -> NT Kernel Logger: Access is denied. (0x5)
wpr -start GeneralProfile -> Access is denied. (0x80070005)
It is not a check an ACE overrides, and Microsoft documents Performance Log
Users access as explicitly not extending to that session. So the privilege
grant and the ACE go, along with the LSA and EventAccessControl interop that
existed only to apply them -- roughly 280 lines, in a script that was getting
long enough to notice.
What stays is the part that works, and it does work: membership in Performance
Log Users lets the account create and control ordinary ETW sessions, verified
unelevated after the reboot --
xperf -start T -on Microsoft-Windows-Kernel-Process -f trace.etl -> exit 0
which was Access denied before. Enough to trace an application's own providers
without a UAC prompt. Kernel and CPU-sampling traces are elevated work now, run
deliberately with xperf, wpr or VTune from an Administrator prompt.
The step and the README keep the negative result rather than quietly dropping
it: it is a plausible-looking path that does not work, and the next person to
try deserves to be told where it ends. The README also says how to undo the two
grants on a box that ran the earlier revision -- they are still applied here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Sat, 29 Aug 2026 17:50:50 +0000 (19:50 +0200)]
dotfiles: record what VTune gives a standard user
Measured on this box as latislab\claude, unelevated: vtune -collect hotspots
runs a full collection and finalization, exit 0. Hardware event-based sampling
does not -- VTune warns "To collect microarchitecture performance insights, run
the product as administrator" at the top of every unelevated run, and
uarch-exploration and hotspots -knob sampling-mode=hw both fail. Which matches
Intel's own documentation: a regular user on Windows gets the User-Mode Sampling
analyses and nothing else.
Worth writing down because of how the failure presents: "cannot recognize the
processor", which reads like unsupported hardware and is not. This is a Kaby
Lake i5-8350U and the sampling drivers (sepdrv5, sepdal, vtss) are installed and
running; VTune identifies the PMU through them and cannot reach them
unelevated. That last step is inference from the evidence rather than something
confirmed - confirming it takes one elevated run.
And unlike the ETW work in this branch there is no group to join: the Linux
sampling driver can be handed to a vtune group, but on Windows the documented
answer is to elevate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Sat, 29 Aug 2026 17:46:46 +0000 (19:46 +0200)]
dotfiles: report Intel VTune rather than installing it
The elevated half now prints whether VTune is on the box, its version and the
path to vtune.exe, and prints the download page when it is not -- adding that
the CPU is not Intel, when it is not.
Not automated on purpose. The offline installer is a ~750 MB download from
registrationcenter-download.intel.com/akdlm/IRC_NAS/<guid>/, and that GUID is
per-release with no "latest" redirect behind it, so every new build would mean
editing a hard-coded link in a script whose whole point is running unattended on
a fresh box. It also only earns its place on Intel silicon, since hardware
event-based sampling reads Intel PMU counters. The unattended incantation is
recorded in the comment and the README for anyone who does want to script it:
-a --silent --cli --eula accept
Detection reads the two Uninstall hives rather than probing a path, so it
follows the install wherever it went, and the CLI path goes through the oneAPI
`latest` junction so it stays right across upgrades. Exercised against the
2026.2.0 install on this box: reports the version and a vtune.exe that exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Sat, 29 Aug 2026 17:38:00 +0000 (19:38 +0200)]
dotfiles: let the group read the kernel logger ACL it was granted
The rights mask handed to EventAccessControl was 0x0FE1 -- the WMI and TRACELOG
rights and nothing else. The SYSTEM and Administrators entries on that GUID
carry 0x120FFF, and the missing 0x120000 is READ_CONTROL and SYNCHRONIZE:
without READ_CONTROL the group cannot read back the descriptor it was just
added to, so EventAccessQuery answers "access denied" whether or not the grant
landed, which makes it useless as the one cheap probe available from the
unelevated account. Now 0x120FE1.
Also corrected, in the step and the README: the ACE is machine state, and a
logon does nothing for it. ETW reads these descriptors into a cache, so a
reboot is what is expected to put it into effect -- the ACE is in the descriptor
(D:...(A;;0xfe1;;;LU)) and xperf -on base is still denied from a fresh shell on
the running system. A first run therefore wants both: a new logon for the group
membership and the privilege, a reboot for this.
Not yet confirmed: whether the reboot is in fact sufficient.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Sat, 29 Aug 2026 17:30:32 +0000 (19:30 +0200)]
dotfiles: give the kernel logger's own ACL to Performance Log Users
The group membership and SeSystemProfilePrivilege were necessary and not
sufficient. Measured on this box after signing in with both in place:
xperf -start X -on Microsoft-Windows-Kernel-Process -> exit 0, trace written
xperf -on base -> NT Kernel Logger:
Access is denied. (0x5)
wpr -start GeneralProfile -> 0x80070005
The user-mode session proves the group fixed session control, and wpr's error
moving off "Failed to enable the policy to profile system performance" proves
the privilege took. What is left is the kernel logger itself: it does not use
ETW's default per-GUID descriptor, and the explicit one on
SystemTraceControlGuid does not mention Performance Log Users.
EventAccessQuery on that GUID returns access denied outright from the account,
which is the tell.
So add an ACE for the group with EventAccessControl (EventSecurityAddDACL, so
the entries Windows relies on stay put), carrying the controller rights
including TRACELOG_ACCESS_KERNEL_LOGGER -- the right that names this particular
session. It goes to the group like the privilege does, keeping membership the
single switch, and the log now records the resulting DACL.
The step moves to the FRONT of the elevated script and gains -EtwRightsOnly,
which runs it and exits. It is seconds of LSA and registry work, where a full
run is dominated by three Visual Studio passes that take minutes with nothing to
do -- and iterating on this needed a way to apply it without paying for those.
`exit` inside the try still runs the finally, so the transcript is stopped and
the log left readable by the non-elevated caller.
The README now states the cost plainly: a member of that group can capture
system-wide kernel traces, including paths and command lines from every account
on the box.
Exercised under Windows PowerShell 5.1: the script parses, the EtwAcl interop
compiles, the rights mask reads 0x0FE1, and both EventAccessControl and
EventAccessQuery return a clean "access denied" from a non-elevated shell rather
than marshalling garbage. The grant itself still needs an elevated run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Sat, 29 Aug 2026 17:17:40 +0000 (19:17 +0200)]
dotfiles: stop the UAC launch line mangling its own arguments
Adding -TraceUser to the elevated launch gave it a second quoted argument, and
the "".."" doubling used to get quotes through cmd only survives ONE. With
two, the quote-state parsing merges the tail into the -File value, so the
elevated PowerShell was handed
and refused it -- "failed because the file does not have a '.ps1' extension" --
exiting -196608 (0xFFFD0000) before Start-Transcript could run. The batch file
then reported no elevated log and guessed at a cancelled UAC prompt, which is
the one thing that had not happened.
Both values now travel in the environment and the quotes the child needs are
built as [char]34 inside PowerShell, so the command line in the batch file
carries no quote characters of its own beyond the outer pair.
Exercised through cmd against a probe script in a directory with a space in its
name: -File binds, -TraceUser arrives as LATISLAB\Claude, and the child's exit
code still propagates. Against the real script with -Verb RunAs dropped, it now
gets as far as the #Requires elevation check, which is where a non-elevated run
should stop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Sat, 29 Aug 2026 16:43:11 +0000 (18:43 +0200)]
dotfiles: let a standard account collect ETW traces
Windows Performance Analyzer is not a Visual Studio component -- VS's own
Performance Profiler is a different, .diagsession-based tool that cannot open an
.etl -- it ships with xperf and wpr in the Windows Performance Toolkit, either
as an optional Windows SDK feature (OptionId.WindowsPerformanceToolkit) or
inside the ADK, which bundles the same toolkit. The WPT step already installed
it; it now detects the toolkit DIRECTORY rather than just xperf.exe, prints the
version of each tool it found (wpa.exe included, since that is the one people
come looking for), and re-asserts the machine PATH entry -- comparing with the
trailing backslash trimmed, because the toolkit's own installer writes one and a
second spelling of the same directory is just noise.
Collection is the half that did not work for an ordinary account, and it fails
in two distinct ways because two distinct things are missing:
xperf -on base -> NT Kernel Logger: Access is denied. (0x5)
wpr -start GeneralProfile -> Failed to enable the policy to profile system
performance.
Controlling ANY event tracing session -- a user-mode one naming a single
provider included, which is the case that shows this is not only about the
kernel -- is checked against the security descriptor ETW keeps per provider
GUID, whose default grants the session-control rights to SYSTEM, Administrators,
the service accounts and BUILTIN\Performance Log Users, and to nobody else.
Switching on the kernel/system provider on top of that needs
SeSystemProfilePrivilege, held by default only by Administrators and
NT SERVICE\WdiServiceHost, and that is the one wpr names in its error.
So grant the privilege to the GROUP and put the account in the group:
membership alone becomes the switch, and enabling the next account is one
net localgroup away with no policy edit. LsaAddAccountRights rather than a
secedit round-trip -- it adds exactly one right to exactly one SID and is a
no-op when already held, where secedit re-applies every user right on the box to
fix one of them. SeDebugPrivilege is deliberately not granted: neither CPU
sampling nor walking stacks in your own processes needs it, and it is equivalent
to handing out administrator.
setup-windows.bat passes -TraceUser across the UAC boundary. Accepting that
prompt with an administrator's credentials runs the elevated half AS that
administrator, so it cannot otherwise tell whose box this is.
Two limits, both documented at the step and in the README. A privilege and a
group membership are read into the access token at LOGON, so the account has to
sign out and back in -- any new logon does, and an ssh login into the box is the
quick way to check without dropping the desktop. And this only helps a
NON-ADMIN account: UAC hands an administrator a filtered token keeping five
harmless privileges, so an admin's ordinary shell still cannot trace however the
policy reads. Analysis was never affected; wpa.exe opens an existing .etl as a
plain user.
Exercised under Windows PowerShell 5.1, which is what the batch file launches:
the script parses, the LSA interop compiles under the in-box CodeDom compiler,
the SID marshalling round-trips S-1-5-32-559, and LsaOpenPolicy fails cleanly
with "Access is denied" from a non-elevated shell. Get-LocalGroup -SID resolves
the localised group name, and the toolkit detection finds the SDK's WPT and
correctly reports its PATH entry as already present. The grants themselves are
unverified: they need an elevated run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMh8i2QzkHNdE3MkKfcaT6
Max Vilimpoc [Fri, 28 Aug 2026 11:22:07 +0000 (13:22 +0200)]
dotfiles: put vswhere.exe on the user PATH
The Visual Studio installer drops vswhere.exe in
%ProgramFiles(x86)%\Microsoft Visual Studio\Installer and nothing adds
that directory to the PATH, so build scripts that locate VS with it -
and VsDevCmd.bat itself - print "'vswhere.exe' is not recognized" on
every run. New VsWhere step in the non-elevated half; it is skipped
with a warning when no VS installer is present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhsGf8BtmQE1PtS1CAKns4
Max Vilimpoc [Tue, 1 Sep 2026 10:49:52 +0000 (12:49 +0200)]
dotfiles: pin WiX to 5.0.2, the last release that owes no fee
winget install WiXToolset.WiXCLI has no version selector, so it installed
7.0.0. From 6.0 onward the WiX package carries OSMFEULA.txt on top of the
Microsoft Reciprocal License: an Open Source Maintenance Fee agreement
charging a monthly fee to anyone using the prebuilt binaries as part of
revenue-generating activity with annual gross revenue >= US$10,000.
The fee buys the binaries; it is not a license fee and restricts nothing
about what we package. MS-RL is file-scoped and never reached the MSIs
WiX builds under any version. Pinning to 5.0.2 is about the invoice.
Install it as a .NET global tool instead, which takes a --version, and
add the .NET 10 SDK it needs. The MSBuild half still has to be pinned in
each .wixproj; the comment says so where someone will look for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gCzbrN6p3emFUyufuJsyq
Max Vilimpoc [Wed, 26 Aug 2026 12:40:08 +0000 (14:40 +0200)]
dotfiles: a Windows 7 test-target setup script
Provisioning a Windows 7 VM so it can be driven from the host by VBoxManage
guestcontrol takes a handful of settings that are easy to forget and easy to
get wrong, and every one of them here was learned by hitting it:
- Windows Error Reporting's dialog blocks a guestcontrol call until the host's
timeout. A harness that scores a timeout as a failure then invents bugs that
do not exist, so DontShowUI is not a nicety.
- The host watches the guest with controlvm screenshotpng, so a screen saver or
a blanked monitor makes every screenshot useless.
- Bulk transfer over the VirtualBox shared folder is far faster than a copyto
per file, but the mapping is per-user and does not survive into a new
interactive session.
The readiness report is the other half of the point. A test run against a box
with no printer, or no audio capture device, reads as "the software under test
refused" when the truth is "this VM never had one". It reports DWM composition
for the same reason: with composition off, SetWindowDisplayAffinity fails with
error 8 for every value, so anything testing screen-capture protection is
testing nothing -- and Windows 7 forces the Basic theme while the install is
not activated, which is exactly the state a throwaway test VM is usually in.
Two shapes in the DWM check are load-bearing and are commented as such. Under
guestcontrol, redirecting PowerShell's stdout to a file (or capturing it with
for /f) hangs the run until the host times out, so PowerShell prints the verdict
and the batch does not capture it. And the if/else must stay on ONE line: split
across two, PowerShell treats the file as an incomplete command and waits on
stdin forever. Two plausible cheaper checks are also wrong here, measured, and
are called out so nobody re-introduces them: dwm.exe keeps running with
composition off, and HKCU\...\DWM\Composition is the stored preference rather
than the live state. Both report ENABLED while the API returns false.
The elevated half is skipped with a notice rather than attempted, because a
guestcontrol-launched process gets a UAC-filtered token even for an account in
Administrators and cannot elevate itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLgzr9B13msJhCLpnmjNJS
Max Vilimpoc [Mon, 31 Aug 2026 15:25:11 +0000 (17:25 +0200)]
dotfiles: install the common tools every project on these machines expects
ffmpeg, adb and python3-venv join shellcheck in the optional set. They are
common across the projects here rather than specific to any one of them, so
installing them once from this script means a per-project setup script only has
to CHECK for them instead of carrying its own package-manager logic and its own
sudo. That is exactly what RAWcorder's setup-linux-tests.sh now does.
python3-venv is the one that is easy to miss. Debian and Ubuntu split venv and
ensurepip out of python3, so `python3 -m venv` fails on a stock Ubuntu with an
error telling you to apt-get it; Arch bundles both into python, so there is
nothing to add there. Both names go through pick_pkg, like everything else here.
ffprobe is checked but not installed: it has no package of its own and ships
inside ffmpeg, so checking it separately also catches a stripped-down distro
build that leaves it out.
What stays out of this script, and the comment now says so: anything
project-shaped. No virtualenvs, no per-repo Python packages, no test tooling.
Those belong beside the repo that needs them.
Also fixes a latent SC2318 in install_agi_deb. It read
local ver="$1" tmp="$2" deb="$tmp/agi.deb" depends
and the words of a `local` are expanded before `local` runs, so $tmp there is
the OUTER scope's tmp, not the parameter being assigned beside it. The caller
passes AGI_TMP and no outer tmp exists, so deb resolved to "/agi.deb" -- the
download would have landed in the filesystem root, which the sudo re-exec would
have permitted. Never observed, because AGI has not been installed from the .deb
path on this machine. The script passes shellcheck on itself again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXBBHFrwVKRsAMhtS7EkCR
Max Vilimpoc [Mon, 31 Aug 2026 09:20:55 +0000 (11:20 +0200)]
dotfiles: install AGI from setup-linux.sh, on Ubuntu and on CachyOS
The script died on anything without apt-get, which is most of the machines it
now has to run on, so the package layer is a two-line wrapper over apt-get and
pacman and the rsync lists gained their Arch spellings -- base-devel, pkgconf,
and the libraries without the -dev half Arch does not split out.
AGI ships a .deb and a .zip per release; Debian and Ubuntu get the .deb, Arch
and CachyOS the .zip unpacked to /opt/agi, since there is no AUR package for it
(the AUR RPC knows no agi, agi-bin or android-graphics-inspector). The .zip
route replays the .desktop and MIME files the .deb installs, so both routes
leave the same system behind, and only ever replaces a /opt/agi that carries a
build.properties.
The .deb cannot be installed as it stands. It declares "Depends:
openjdk-22-jre, libgtk-3-0, libwebkit2gtk-4.1-0" and Ubuntu has never packaged
openjdk-22 -- jammy through resolute ship 11, 17, 21 and 25 -- so apt has
nothing to resolve that against. Nothing in the package needs 22 either: it
bundles no JRE, the Go launcher settles for >= 11, and the highest class file
version in gapic.jar is 63, i.e. Java 19. openjdk-21-jre satisfies both and
exists on every release from 22.04 on, so the install picks a JRE that is
actually in the archive and rewrites that one control field to name it.
Only the control member is rebuilt, with ar; dpkg-deb -R/-b would unpack and
recompress 230MB of payload for a one-line edit. Verified against the real
84MB package: member order preserved, Depends retargeted, and dpkg-deb still
parses the result. Should any of that fail, the zip installer is the fallback,
so the .deb path can only cost time.
libgtk-3-0 became libgtk-3-0t64 in 24.04 and SWT wants webkit 4.1 rather than
the 4.0 only jammy has, so both are chosen by asking the archive rather than by
name. adb is installed too: it is not a hard dependency, but gapis carries the
string "adb could not be found from ANDROID_HOME or PATH", which is all AGI can
do without one.
--agi-only and --no-agi split the two halves; --minimal implies --no-agi.
AGI_VERSION pins a release, otherwise the version comes from the releases API
with the last-checked 3.3.3 as the offline fallback, and a re-run whose
/opt/agi/build.properties already matches skips the 85MB download. No pacman
-Sy anywhere -- refreshing the database without upgrading is how an Arch box
ends up half-broken -- so a stale database is reported rather than worked
around.
Exercised on CachyOS: every --dry-run path, the version lookup, and AGI 3.3.3
itself starting against the installed JRE with gtk3 and webkit2gtk-4.1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqwcPKuLX7xML2DZqHnNJY
Max Vilimpoc [Fri, 28 Aug 2026 00:05:27 +0000 (02:05 +0200)]
dotfiles: install VirtualBox with the other winget packages
The test VMs this box drives are VirtualBox ones -- it is why sshd's firewall
rule is widened to every profile, a host-only or bridged adapter being routinely
classified Public -- so the hypervisor belongs in the same list as everything
else the box is provisioned with, rather than being the one install left to do
by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D4ozcQcqMSJBi2Ez4Pfxyc
Max Vilimpoc [Fri, 28 Aug 2026 00:04:08 +0000 (02:04 +0200)]
dotfiles: give git the ssh.exe that pushes at line rate
core.sshCommand pointed at the in-box System32 client, which reads its stdin
3KB at a time -- the same limit that held an rsync push to ~17MB/s, and it
applies to anything git pushes through it too. The elevated half now unpacks a
client without that limit beside rsync.exe, so prefer it and keep the in-box one
as the fallback. Same client from the same source: same ~/.ssh, same ssh-agent
named pipe, same known_hosts.
Candidates are tried by RUNNING them rather than by Test-Path. The fast build
carries no libcrypto of its own -- it links the one the OpenSSH Client
capability puts in System32 -- so on an image without that capability it is a
file that exists and a binary that will not start, and the next `git push` is a
bad place to discover it. Two traps in doing that from this script: with
$ErrorActionPreference = 'Stop' a native command's stderr becomes a terminating
error, and `ssh -V` writes its version to stderr, so the WORKING client is the
one that looks broken; and an exe that cannot start never sets $LASTEXITCODE,
leaving the stale 0 from the last native command that did run to read as
success. Hence the EAP dance and the explicit $global:LASTEXITCODE = $null.
setup-windows.bat runs the elevated half first for this: the fast ssh.exe has to
be on disk before the git config step can prefer it. The non-elevated half
still runs when the elevated one failed -- nothing in it depends on that half,
and the fallback is the client Windows already has -- and the elevated exit code
is still what the batch file exits with.
Exercised against real binaries under Windows PowerShell 5.1, with
GIT_CONFIG_GLOBAL pointed at a scratch file: fast client present picks
10.0p2, install directory empty falls back to the in-box 9.5p1, a candidate
that exits non-zero and one that cannot start at all both warn and fall back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D4ozcQcqMSJBi2Ez4Pfxyc
Max Vilimpoc [Thu, 27 Aug 2026 23:56:49 +0000 (01:56 +0200)]
dotfiles: install the rsync release's ssh.exe beside rsync.exe
The rsync-windows release is now one zip per architecture -- rsync.exe, the
ssh.exe it runs, and the licence texts, under exactly those names -- rather
than a bare rsync.exe, so the pinned .../download/v3.5.0-g521ad8ad/rsync.exe
this script fetched no longer exists.
Fetch rsync-windows-x64.zip or rsync-windows-x86.zip by OS bitness, verify it
against the .sha256 published beside it, unpack to a scratch directory and move
out the four files we asked for -- so a future release adding a fifth cannot
quietly drop it onto the machine PATH. The two exes go in together on purpose:
rsync.exe prefers an ssh.exe in its own directory, and the release builds one
because the client Windows ships reads its stdin 3KB at a time, which holds a
transfer *from* the box at ~17MB/s however fast the link is. Nothing else
about it differs -- same ~/.ssh, same ssh-agent, same known_hosts -- and a bare
`ssh` still resolves to the in-box client, which sits ahead of C:\Tools\rsync
on the PATH.
That ssh.exe links against the libcrypto.dll the OpenSSH Client capability puts
in System32, and ships no copy of its own, so that capability is now installed
here rather than assumed: it was already the thing ssh-agent, the git
core.sshCommand in the non-elevated half, and rsync's own transport all depend
on. Where it is absent, or its LibreSSL is older than the 3.8.2 the release is
built against, the script says so and installs rsync alone.
The URL follows the releases/latest/download/ redirect rather than the API,
whose unauthenticated 60/hour per-IP limit a provisioning run behind a shared
NAT can genuinely exhaust; pin the tag in $RsyncUrl to hold a box on a build.
Verified end to end against the live release under Windows PowerShell 5.1 --
which is what the .bat launches, and which refuses to Expand-Archive anything
not named .zip, hence "download-$RsyncAsset" for the scratch file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D4ozcQcqMSJBi2Ez4Pfxyc
Max Vilimpoc [Wed, 26 Aug 2026 10:33:18 +0000 (12:33 +0200)]
dotfiles: point git at the Windows SSH client, split out the non-elevated half
git config core.sshCommand -> %WINDIR%/System32/OpenSSH/ssh.exe, so git shares
the Windows ssh-agent that the elevated half enables. Git for Windows otherwise
prefers its bundled MSYS2 ssh.exe, which cannot reach that agent (Win32-OpenSSH
publishes it on a named pipe the MSYS2 build does not speak), leaving keys added
with `ssh-add` invisible to git.
The .bat already carried a bare version of this line, but it was inert on a fresh
box: `winget install Git.Git` runs a few lines above it, so that cmd session's
PATH predates the install and `git config` only printed "not recognized" before
carrying on. Resolve git.exe explicitly (PATH, then the standard install roots)
before configuring anything.
Move the PowerShell-driven per-user work - BinSkim, the WinMerge PATH edit, and
the git config - out of the .bat into setup-windows-no-uac.ps1, mirroring
setup-windows-with-uac.ps1. It is standalone-runnable, takes -Skip to re-run a
subset, runs each step independently (a failure warns, the rest still run, exit 1
if any did), and warns when run elevated, since every step writes per-user state
that would otherwise land in the administrator's profile. The .bat is left as
winget installs plus two script calls.
Two behaviour changes while moving that code:
- The git identity is empty strings rather than PLACEHOLDER_NAME, and is skipped
when unset instead of being written. The placeholder appeared both in the
assignment and in the check that guarded it, so a find/replace over the name -
exactly what the README told you to do - silently disabled the guard.
- The BinSkim version marker is written on the up-to-date path too. Previously it
was written only after a download, so an install predating the marker re-derived
its version from BinSkim.exe's ProductVersion on every run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019avXmzifMt4wJnJzQDUJCU
Max Vilimpoc [Tue, 25 Aug 2026 10:45:40 +0000 (12:45 +0200)]
dotfiles: sync the Windows provisioning scripts
Three changes made in the other copy of these scripts, ported back so
the two do not drift. The scripts are now byte-identical apart from a few
naming lines and the one divergence that is deliberate: this copy keeps
the PLACEHOLDER git identity, which the README tells you to edit before
running.
OpenSSH Server. Installed from the Windows on-demand capability (10/1809
and later), set Automatic, started, and reachable on all firewall
profiles. That last part is the one worth having: the capability ships
its own inbound rule, but it is Private-only on some images, and a VM's
host-only or bridged adapter gets classified Public more often than not
-- which presents as a service that is plainly running and plainly
unreachable.
That rule is adopted rather than duplicated. OpenSSH-Server-In-TCP is the
name the capability itself uses, so a second rule beside it under another
name would leave the narrow one in place and merely work around it, while
one under the same name would collide. Widen it to all profiles if it
exists, create it if it does not. One rule either way, under the name the
platform expects.
rsync. Windows ships the SSH transport and nothing to run over it, so
`rsync host:path` has no remote end. The nuket/rsync-windows build is
downloaded to C:\Tools\rsync and added to the machine PATH. Not "Program
Files", because the fallback when PATH lookup fails is --rsync-path and a
path with spaces is painful to quote through two shells. Machine rather
than user PATH, because the remote end runs as `rsync --server ...` in a
non-interactive session with no login shell: Win32-OpenSSH composes that
environment from the registry, so a machine entry resolves there and does
so for every account on the box. sshd is restarted after the write, since
the running service holds the environment it started with.
BinSkim now checks before it fetches. The .nupkg is a self-contained .NET
build -- 141 MB at 4.4.9.11 -- and the old code downloaded it every run
before working out it had nothing to do. The flat-container index is a
few KB of JSON; take the newest non-prerelease and compare against
nupkg-version.txt beside the installed tool. The download URL now
interpolates the version we checked, rather than the v2 /package/<id>
endpoint that redirects to whatever is newest right now. The PATH append
moved out of the download branch so a lost PATH entry no longer costs
141 MB to repair.
Both new sections warn rather than throw: a box that cannot run sshd
should still finish provisioning the toolchain it came for.
README picks up the remote-access notes, including the authorized_keys
ACL requirement and the separate file that accounts in the Administrators
group need.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Max Vilimpoc [Wed, 5 Aug 2026 09:48:21 +0000 (11:48 +0200)]
dotfiles: Windows dev-box provisioning scripts
Extracted from a native Windows project so the box setup can be reused
and versioned on its own.
setup-windows.bat runs the non-elevated half (winget installs, user PATH
edits for WinMerge and BinSkim, global git config) and then launches
setup-windows-with-uac.ps1 elevated, printing its transcript when the
elevated window closes.
setup-windows-with-uac.ps1 enables ssh-agent and installs Visual Studio
2022 Community in three labelled passes (base C++ workload, Clang/LLVM,
v141 + Windows XP toolset), the WDK 10.0.26100, and the Windows
Performance Toolkit.
The global git identity is PLACEHOLDER_NAME / PLACEHOLDER_EMAIL and must
be edited before the script is run. The runtime transcript
(setup-windows-uac.log) is gitignored: it embeds local machine paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>