Proposal: degraded native-Windows write mode behind an explicit reduced-guarantees flag

Author: AgriciDanielCreated Aug 1, 2026Updated Aug 1, 2026

Motivation

v2.1.0 (#150) made native Windows a supported read-only platform: inspection, dry-runs, and retrieval work natively, while vault writes fail closed with UNSUPPORTED_PLATFORM and point to WSL. That boundary is deliberate — mutation safety is built on POSIX directory-descriptor confinement that native Windows cannot provide.

Real-world feedback since release: WSL itself can be a nontrivial hurdle. One user verified native read-only works, then hit the write refusal — and found wsl --install completed but wsl --status / wsl -l -v hung indefinitely (Hyper-V / Virtualization-Based-Security conflict class, survived a reboot). For users in that position the current posture is all-or-nothing: WSL works perfectly, or zero write capability.

Short-term, docs/windows-wsl.md now covers WSL troubleshooting. This issue tracks the longer-term ask: a degraded native-Windows write mode, default-off, behind an explicit reduced-guarantees flag.

What full confinement guarantees today (what would be traded)

  • Descriptor pinning: the vault root, its parent, .vault-meta, and every runtime directory are held as O_DIRECTORY|O_NOFOLLOW descriptors for the whole mutation window; concurrent alias/namespace swaps fail closed (_lock_entry_matches dev/ino re-verification, ~8 checks per write via _assert_transaction_namespaces).
  • Advisory lock on the vault inode: fcntl.flock(root_fd, LOCK_EX|LOCK_NB) serializes writers across .vault-meta replacement.
  • fsync-ordered lock publication: directory fsyncs order mutation.lock creation and owner.json publication across crashes.
  • Staleness probing: os.kill(pid, 0) + quarantine-rename reaping.
  • No recursive path deletion, ever: runtime teardown unlinks only descriptor-pinned, schema-known entries.

Proposed architecture (honest deltas)

Lock (wire-compatible with claude-obsidian.mutation-lock.v1):

  • Keep the on-disk shape: .vault-meta/mutation.lock/ directory + owner.json (O_CREAT|O_EXCL) — both primitives are portable once dir_fd= is dropped.
  • Serialization: msvcrt.locking byte-range lock on a stable file inside the lock directory. Documented delta: Windows byte-range locks are mandatory, per-file, released on handle close — the "serialize the vault inode across runtime-namespace replacement" property is lost (a swapped .vault-meta yields a different lock file).
  • No auto-reaping on Windows: PID liveness probing is unreliable there; stale locks require explicit transaction recover --force-stale-lock, matching the existing "never automate the override" doctrine (docs/compound-vault-guide.md).
  • Identity checks become cached (st_dev, st_ino) lstat re-derivations — a point-in-time check with a documented TOCTOU window, the same guarantee level as the (already shipped) degraded read mode.

Journal/runtime layer — path-mode ports needed for roughly 12 fd-anchored functions: _OperationStore, _RuntimeStore, _open_runtime_directory_at, _bounded_runtime_names (fd-scandir), _read_runtime_bytes_at, _atomic_runtime_write_at (also needs os.replace semantics — os.rename cannot overwrite on Windows), the recovery readers/validators, and _restore_journal plumbing (whose leaf helpers already have path branches from v2.1.0).

The hardest problem — runtime teardown: _remove_pinned_runtime_tree_at exists precisely because the design refuses recursive path deletion. Proposal: replace with bounded schema-validated deletion — only known entry names, depth ≤ 2, per-level assert_unaliased_directory (symlink/junction rejection), refuse anything unexpected and leave it for manual recovery.

Consent gate:

  • New CLI flag --allow-degraded-confinement following the house --allow-egress/--allow-remote-ollama convention: default-deny, the refusal error names the flag.
  • Threaded through all four enforcement chokepoints (apply_bundle, command_init, capture runtime + queue lock).
  • Echoed in result JSON and the journal (precedent: forced_stale_lock).
  • The approval hash binds the flag: a plan reviewed under full confinement cannot be replayed with degraded confinement, and vice versa.
  • Declared in config/capabilities.json confirmation surfaces and noted in the product contract.

Also in scope: fixing the read-only asymmetry where capture queue list is refused on native Windows (the capture runtime layer currently has no path-mode reads at all).

Non-goals: git checkpointing (independently Linux/macOS-only), the bash setup scripts and shell lock tooling (#117), FAT/exFAT/network-share vaults (unstable file identity stays fail-closed), auto-reaping on Windows.

Test/CI plan

  • POSIX simulation coverage (the windows_mode() pattern from tests/test_windows_compat.py) plus a new tests/test_windows_degraded_write.py added to the windows-smoke allowlist (repo convention: explicit allowlists over in-test skips).
  • A meaningful subset of test_transaction.py scenarios (plain apply/rollback/journal/idempotency) becomes portable and should gain native runs; fd-pinning race tests and reap-semantics tests stay POSIX-only.

Size and risk

Size: L — comparable to the v2.1.0 effort. Risks: the teardown redesign is security-sensitive; mandatory-lock semantics can surprise (e.g. an open handle in Obsidian blocking rename-over); TOCTOU windows are real and must be documented, not hidden; every new branch needs both simulated and native coverage before ship.

Feedback wanted

  • From Windows users blocked on WSL (Hyper-V/VBS conflicts especially): would a default-off, reduced-guarantees write mode meet your need, and is the explicit-flag friction acceptable?
  • From anyone relying on the confinement guarantees: objections to the deltas above, or additional invariants the degraded mode must still hold?

Source: AgriciDaniel/claude-obsidian