Skip to content

fix: handle Windows-style session paths when running on POSIX#50116

Open
RIPRODUCTIONS wants to merge 1 commit intoopenclaw:mainfrom
RIPRODUCTIONS:fix/windows-session-paths-on-posix
Open

fix: handle Windows-style session paths when running on POSIX#50116
RIPRODUCTIONS wants to merge 1 commit intoopenclaw:mainfrom
RIPRODUCTIONS:fix/windows-session-paths-on-posix

Conversation

@RIPRODUCTIONS
Copy link
Copy Markdown

Summary

  • When sessions.json is written by a Windows host but read inside a Linux Docker container, paths like C:\Users\...\abc.jsonl are treated as relative by path.isAbsolute (which only recognises POSIX absolutes)
  • This causes resolvePathWithinSessionsDir to concatenate the Windows path onto the container base, producing broken paths like /home/node/.openclaw/.../C:\Users\...\abc.jsonl
  • Adds isWindowsAbsoluteOnPosix() guard that detects drive-letter paths on POSIX and extracts the leaf filename, resolving it within the sessions directory

Test plan

  • Verify resolvePathWithinSessionsDir correctly extracts basename from C:\Users\foo\sessions\abc.jsonl on Linux
  • Verify C:/forward/slash/abc.jsonl variant also handled
  • Verify no behaviour change on native Windows (guard returns false when path.sep === "\\")
  • Verify normal POSIX relative paths still resolve correctly

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 19, 2026 01:36
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 19, 2026

Greptile Summary

This PR adds a cross-platform path guard to resolvePathWithinSessionsDir so that Windows-style absolute paths stored in sessions.json by a Windows host are handled correctly when read inside a Linux/Docker container. The approach — detect a drive-letter prefix on POSIX and extract only the leaf filename — is the right strategy given that path.isAbsolute is OS-specific and cannot recognise Windows paths on POSIX.

Issues found:

  • The early-return guard silently falls through for degenerate Windows paths that yield an empty basename (e.g. C:\ or C:\foo\), which can reproduce the broken concatenated path the PR is trying to fix. An explicit throw would be safer.
  • The ?? candidate fallback in extractBasename is dead code — split always returns at least one element, so the right-hand side of ?? is never reached. Using || instead would also cover the empty-string trailing-separator case.
  • Minor: the inner path.resolve(sessionsDir) in the early-return is redundant since path.resolve(a, b) already resolves a.

Confidence Score: 3/5

  • Safe to merge for the common case; degenerate Windows paths with no filename can still silently produce broken results.
  • The fix is correct for the documented scenario (well-formed Windows paths like C:\Users\foo\sessions\abc.jsonl). However, the silent fallthrough when basename extraction fails means edge-case inputs can bypass the guard and hit the old broken behaviour, and there are a couple of minor dead-code issues. The change is isolated to a single path-resolution helper, limiting blast radius.
  • src/config/sessions/paths.ts — specifically the fallthrough path in the new isWindowsAbsoluteOnPosix guard block.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 28

Comment:
**Unreachable `?? candidate` fallback**

`Array.prototype.split` always returns an array with at least one element (even `"".split(/[/\\]/)` yields `[""]`), so `parts[parts.length - 1]` can never be `undefined`. The `?? candidate` branch is dead code.

```suggestion
  return parts[parts.length - 1] || candidate;
```

Switching to `||` also covers the empty-string case — if the path ends with a separator (e.g. `C:\foo\`), `parts.at(-1)` is `""`, and `|| candidate` falls back to the original string, giving the caller a non-empty value to check against.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 211

Comment:
**Redundant double `path.resolve`**

`path.resolve(a, b)` already resolves `a` internally before joining, so the inner `path.resolve(sessionsDir)` is a no-op and just adds noise.

```suggestion
      return path.resolve(sessionsDir, basename);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 208-213

Comment:
**Silent fallthrough on degenerate Windows paths**

If `isWindowsAbsoluteOnPosix` returns `true` but `basename` ends up empty (e.g. `C:\` or `C:\foo\`, where `split` yields a trailing `""`), the guard block is skipped and execution falls through to the normal POSIX logic. On POSIX `path.isAbsolute("C:\\foo\\")` returns `false`, so the Windows string is treated as a *relative* path and `path.resolve(realBase, "C:\\foo\\")` produces the broken concatenated path the PR is trying to prevent.

While a sessions entry stored without a filename is highly unlikely, the silent fallthrough means the guard's invariant can be violated. Consider throwing explicitly in this case:

```typescript
  if (isWindowsAbsoluteOnPosix(trimmed)) {
    const basename = extractBasename(trimmed);
    if (basename && basename !== "." && basename !== "..") {
      return path.resolve(sessionsDir, basename);
    }
    // Degenerate Windows path with no usable filename — cannot resolve safely.
    throw new Error("Session file path must be within sessions directory");
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: handle Windows-..."

Re-review progress:

function extractBasename(candidate: string): string {
// Split on both separators to handle Windows paths on POSIX.
const parts = candidate.split(/[/\\]/);
return parts[parts.length - 1] ?? candidate;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unreachable ?? candidate fallback

Array.prototype.split always returns an array with at least one element (even "".split(/[/\\]/) yields [""]), so parts[parts.length - 1] can never be undefined. The ?? candidate branch is dead code.

Suggested change
return parts[parts.length - 1] ?? candidate;
return parts[parts.length - 1] || candidate;

Switching to || also covers the empty-string case — if the path ends with a separator (e.g. C:\foo\), parts.at(-1) is "", and || candidate falls back to the original string, giving the caller a non-empty value to check against.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 28

Comment:
**Unreachable `?? candidate` fallback**

`Array.prototype.split` always returns an array with at least one element (even `"".split(/[/\\]/)` yields `[""]`), so `parts[parts.length - 1]` can never be `undefined`. The `?? candidate` branch is dead code.

```suggestion
  return parts[parts.length - 1] || candidate;
```

Switching to `||` also covers the empty-string case — if the path ends with a separator (e.g. `C:\foo\`), `parts.at(-1)` is `""`, and `|| candidate` falls back to the original string, giving the caller a non-empty value to check against.

How can I resolve this? If you propose a fix, please make it concise.

if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant double path.resolve

path.resolve(a, b) already resolves a internally before joining, so the inner path.resolve(sessionsDir) is a no-op and just adds noise.

Suggested change
return path.resolve(path.resolve(sessionsDir), basename);
return path.resolve(sessionsDir, basename);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 211

Comment:
**Redundant double `path.resolve`**

`path.resolve(a, b)` already resolves `a` internally before joining, so the inner `path.resolve(sessionsDir)` is a no-op and just adds noise.

```suggestion
      return path.resolve(sessionsDir, basename);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +208 to +213
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent fallthrough on degenerate Windows paths

If isWindowsAbsoluteOnPosix returns true but basename ends up empty (e.g. C:\ or C:\foo\, where split yields a trailing ""), the guard block is skipped and execution falls through to the normal POSIX logic. On POSIX path.isAbsolute("C:\\foo\\") returns false, so the Windows string is treated as a relative path and path.resolve(realBase, "C:\\foo\\") produces the broken concatenated path the PR is trying to prevent.

While a sessions entry stored without a filename is highly unlikely, the silent fallthrough means the guard's invariant can be violated. Consider throwing explicitly in this case:

  if (isWindowsAbsoluteOnPosix(trimmed)) {
    const basename = extractBasename(trimmed);
    if (basename && basename !== "." && basename !== "..") {
      return path.resolve(sessionsDir, basename);
    }
    // Degenerate Windows path with no usable filename — cannot resolve safely.
    throw new Error("Session file path must be within sessions directory");
  }
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 208-213

Comment:
**Silent fallthrough on degenerate Windows paths**

If `isWindowsAbsoluteOnPosix` returns `true` but `basename` ends up empty (e.g. `C:\` or `C:\foo\`, where `split` yields a trailing `""`), the guard block is skipped and execution falls through to the normal POSIX logic. On POSIX `path.isAbsolute("C:\\foo\\")` returns `false`, so the Windows string is treated as a *relative* path and `path.resolve(realBase, "C:\\foo\\")` produces the broken concatenated path the PR is trying to prevent.

While a sessions entry stored without a filename is highly unlikely, the silent fallthrough means the guard's invariant can be violated. Consider throwing explicitly in this case:

```typescript
  if (isWindowsAbsoluteOnPosix(trimmed)) {
    const basename = extractBasename(trimmed);
    if (basename && basename !== "." && basename !== "..") {
      return path.resolve(sessionsDir, basename);
    }
    // Degenerate Windows path with no usable filename — cannot resolve safely.
    throw new Error("Session file path must be within sessions directory");
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves cross-platform compatibility for session transcript paths by correctly handling Windows-style absolute paths when session metadata is read on POSIX (e.g., Linux containers).

Changes:

  • Adds POSIX-only detection for Windows drive-letter absolute paths.
  • Extracts the leaf filename from Windows-style paths and resolves it within the sessions directory to prevent malformed concatenations.

Comment thread src/config/sessions/paths.ts Outdated
Comment on lines +26 to +30
// Split on both separators to handle Windows paths on POSIX.
const parts = candidate.split(/[/\\]/);
return parts[parts.length - 1] ?? candidate;
}

Comment on lines +208 to +213
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
}
}
Comment thread src/config/sessions/paths.ts Outdated
Comment on lines +204 to +213
// Guard: if running on POSIX but the candidate is a Windows-style absolute
// path (e.g. stored in sessions.json by a Windows host and read inside a
// Docker container), extract the leaf filename to avoid creating paths like
// "/home/node/.openclaw/.../C:\Users\...".
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
}
}
// Already on Windows — path.isAbsolute handles it natively.
return false;
}
return /^[A-Za-z]:[/\\]/.test(candidate);
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6075f5b08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +208 to +211
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve agent isolation when rewriting Windows session paths

When a POSIX host reads a Windows-authored sessionFile whose basename already exists locally (for example C:\...\agents\bot2\sessions\sess-1.jsonl while resolving a bot1 session), this branch now rewrites it straight to <sessionsDir>/sess-1.jsonl. That is a new silent aliasing bug: before, the bad Windows path stayed isolated under a distinct broken name, but after this change it can append to the wrong live transcript and resolveAndPersistSessionFile() will persist that alias back into sessions.json. The Windows-path fix needs to preserve agent/root context instead of collapsing everything to the basename.

Useful? React with 👍 / 👎.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs real behavior proof before merge.

Summary
The PR adds POSIX-only Windows drive-letter detection in session path resolution, maps detected paths to a local basename, adds one regression test, and adds a changelog entry.

Reproducibility: yes. for the bug class: a POSIX Node path check shows Windows drive-letter paths are treated as non-absolute, and current main uses that same path.isAbsolute branch before resolving persisted sessionFile values. I did not run the full TypeScript helper to avoid write-producing test/tool caches in this read-only review.

Real behavior proof
Needs real behavior proof before merge: The PR body has an unchecked test plan and the latest maintainer comment lists tests/build/check, but there is no real Linux or Docker imported-session run, terminal output, log, screenshot, recording, or linked artifact showing the fixed behavior after the change. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Human or author revision is needed because the PR has a P1 data-isolation finding and the external-PR real behavior proof gate is still unmet.

Security
Needs attention: The diff introduces a concrete session data-isolation risk by rewriting Windows-authored paths to the current agent's same-basename transcript.

Review findings

  • [P1] Preserve agent context for Windows session paths — src/config/sessions/paths.ts:201
  • [P2] Reject degenerate Windows session paths — src/config/sessions/paths.ts:198-203
Review details

Best possible solution:

Revise the resolver to recognize Windows absolute paths on POSIX while preserving agent/root context through the existing fallback flow, reject degenerate path shapes, and add focused regression coverage plus redacted real import proof.

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

Yes, for the bug class: a POSIX Node path check shows Windows drive-letter paths are treated as non-absolute, and current main uses that same path.isAbsolute branch before resolving persisted sessionFile values. I did not run the full TypeScript helper to avoid write-producing test/tool caches in this read-only review.

Is this the best way to solve the issue?

No. The PR fixes the simplest same-agent filename case, but the basename-only early return bypasses existing realpath normalization and cross-agent fallback behavior; the safer solution is to normalize Windows absolute paths into the existing resolver path instead of returning early.

Full review comments:

  • [P1] Preserve agent context for Windows session paths — src/config/sessions/paths.ts:201
    This early return collapses every Windows absolute sessionFile to the current sessionsDir plus basename before the existing agent fallback logic can inspect agents//sessions. If another agent has a transcript with the same basename, resolveAndPersistSessionFile can persist the current agent's path for a Windows-authored path that belonged elsewhere.
    Confidence: 0.89
  • [P2] Reject degenerate Windows session paths — src/config/sessions/paths.ts:198-203
    When a Windows-style path ends with a slash or backslash, extractBasename returns an empty string and this guard falls through to the POSIX relative-path branch. That reproduces the literal drive-letter concatenation this PR is trying to prevent, so the Windows branch should reject or safely normalize paths without a usable filename.
    Confidence: 0.82

Overall correctness: patch is incorrect
Overall confidence: 0.91

Security concerns:

  • [medium] Session path rewrite can alias another agent transcript — src/config/sessions/paths.ts:201
    The basename-only return can turn a Windows-authored cross-agent transcript path into the current agent's local transcript path, and the resolved value is persisted in the session store. That is a data-integrity and agent-isolation concern.
    Confidence: 0.86

Acceptance criteria:

  • node scripts/test-projects.mjs src/config/sessions.test.ts
  • Real Linux or Docker import of a Windows-authored sessions.json with redacted output showing the resolved transcript path

What I checked:

  • Current resolver is host-native for absolutes: Current main trims the persisted sessionFile, uses path.isAbsolute(trimmed), and treats non-absolute values as relative before resolving under the sessions directory; on POSIX, Windows drive-letter paths are not classified as absolute. (src/config/sessions/paths.ts:190, d8d441cd496e)
  • Focused POSIX path check confirms the bug class: A Node path check on this POSIX runner reported both backslash and forward-slash drive-letter examples as non-absolute, which matches the current-main branch used by resolvePathWithinSessionsDir. (d8d441cd496e)
  • Existing tests protect cross-agent session paths: Current tests assert that absolute sessionFile values for another agent preserve that other agent's transcript path, including same-root, cross-root, and sibling-agent option cases. (src/config/sessions.test.ts:654, d8d441cd496e)
  • Resolved sessionFile values are durable: resolveAndPersistSessionFile writes the resolved sessionFile back into the session store when it differs, so an incorrect alias produced by path resolution can become persisted metadata. (src/config/sessions/session-file.ts:31, d8d441cd496e)
  • PR still bypasses fallback flow: The PR head returns path.resolve(path.resolve(sessionsDir), basename) inside the Windows-on-POSIX guard before realBase, sibling-agent, extracted-agent, and structural fallbacks can run. (src/config/sessions/paths.ts:201, 7cc30a64b1e5)
  • Latest maintainer note did not change the risky behavior: The latest comment says the head was rebased, a focused test and changelog entry were added, and helper comments were trimmed while preserving the Windows-style path behavior; it lists tests/build/check but no real imported-session runtime proof. (7cc30a64b1e5)

Likely related people:

  • Peter Steinberger: Current-main blame points to recent edits across the session path resolver, persistence helper, and related tests; Peter also authored the recent structural session path hardening commit. (role: recent maintainer; confidence: high; commits: 5cf4969911ec, fefc414576cb; files: src/config/sessions/paths.ts, src/config/sessions/session-file.ts, src/config/sessions.test.ts)
  • vincentkoc: The behavior appears closely related to the path hardening and fallback resilience work that consolidated session path containment, symlink, and stale metadata handling. (role: path hardening owner; confidence: high; commits: 6a0fcf6518fd; files: src/config/sessions/paths.ts, src/config/sessions.test.ts)
  • Xinhua Gu: The cross-agent structural fallback behavior that this PR can bypass traces to the multi-agent session path fix allowing valid cross-agent sessionFile paths. (role: cross-agent fallback introducer; confidence: medium; commits: 90774c098ade; files: src/config/sessions/paths.ts, src/config/sessions.test.ts)

Remaining risk / open question:

  • Merging as-is can alias a Windows-authored cross-agent transcript path to the current agent's same-basename transcript and persist that alias.
  • The PR still has only test/build/check evidence, not after-fix proof from a real Linux or Docker setup importing a Windows-authored sessions.json.

Codex review notes: model gpt-5.5, reasoning high; reviewed against d8d441cd496e.

When sessions.json is written by a Windows host but read inside a Linux
Docker container, paths like "C:\Users\...\abc.jsonl" are treated as
relative by path.isAbsolute (which only recognises POSIX absolutes).
This causes resolvePathWithinSessionsDir to concatenate the Windows path
onto the container base, producing broken paths like
"/home/node/.openclaw/.../C:\Users\...\abc.jsonl".

Add isWindowsAbsoluteOnPosix() guard that detects drive-letter paths on
POSIX and extracts the leaf filename, resolving it within the sessions
directory instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@BradGroux BradGroux force-pushed the fix/windows-session-paths-on-posix branch from c6075f5 to 7cc30a6 Compare May 8, 2026 07:33
@BradGroux
Copy link
Copy Markdown
Member

Prepared this one for maintainer review/CI.\n\nChanges made on top of the original patch:\n- Rebased onto current origin/main.\n- Added a focused POSIX regression test for persisted Windows absolute sessionFile paths.\n- Added the missing changelog attribution.\n- Trimmed the helper comments while preserving the Windows-style path behavior.\n\nPrepared head: 7cc30a64b1e5db19084961a9f91073af401b9dbf\n\nLocal verification:\n- node scripts/test-projects.mjs src/config/sessions.test.ts ✅ after final rebase\n- pnpm build ✅ before final rebase\n- pnpm check ✅ before final rebase\n\nFresh CI is running now.

@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants