This note records the measured scope behind Caty Agent Harness's WSL2 claim. It is intentionally narrower than "Windows support": native Windows remains unsupported, while WSL2 (Ubuntu on Windows) is a separate verified-with-conditions tier.
For the full decision lane, issue history, and wording discussion, see issue #158.
- Native Windows: not supported. The measured blockers are filesystem and locking semantics that break harness assumptions.
- WSL2 (Ubuntu on Windows): supported with conditions, verified on one dated machine on 2026-08-23, but not CI-tested.
- Closest CI approximation:
ubuntu-wsl2-profileinci-matrix(umask 002, non-root container). That is useful evidence, but it is still not the same thing as a real WSL2 run.
- Native Windows: 5/30 suites passed
- WSL2 before correcting ambient conditions: 24/30 suites passed
- WSL2 after correcting ambient conditions but before issue #157: 29/30 suites passed
The one remaining failure at that point was the real harness bug fixed by issue #157, not a WSL2-only environment artifact.
- WSL2 with the
umask 022profile, after #157: 30/30 suites passed - WSL2 after D1', under raw
umask 0002: 30/30 suites passed
The dated machine was win11-test-vm, a Windows 11 Enterprise Evaluation VM on the family server. The verified WSL2 environment used Ubuntu, a non-root user, git 2.50, and a repository checkout on the Linux filesystem.
These three measured walls are why native Windows is still marked unsupported:
chmodsilently becomes644, so wrapper and trust-mode checks cannot rely on the requested executable mode.ln -sbecomes a copy rather than a real symlink in the tested path, so harness symlink assumptions do not hold.- There is no
flock, so the locking contract used by the harness is missing.
Any one of those would be a serious caveat. Together they are enough to keep native Windows out of the supported set.
The WSL2 claim is real only under these conditions:
- Your AI tool must run inside the same WSL2 distro. A Windows-side agent can install successfully while the harness hooks simply never fire. This is the easiest failure to miss, so it is the first condition everywhere the claim is made.
- The repository must live on the Linux filesystem.
Use a path like
/home/..., not/mnt/c/.... This is a correctness requirement, not just a performance tip: the harness relies on Linux filesystem semantics for mode checks, symlinks, and related trust decisions. - Use
git 2.34+. Older Git versions miss the SSH-signing floor used by the updater suites. - Run as a non-root user. Root can make permission-sensitive tests pass for the wrong reason, hiding the behavior the harness is trying to verify.
- Wrapper-type files must not be group/world-writable.
Use modes like
0755. Underumask 002, a barechmod +xcan leave wrappers at0775, and the harness correctly refuses those files. That refusal is intentional fail-closed behavior, not a bug.
The adapter scheduling docs were written macOS-first (LaunchAgent). The launchd rationale — macOS crontab sessions cannot reach the user Keychain, so claude -p exits with Not logged in under cron — is macOS-specific. On Linux and WSL2 the claude CLI discovers its credentials under $HOME on disk, so cron is a valid scheduler for the same tick wrappers. Two working options:
The WSL2-specific trap: cron does not autostart in a stock WSL2 distro. Either
- enable systemd — put
[boot]/systemd=truein/etc/wsl.confinside the distro, then runwsl --shutdownfrom Windows and reopen; cron and systemd timers then start on distro boot — or - without systemd, start it per boot with
sudo service cron start(manually, or via a[boot]command=service cron startline in/etc/wsl.conf).
Either way, remember that a WSL2 distro only runs while Windows keeps it alive; a tick schedule assumes the distro is up at tick time.
A crontab entry for the harness cron wrapper looks like:
CATY_WRAPPER_EXTRA_PATH=/home/<user>/.local/bin:/home/<user>/.npm-global/bin
TARGET=/absolute/path/to/caty-agent-harness/adapters/claude-code/flush-intake.sh
CATY_HARNESS_ROOT=/absolute/path/to/caty-agent-harness
0 */8 * * * /bin/bash /absolute/path/to/workspace/scripts/cron-wrapper.sh /absolute/path/to/workspaceBoth wrapper templates (templates/cron-wrapper.tmpl.sh, templates/updater-cron.tmpl.sh) pin PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin. On Linux/WSL2 the claude/codex CLIs are almost always under a user-local prefix — ~/.nvm/versions/node/<version>/bin, ~/.npm-global/bin, or ~/.local/bin — none of which is on that list, so a tick can fail to find the CLI even though interactive shells work. Set CATY_WRAPPER_EXTRA_PATH in the crontab (or unit) environment to a colon-separated list of absolute directories. The wrapper validates each entry (absolute, non-empty; anything else is a fail-closed infra error, exit 3) and appends the list after the pinned baseline, so user directories can never shadow system tools. Find the real directory with dirname "$(command -v claude)" in an interactive shell; note that nvm paths are version-specific and move on nvm install, so prefer a stable prefix such as ~/.npm-global/bin or ~/.local/bin where you can.
This variable is for the two wrapper templates only. The Hermes verifier provider intentionally keeps its own fixed PATH=/usr/bin:/bin and does not honor it; see adapters/hermes/INSTALL.md for that contract.
With systemd=true enabled, a user unit + timer pair fills the LaunchAgent role:
# ~/.config/systemd/user/caty-intake.service
[Unit]
Description=Caty harness flush intake tick
[Service]
Type=oneshot
Environment=TARGET=/absolute/path/to/caty-agent-harness/adapters/claude-code/flush-intake.sh
Environment=CATY_HARNESS_ROOT=/absolute/path/to/caty-agent-harness
Environment=CATY_WRAPPER_EXTRA_PATH=/home/<user>/.local/bin
ExecStart=/bin/bash /absolute/path/to/workspace/scripts/cron-wrapper.sh /absolute/path/to/workspace# ~/.config/systemd/user/caty-intake.timer
[Unit]
Description=Caty harness flush intake cadence
[Timer]
OnUnitActiveSec=8h
OnBootSec=5min
Persistent=true
[Install]
WantedBy=timers.targetEnable with systemctl --user enable --now caty-intake.timer, and run loginctl enable-linger <user> so the user manager keeps running without an open session — on WSL2, sessions close often.
- mawk (
--checkfreshness warning) — tracked as issue #84. Ubuntu's defaultawkis mawk, which does not support the ERE interval regex used by theinstall.shfreshness check, producing a spuriouscannot prove freshwarning. Until #84 closes, treat that specific warning as unproven-not-stale under mawk, or installgawk. - perl is a runtime prerequisite for task-runner. Stock Ubuntu ships it; minimal WSL2/container images may not. See the prerequisites list in CONTRIBUTING.md for the fail-closed behavior when it is absent.
- Locale coverage differs from CI in human runs.
tests/pause-contract.test.shruns itsen_US.UTF-8locale-path case only where that locale exists; its absence is a hard failure on macOS and a silent SKIP elsewhere, so a human WSL2make testsilently loses that coverage unless the locale is generated. CI restores it withlocale-gen; for parity runsudo locale-gen en_US.UTF-8once in the distro.
The first fail-open lesson came from issue #157 and commit a58062a, not from umask. Before that fix, mode detection assumed BSD-first stat handling; on GNU userlands it could yield invalid or empty mode data. WSL2 exposed that as the one remaining real suite failure after the ambient setup was corrected. At the same time, the existing CI GNU cells stayed green because the arithmetic path did not turn that bad mode data into a failing check, so those green cells were effectively fail-open. #157 changed mode detection to GNU-first and validates a pure octal mode string before doing arithmetic, so invalid or empty data now fails closed instead of slipping through.
The later umask lesson is separate. The 2026-08-22 run also showed that a WSL2 result could look "almost supported" while still depending on ambient umask behavior. That is why D1' made trust fixtures umask-independent and why D2' added the ubuntu-wsl2-profile CI cell. The pre-D1' measurement under raw umask 0002 was 6 failing suites / 50 FAIL lines (issue #158 実測ゲート, 2026-08-23), which D1' took to zero. If the suite had only passed under umask 022, it would still have been too easy to document WSL2 as supported while missing a real-world umask 002 refusal path. After D1', the raw umask 0002 run passed 30/30, which is the evidence behind the current wording.
- This is a dated, one-machine verification record, not a broad claim about every Windows laptop or every WSL2 distro.
- WSL2 is therefore marked 🟡 supported with conditions, not ✅ CI-tested.
- Native Windows remains ❌ not supported until those measured walls are removed or a new support path is proven.