Skip to content

fix(path): keep whitespace in Windows comparison paths - #78

Merged
steipete merged 2 commits into
openclaw:mainfrom
Yigtwxx:fix/windows-path-comparison-trim
Aug 2, 2026
Merged

fix(path): keep whitespace in Windows comparison paths#78
steipete merged 2 commits into
openclaw:mainfrom
Yigtwxx:fix/windows-path-comparison-trim

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where consumers relying on root confinement would have an outside path reported as inside the root when the root string carries surrounding whitespace. The affected surface is Windows path validation and root confinement: isPathInside is the predicate other guards build on, and on win32 its only normalization step is normalizeWindowsPathForComparison.

That function ends by delegating to normalizeLowercaseStringOrEmpty (src/string-coerce.ts:31), which is a free-text string coercion helper — the same module also normalizes fast-mode flags and thread values. Its chain reaches normalizeNullableString (src/string-coerce.ts:5-11), which calls value.trim(). So a path used for containment math is trimmed before it is lowercased, and leading or trailing whitespace silently disappears from the comparison key.

Whitespace is a legal part of a Windows path component, so trimming merges two genuinely different directories into one comparison key: C:\root and C:\root both become c:\root.

Why This Change Was Made

The final step now lowercases in place instead of routing the path through the free-text coercion helper. Separator normalization and extended-length (\\?\, \\?\UNC\) handling are untouched, so the only behavior that shifts is that surrounding whitespace is preserved. Case folding is deliberately left exactly as it was, since it is the documented comparison semantic and is covered by an existing assertion in test/api-coverage.test.ts.

Non-goal, stated explicitly: this change does not alter the Unicode case-folding behavior. toLowerCase() is not injective — on a Turkish-language Windows install, "İstanbul" folds to a 9-code-point string that never round-trips back, so two distinct NTFS names can still collapse onto one comparison key. Deciding whether comparison should move to an ASCII-only or locale-invariant fold changes behavior for every non-ASCII path, which reads as an owner decision rather than a bug fix. I left it out of this PR and can follow up separately if you want it addressed.

User Impact

Consumers that derive a root from configuration or environment input — where a stray trailing space is easy to introduce — no longer get a false true from isPathInside for files that live outside that root. Paths without surrounding whitespace are unaffected, and no export, error shape, or default changes.

Evidence

Reproduction on Windows 11 with Node 24.15.0, using the real exported functions:

BEFORE (src/path.ts @ main ab93382)
  normalizeWindowsPathForComparison("C:\\root ")  -> "c:\\root"
  isPathInside("C:\\root ", "C:\\root\\secret.txt") -> true      <- outside path reported as inside

AFTER (this branch)
  normalizeWindowsPathForComparison("C:\\root ")  -> "c:\\root "
  isPathInside("C:\\root ", "C:\\root\\secret.txt") -> false
  isPathInside("C:\\root",  "C:\\root\\secret.txt") -> true      <- genuine descendants unaffected

Regression tests were added to test/windows-path.test.ts: one platform-independent case for the normalizer (trailing space and a NBSP), one win32-only pair for isPathInside covering both the padded root and a genuine descendant, plus an assertion that lowercasing, separator normalization, and extended-length stripping still behave as before.

The new tests are load-bearing. Stashing only src/path.ts and re-running the file against the unmodified normalizer:

BEFORE (fix reverted, tests kept)
  FAIL  Windows comparison normalization > keeps surrounding whitespace so padded paths stay distinct
  FAIL  Windows containment with padded roots > does not report a sibling directory as inside a space-padded root
        Tests  2 failed | 3 passed (5)

AFTER
        Tests  5 passed (5)

What the other isPathInside callers do with this change

The review asks for the intended Windows contract across boundary layers, so I inventoried every
in-package caller rather than reasoning about it:

call site root argument effect of preserving whitespace
src/archive-staging.ts:124,221,290,321,368 destinationRealDir / sourceRootReal (realpath results) none in practice; exactness is the fail-closed direction
src/file-store-boundary.ts:64,192 · src/file-store-prune.ts:54 · src/file-store.ts:431 rootReal / scopedRoot.rootWithSep (realpath results) same
src/file-store.ts:124 store rootDir same
src/install-path.ts:67 path.resolve(baseDir) same
src/deny-mutations.ts:49,96,109,110 user-supplied denyMutations policy entries behavior changes — see below

A realpath result carries the on-disk spelling, so for every confinement caller the change is
either invisible or the intended fail-closed correction. deny-mutations is the one caller that
compares free-text configuration, and there the current Windows behavior is worse than "lenient".

The deny-list case: Windows applies the policy to the wrong directory

The package already pins the intended contract, on POSIX, with two committed tests —
test/deny-mutations.test.ts preserves trailing whitespace in denied paths and
… in denied prefixes. Both carry it.skipIf(skipOnWindows), which is what the trimming
normalizer forced. Driving those same two scenarios through the public root() API on Windows 11
(Node 24.15.0), with only src/path.ts swapped between runs:

denied prefixes entry: "private "        (both "private " and "private" exist on disk)

  main ab93382     write("private /file.txt") -> ALLOWED        <- the denied directory is writable
                   write("private/file.txt")  -> BLOCKED        <- an undenied sibling is blocked

  this branch      write("private /file.txt") -> BLOCKED
                   write("private/file.txt")  -> ALLOWED

So on Windows the deny list is not merely approximate today, it is applied to the wrong directory:
the protected path stays writable while its unprotected sibling is refused. The branch produces the
POSIX behavior the repository already documents.

With that established, the two cases no longer need the Windows skip, and they are load-bearing
there:

test/deny-mutations.test.ts on Windows
  this branch    Tests  10 passed | 1 skipped (11)
  main path.ts   Tests   2 failed | 8 passed | 1 skipped (11)   <- exactly the two unskipped cases

Full suite on Windows moves from 447 passed | 185 skipped to 449 passed | 183 skipped — the two
cases that started running, and nothing else. CI already covers windows-latest on Node 22 and 24,
so this is enforced rather than local-only. The remaining skipIf in that file (symlink ancestors)
is unrelated and untouched.

Validation performed on this branch:

  • pnpm exec vitest run test/windows-path.test.ts — 5 passed
  • pnpm exec vitest run (full suite) — 447 passed, 185 skipped (632); 54 files passed, 6 skipped
  • pnpm lint:file-size — passed
  • pnpm lint:fs-boundary — passed
  • pnpm build (tsc -p tsconfig.json) — passed
  • git diff --check — clean

CHANGELOG.md has an entry under Unreleased -> Security and Correctness.

Related: openclaw/openclaw#109823, where this normalizer's lowercasing had to be worked around downstream; that PR documents the same helper being copied to obtain a case-preserving variant.

  • Tests cover the change
  • pnpm check gates run locally
  • CHANGELOG.md updated when release-relevant
  • Docs updated when behavior or API changed (no documented behavior changed; the trimming was undocumented)

normalizeWindowsPathForComparison delegated its final step to
normalizeLowercaseStringOrEmpty, a free-text string coercion helper that
trims before lowercasing. Windows containment math therefore compared
paths with their surrounding whitespace removed, so a space-padded root
collapsed onto its unpadded sibling and isPathInside reported a file in
the sibling directory as inside the root.

Lowercase in place and keep separator and extended-length handling
unchanged, so only the trimming behavior shifts.
@Yigtwxx
Yigtwxx requested a review from a team as a code owner August 1, 2026 12:53
@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P0 Emergency: data loss, security bypass, crash loop, or unusable core runtime. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. labels Aug 1, 2026
@clawsweeper

clawsweeper Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 1, 2026, 8:15 PM ET / August 2, 2026, 00:15 UTC.

ClawSweeper review

What this changes

The PR stops Windows comparison normalization from trimming surrounding whitespace, adds containment regression coverage, and enables existing trailing-whitespace deny-policy tests on Windows.

Merge readiness

⚠️ Ready for maintainer review - 4 items remain

This PR is a narrowly scoped, source-supported repair for a Windows comparison bug and includes credible real Windows before/after proof. It should remain open for a maintainer to explicitly confirm the security-boundary contract: padded Windows paths must compare as distinct lexical paths, even though some external callers may newly fail closed.

Priority: P1
Reviewed head: bb5b29bb99d127e450d7a33b0aaf25f3fbbc0f33
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused, well-explained security-boundary repair with credible Windows runtime proof; merge readiness depends on the maintainer’s compatibility-contract decision rather than a patch defect.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (terminal): The contributor supplied detailed, after-fix Windows 11 terminal evidence through exported functions, a revert-the-fix regression demonstration, and focused/full-suite results; an approved Windows CI run would add independent confirmation but is not needed to establish real behavior proof.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The contributor supplied detailed, after-fix Windows 11 terminal evidence through exported functions, a revert-the-fix regression demonstration, and focused/full-suite results; an approved Windows CI run would add independent confirmation but is not needed to establish real behavior proof.
Evidence reviewed 9 items Current main trims comparison keys: Current normalizeWindowsPathForComparison delegates to the free-text lowercase helper, so Windows containment keys are trimmed before comparison.
Trim originates in generic string coercion: normalizeNullableString calls value.trim(), confirming that the current Windows comparison path collapses surrounding whitespace rather than merely lowercasing it.
Patch removes only the inappropriate coercion: The PR replaces the trimming helper with direct separator replacement and toLowerCase(), leaving extended-length-prefix handling and case-insensitive comparison intact.
Findings None None.
Security None None.

How this fits together

isPathInside is the package’s lexical containment predicate for Windows paths and feeds root confinement plus mutation-deny checks. Roots, targets, and deny-policy entries enter the comparison layer; its result permits or blocks filesystem operations.

flowchart LR
  A[Roots, targets, and policy entries] --> B[Windows path normalization]
  B --> C[Lexical containment comparison]
  C --> D[Root confinement]
  C --> E[Mutation deny policy]
  D --> F[Filesystem operation]
  E --> F
Loading

Decision needed

Question Recommendation
Should Windows root confinement and denyMutations treat surrounding whitespace as part of the supplied path identity, so a padded path is distinct from its unpadded sibling and ambiguous callers fail closed? Confirm exact Windows path identity: Merge the patch and make padded-versus-unpadded Windows paths compare as distinct values throughout containment and deny-policy evaluation.

Why: The implementation is mechanically narrow and follows the repository’s fail-closed guidance, but it intentionally changes behavior for external callers that previously depended on an undocumented whitespace-trimming comparison.

Before merge

  • Resolve merge risk (P1) - Merging changes an externally observable Windows compatibility behavior: a padded root or deny entry no longer matches its unpadded sibling and will fail closed instead.
  • Resolve merge risk (P1) - Fork workflow approval has not yet produced hosted Windows CI results; the contributor supplied detailed local Windows proof, but an approved windows-latest run would independently enforce the new unskipped deny-policy cases.
  • Complete next step (P1) - No mechanical repair remains: a maintainer must confirm the intended compatibility and fail-closed semantics for padded Windows paths before merge.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch surface 4 files affected; 36 added, 4 removed The patch limits production code changes to one Windows comparison return path, with focused regression coverage and release notes.
Windows regressions 2 existing deny-policy tests unskipped; 4 new path assertions The patch converts existing POSIX-only whitespace expectations into Windows enforcement and adds direct containment coverage.

Merge-risk options

Maintainer options:

  1. Confirm exact-path contract (recommended)
    Accept the fail-closed compatibility change and merge once a maintainer confirms that padded Windows paths are distinct policy and containment identities.
  2. Pause for boundary-specific input policy
    Do not merge until maintainers decide whether any public configuration boundary should intentionally trim whitespace before passing paths to the comparison primitive.

Technical review

Best possible solution:

Adopt exact lexical Windows comparison semantics in the shared containment primitive; if a specific configuration surface should tolerate accidental whitespace, trim there explicitly rather than weakening the generic security predicate.

Do we have a high-confidence way to reproduce the issue?

Yes, source-reproducible with high confidence: current main sends Windows comparison keys through a helper that trims whitespace, and the PR documents a Windows exported-API before/after run plus load-bearing regression failures when the fix is reverted.

Is this the best way to solve the issue?

Yes, technically: preserving whitespace in the containment primitive is the narrowest repair and aligns with exact lexical comparison; the remaining question is maintainer confirmation of the external compatibility contract.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against ab933820c089.

Labels

Label justifications:

  • P1: Windows root-confinement and deny-policy decisions can currently target the wrong lexical path, affecting real security-sensitive filesystem operations.
  • merge-risk: 🚨 compatibility: External Windows callers that relied on padded and unpadded paths comparing equal will instead receive a fail-closed result.
  • merge-risk: 🚨 security-boundary: The modified comparison function directly controls root-confinement and mutation-deny boundary checks.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The contributor supplied detailed, after-fix Windows 11 terminal evidence through exported functions, a revert-the-fix regression demonstration, and focused/full-suite results; an approved Windows CI run would add independent confirmation but is not needed to establish real behavior proof.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor supplied detailed, after-fix Windows 11 terminal evidence through exported functions, a revert-the-fix regression demonstration, and focused/full-suite results; an approved Windows CI run would add independent confirmation but is not needed to establish real behavior proof.

Evidence

What I checked:

  • Current main trims comparison keys: Current normalizeWindowsPathForComparison delegates to the free-text lowercase helper, so Windows containment keys are trimmed before comparison. (src/path.ts:28, ab933820c089)
  • Trim originates in generic string coercion: normalizeNullableString calls value.trim(), confirming that the current Windows comparison path collapses surrounding whitespace rather than merely lowercasing it. (src/string-coerce.ts:9, 66201c1f347a)
  • Patch removes only the inappropriate coercion: The PR replaces the trimming helper with direct separator replacement and toLowerCase(), leaving extended-length-prefix handling and case-insensitive comparison intact. (src/path.ts:28, bb5b29bb99d1)
  • Security-sensitive callers use the predicate: isPathInside decides Windows lexical containment and is used by archive staging, file-store boundaries, install-path validation, output handling, and deny-mutation checks; the deny policy compares user-provided absolute entries through this predicate. (src/deny-mutations.ts:49, ab933820c089)
  • Documented contract supports exact lexical identity: The public path documentation describes isPathInside as a pure lexical check after normalization, with Windows normalization limited to case and separators; it does not document free-text whitespace coercion. (docs/path.md:28, ab933820c089)
  • Feature provenance: The current normalization function and deny-mutation implementation appear to date to the v0.5.0 feature introduction by Peter Steinberger; this makes that area owner the best routing candidate for the contract decision. (src/path.ts:20, 66201c1f347a)

Likely related people:

  • Peter Steinberger: Git blame attributes the current Windows comparison function to the v0.5.0 introduction, whose release commit also added the deny-mutation implementation and its regression suite. (role: feature introducer and adjacent area owner; confidence: high; commits: 66201c1f347a; files: src/path.ts, src/deny-mutations.ts, test/deny-mutations.test.ts)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Confirm the exact Windows path-identity contract for external callers.
  • Approve or obtain the pending windows-latest CI run to independently enforce the newly unskipped cases.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (5 earlier review cycles)
  • reviewed 2026-08-01T12:57:41.958Z sha c0696e5 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T14:53:14.223Z sha c0696e5 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T20:14:42.558Z sha c0696e5 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T20:26:19.433Z sha bb5b29b :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T22:36:59.318Z sha bb5b29b :: needs maintainer review before merge. :: none

@Yigtwxx

Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Answering the maintainer decision the review flagged, since it is the only thing left on this PR.

I intended option 1 — confirm fail-closed root semantics, and the patch is written for that reading: normalizeWindowsPathForComparison produces a comparison key for containment math, so the lexical spelling of the root has to survive into the key. A space is a legal path component character on Windows, so C:\root and C:\root are two different directories and merging them into one key is what produced the false true.

On the compatibility risk, to scope what actually changes:

  • Callers whose root has no surrounding whitespace: unchanged.
  • Callers who pass a padded root and padded candidates: unchanged — both sides keep the same spelling and still compare equal.
  • Callers who pass a padded root against unpadded candidates: this is the case that flips, and it flips from "reported inside" to "reported outside". That direction is fail-closed, so the failure mode is a denied operation rather than an escape.

Option 2 (normalize root input at the configuration boundary) is not mutually exclusive with this and I think it is the better long-term shape — but it belongs where roots are constructed, not inside a containment predicate, and it does not fix the predicate for callers who build paths themselves. Happy to open that as a follow-up if you want it.

Also flagged in the PR body and worth repeating here: toLowerCase() is not injective, so this key can still collapse distinct NTFS names on non-ASCII input (Turkish İ being the obvious one). That is pre-existing, deliberately out of scope here, and a separate follow-up if you want comparison moved to an ASCII-only or locale-invariant fold.

No further changes planned from my side — the branch is at c0696e5 and I'll hold it there unless you'd like something reshaped.

Both cases were skipped on win32 because the trimming normalizer made a
padded deny prefix compare equal to its unpadded sibling. With the
comparison fixed they describe real Windows behavior, and on the old
normalizer they fail for the reason that matters: the named directory
stayed writable while the sibling was blocked.
@Yigtwxx

Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Both remaining items are about the intended contract across boundary layers, so I measured it instead of arguing it. bb5b29b adds what the measurement produced; the PR body carries the full write-up.

The contract is already yours, and it is pinned by committed tests. test/deny-mutations.test.ts has preserves trailing whitespace in denied paths and … in denied prefixes. Both carry it.skipIf(skipOnWindows) — the skip exists because the trimming normalizer made Windows disagree with them.

On Windows the deny list is currently applied to the wrong directory. Driving those two scenarios through the public root() API on Windows 11 / Node 24.15.0, swapping only src/path.ts between runs, with both private and private present on disk:

denied prefixes entry: "private "

  main ab93382   write("private /file.txt") -> ALLOWED     <- the denied directory is writable
                 write("private/file.txt")  -> BLOCKED     <- an undenied sibling is blocked

  this branch    write("private /file.txt") -> BLOCKED
                 write("private/file.txt")  -> ALLOWED

That is stronger than the isPathInside example already in the body: it is not an approximation, it is an inversion, and it affects the mutation guard rather than only the containment predicate.

Given that, I dropped the Windows skip on those two cases. They are load-bearing there:

test/deny-mutations.test.ts on Windows
  this branch    Tests  10 passed | 1 skipped (11)
  main path.ts   Tests   2 failed | 8 passed | 1 skipped (11)

Full Windows suite: 447 passed | 185 skipped449 passed | 183 skipped. Nothing else moved. ci.yml already runs windows-latest on Node 22 and 24, so this is now enforced in CI rather than only on my machine. The third skipIf in that file (symlink ancestors) is unrelated and untouched.

On the compatibility item: every other in-package caller passes a realpath result (archive-staging, file-store*, install-path), where the on-disk spelling is authoritative and exactness is the fail-closed direction. deny-mutations was the only caller reading free-text configuration, and it is the one the change repairs. So the blast radius for "a caller that supplied a padded root and relied on the trim" is limited to callers outside this package; if you would rather keep that leniency for policy input specifically, the coercion belongs at the policy boundary (policyPathEntries, next to safeDirName's existing trim() in install-path.ts:7) rather than in the comparison primitive — happy to add that instead if you prefer it.

CHANGELOG updated with the deny-mutations effect under the existing Unreleased entry.

@Yigtwxx

Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

One correction to the note above, so the claim is not read as stronger than it is.

ci.yml does define windows-latest on Node 22 and 24, but no workflow run has executed on this PR: ci, coverage, benchmarks, and CodeQL are all sitting at action_required on both this head and the previous one, so they are waiting on a maintainer to approve workflows for the fork. Only ClawSweeper Dispatch and the Socket checks have run.

So the Windows numbers above are from my machine (Windows 11, Node 24.15.0), and the unskipped cases will only be enforced once you approve the run. If you would rather not spend a CI approval on it yet, the local before/after is reproducible with:

git checkout ab93382 -- src/path.ts && pnpm exec vitest run test/deny-mutations.test.ts   # 2 failed
git checkout HEAD -- src/path.ts     && pnpm exec vitest run test/deny-mutations.test.ts   # 10 passed

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. P1 Urgent regression or broken agent/channel workflow affecting real users now. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. P0 Emergency: data loss, security bypass, crash loop, or unusable core runtime. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Aug 1, 2026
@steipete

steipete commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Maintainer-side reproduction on the exact head bb5b29bb99d127e450d7a33b0aaf25f3fbbc0f33 supports LAND.

Verified locally with Node 24.18.0 and pnpm 10.34.5:

  • pnpm exec vitest run test/windows-path.test.ts test/deny-mutations.test.ts — 14 passed, 2 platform skips.
  • pnpm check — lint and build passed; full suite 607 passed, 25 skipped. The final temporary consumer install was interrupted after npm registry requests stalled for five minutes, with no package assertion failure emitted.
  • Built-library proof after pnpm build: normalizeWindowsPathForComparison("C:\\root ") returned "c:\\root ", while the unpadded path returned "c:\\root"; the comparison keys were distinct.
  • git diff --check — clean.

Source review agrees with the patch shape: path comparison must preserve legal path characters, and any input convenience trimming belongs at a specific configuration boundary rather than inside the shared confinement predicate. This also makes the existing trailing-whitespace deny-policy contract enforceable on Windows in the fail-closed direction.

Hosted Windows CI is still the remaining independent gate; the contributor's real Windows before/after proof covers the platform behavior meanwhile.

@steipete
steipete merged commit cf4e297 into openclaw:main Aug 2, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P1 Urgent regression or broken agent/channel workflow affecting real users now. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants