Skip to content

fix: atomic file writes on Windows-mounted Docker volumes#53965

Open
jhawpetoss6-collab wants to merge 1 commit intoopenclaw:mainfrom
jhawpetoss6-collab:strike/atomic-write-windows-docker-fix
Open

fix: atomic file writes on Windows-mounted Docker volumes#53965
jhawpetoss6-collab wants to merge 1 commit intoopenclaw:mainfrom
jhawpetoss6-collab:strike/atomic-write-windows-docker-fix

Conversation

@jhawpetoss6-collab
Copy link
Copy Markdown

This PR fixes a crash when OpenClaw attempts to write config or auth files while running in Docker on a Windows host (#53947).

Changes:

  • Wrapped chmodSync in a try/catch to handle EPERM on NTFS/CIFS mounts.
  • Aligns synchronous file writing with the existing async implementation.
  • Essential for stability on Windows+Docker Desktop environments.

/claim #53947

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR intends to fix EPERM crashes on chmodSync when OpenClaw writes files on Windows-mounted Docker volumes, but the fix does not accomplish its goal. A new utility file (src/shared/fs-atomic-fix.ts) is added with a guarded writeTextFileAtomicSync function, but it is never imported or used anywhere in the codebase. The actual crash-prone function — writeTextFileAtomic in src/secrets/shared.ts — still calls fs.chmodSync without any error handling, so issue #53947 will continue to reproduce on every atomic write operation.

  • P0 – Fix is orphaned: writeTextFileAtomicSync is defined but has zero callers. The existing writeTextFileAtomic (called from src/secrets/apply.ts and elsewhere) is untouched and still throws EPERM on NTFS/CIFS mounts. writeJsonFileSecure in the same file has the same unguarded chmodSync and also needs patching.
  • P1 – Missing directory guard: The new function skips the ensureDirForFile call that the existing implementation relies on, which would cause ENOENT failures when the parent directory doesn't exist.
  • P2 – Import style: Uses "fs" instead of the "node:fs" protocol prefix used consistently throughout the codebase.

Confidence Score: 1/5

  • Not safe to merge — the fix is completely disconnected from the broken code path it claims to repair.
  • The new file is never wired into the rest of the codebase. The crash reported in writeTextFileAtomic (sync) crashes with EPERM on Docker volumes mounted from Windows #53947 originates in writeTextFileAtomic inside src/secrets/shared.ts, which is unchanged. Merging this PR would close the issue ticket without actually fixing the bug, leaving all production users on Windows+Docker still exposed to the crash.
  • src/secrets/shared.tswriteTextFileAtomic (line 62) and writeJsonFileSecure (line 48) both need the chmodSync guard; the new src/shared/fs-atomic-fix.ts needs to either be deleted or properly integrated.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/shared/fs-atomic-fix.ts
Line: 1-19

Comment:
**Fix is orphaned — crash still occurs**

`writeTextFileAtomicSync` is defined here but never imported or called anywhere in the codebase. The function that actually triggers the crash is `writeTextFileAtomic` in `src/secrets/shared.ts` (line 62), which still contains the unprotected `fs.chmodSync` call:

```typescript
// src/secrets/shared.ts – still unpatched
fs.chmodSync(tempPath, mode);
```

All callers (e.g. `src/secrets/apply.ts` line 829, `restoreFileSnapshot` at line 737) go through that existing function. Until `src/secrets/shared.ts` is updated, issue #53947 will continue to reproduce on every write.

The fix should be applied directly to `writeTextFileAtomic` in `src/secrets/shared.ts`:

```typescript
export function writeTextFileAtomic(pathname: string, value: string, mode = 0o600): void {
  ensureDirForFile(pathname);
  const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
  fs.writeFileSync(tempPath, value, "utf8");
  try {
    fs.chmodSync(tempPath, mode);
  } catch {
    // EPERM expected on NTFS/CIFS mounts (Windows Docker Desktop)
  }
  fs.renameSync(tempPath, pathname);
}
```

`writeJsonFileSecure` (line 48 of `src/secrets/shared.ts`) has the same unguarded `chmodSync` and should also be patched.

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/shared/fs-atomic-fix.ts
Line: 8-9

Comment:
**Missing `ensureDirForFile` before write**

The existing `writeTextFileAtomic` in `src/secrets/shared.ts` calls `ensureDirForFile(pathname)` before creating the temp file (line 59), ensuring the parent directory exists. This new implementation skips that step and will throw `ENOENT` if the destination directory doesn't already exist — a regression compared to the function it intends to replace.

```suggestion
    const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
    fs.mkdirSync(require("path").dirname(pathname), { recursive: true, mode: 0o700 });
    fs.writeFileSync(tempPath, value, "utf8");
```

Or import and call the shared `ensureDirForFile` helper if this file is kept.

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/shared/fs-atomic-fix.ts
Line: 1

Comment:
**Node built-in import convention**

The rest of the codebase uses the `node:` protocol prefix for built-in modules (e.g. `import fs from "node:fs"` in `src/secrets/shared.ts`, `src/secrets/apply.ts`, etc.). The bare `"fs"` import here is inconsistent.

```suggestion
import fs from "node:fs";
```

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

Reviews (1): Last reviewed commit: "fix: handle EPERM on chmod during atomic..." | Re-trigger Greptile

Comment on lines +1 to +19
import fs from "fs";

/**
* Handles EPERM issues during chmod on Windows-mounted Docker volumes.
* Addresses #53947.
*/
export function writeTextFileAtomicSync(pathname: string, value: string, mode: number = 0o600) {
const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tempPath, value, "utf8");

try {
// This fails on Windows-mounted volumes in Docker
fs.chmodSync(tempPath, mode);
} catch (e) {
// Silently continue if chmod fails (EPERM expected on NTFS/CIFS mounts)
}

fs.renameSync(tempPath, pathname);
}
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.

P0 Fix is orphaned — crash still occurs

writeTextFileAtomicSync is defined here but never imported or called anywhere in the codebase. The function that actually triggers the crash is writeTextFileAtomic in src/secrets/shared.ts (line 62), which still contains the unprotected fs.chmodSync call:

// src/secrets/shared.ts – still unpatched
fs.chmodSync(tempPath, mode);

All callers (e.g. src/secrets/apply.ts line 829, restoreFileSnapshot at line 737) go through that existing function. Until src/secrets/shared.ts is updated, issue #53947 will continue to reproduce on every write.

The fix should be applied directly to writeTextFileAtomic in src/secrets/shared.ts:

export function writeTextFileAtomic(pathname: string, value: string, mode = 0o600): void {
  ensureDirForFile(pathname);
  const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
  fs.writeFileSync(tempPath, value, "utf8");
  try {
    fs.chmodSync(tempPath, mode);
  } catch {
    // EPERM expected on NTFS/CIFS mounts (Windows Docker Desktop)
  }
  fs.renameSync(tempPath, pathname);
}

writeJsonFileSecure (line 48 of src/secrets/shared.ts) has the same unguarded chmodSync and should also be patched.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/shared/fs-atomic-fix.ts
Line: 1-19

Comment:
**Fix is orphaned — crash still occurs**

`writeTextFileAtomicSync` is defined here but never imported or called anywhere in the codebase. The function that actually triggers the crash is `writeTextFileAtomic` in `src/secrets/shared.ts` (line 62), which still contains the unprotected `fs.chmodSync` call:

```typescript
// src/secrets/shared.ts – still unpatched
fs.chmodSync(tempPath, mode);
```

All callers (e.g. `src/secrets/apply.ts` line 829, `restoreFileSnapshot` at line 737) go through that existing function. Until `src/secrets/shared.ts` is updated, issue #53947 will continue to reproduce on every write.

The fix should be applied directly to `writeTextFileAtomic` in `src/secrets/shared.ts`:

```typescript
export function writeTextFileAtomic(pathname: string, value: string, mode = 0o600): void {
  ensureDirForFile(pathname);
  const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
  fs.writeFileSync(tempPath, value, "utf8");
  try {
    fs.chmodSync(tempPath, mode);
  } catch {
    // EPERM expected on NTFS/CIFS mounts (Windows Docker Desktop)
  }
  fs.renameSync(tempPath, pathname);
}
```

`writeJsonFileSecure` (line 48 of `src/secrets/shared.ts`) has the same unguarded `chmodSync` and should also be patched.

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

Comment on lines +8 to +9
const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tempPath, value, "utf8");
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.

P1 Missing ensureDirForFile before write

The existing writeTextFileAtomic in src/secrets/shared.ts calls ensureDirForFile(pathname) before creating the temp file (line 59), ensuring the parent directory exists. This new implementation skips that step and will throw ENOENT if the destination directory doesn't already exist — a regression compared to the function it intends to replace.

Suggested change
const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tempPath, value, "utf8");
const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
fs.mkdirSync(require("path").dirname(pathname), { recursive: true, mode: 0o700 });
fs.writeFileSync(tempPath, value, "utf8");

Or import and call the shared ensureDirForFile helper if this file is kept.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/shared/fs-atomic-fix.ts
Line: 8-9

Comment:
**Missing `ensureDirForFile` before write**

The existing `writeTextFileAtomic` in `src/secrets/shared.ts` calls `ensureDirForFile(pathname)` before creating the temp file (line 59), ensuring the parent directory exists. This new implementation skips that step and will throw `ENOENT` if the destination directory doesn't already exist — a regression compared to the function it intends to replace.

```suggestion
    const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
    fs.mkdirSync(require("path").dirname(pathname), { recursive: true, mode: 0o700 });
    fs.writeFileSync(tempPath, value, "utf8");
```

Or import and call the shared `ensureDirForFile` helper if this file is kept.

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

@@ -0,0 +1,19 @@
import fs from "fs";
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 Node built-in import convention

The rest of the codebase uses the node: protocol prefix for built-in modules (e.g. import fs from "node:fs" in src/secrets/shared.ts, src/secrets/apply.ts, etc.). The bare "fs" import here is inconsistent.

Suggested change
import fs from "fs";
import fs from "node:fs";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/shared/fs-atomic-fix.ts
Line: 1

Comment:
**Node built-in import convention**

The rest of the codebase uses the `node:` protocol prefix for built-in modules (e.g. `import fs from "node:fs"` in `src/secrets/shared.ts`, `src/secrets/apply.ts`, etc.). The bare `"fs"` import here is inconsistent.

```suggestion
import fs from "node:fs";
```

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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: bc3f5681f7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

* Handles EPERM issues during chmod on Windows-mounted Docker volumes.
* Addresses #53947.
*/
export function writeTextFileAtomicSync(pathname: string, value: string, mode: number = 0o600) {
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 Wire this helper into active sync write paths

This commit adds writeTextFileAtomicSync, but it is never imported or called anywhere (repo-wide search for writeTextFileAtomicSync|fs-atomic-fix only matches this new file), so runtime behavior is unchanged. The existing synchronous writers that still do direct fs.chmodSync (for example src/secrets/shared.ts:62 and src/infra/json-file.ts:22) can still throw EPERM on Windows-mounted Docker volumes, so the crash this commit claims to fix remains.

Useful? React with 👍 / 👎.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 29, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: needs changes before merge.

Summary
The PR adds a new synchronous atomic text writer in src/shared/fs-atomic-fix.ts that catches chmodSync errors before renaming the temp file.

Reproducibility: yes. for the PR defect: source inspection shows the new helper has no callers, while runSecretsApply still reaches the unguarded writeTextFileAtomic path. A focused test can mock fs.chmodSync throwing EPERM on that active path without requiring a live Windows/Docker mount.

Next step before merge
The blocking defects are narrow and file-level: replace the orphan helper with a fix on the active secrets writer, add regression coverage, and include the required changelog entry.

Security
Needs attention: The diff has no dependency or workflow change, but its proposed credential/config writer suppresses all chmod hardening failures if wired in unchanged.

Review findings

  • [P1] Wire the helper into active writers — src/shared/fs-atomic-fix.ts:7
  • [P2] Preserve parent directory creation — src/shared/fs-atomic-fix.ts:8-9
  • [P2] Narrow the chmod fallback — src/shared/fs-atomic-fix.ts:14-15
Review details

Best possible solution:

Replace the orphan helper with a focused change on the active secrets persistence writer, preserving parent-directory creation and credential-file permissions while adding targeted regression coverage and a changelog entry.

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

Yes for the PR defect: source inspection shows the new helper has no callers, while runSecretsApply still reaches the unguarded writeTextFileAtomic path. A focused test can mock fs.chmodSync throwing EPERM on that active path without requiring a live Windows/Docker mount.

Is this the best way to solve the issue?

No. Adding a disconnected helper is not the narrow maintainable fix; the existing writer or a shared helper actually used by it needs the guarded behavior while preserving directory creation and permission semantics.

Full review comments:

  • [P1] Wire the helper into active writers — src/shared/fs-atomic-fix.ts:7
    This adds writeTextFileAtomicSync, but nothing imports or calls it. Current secrets apply still uses writeTextFileAtomic in src/secrets/shared.ts, whose chmodSync remains unguarded, so merging this leaves the reported Windows/Docker crash unchanged.
    Confidence: 0.98
  • [P2] Preserve parent directory creation — src/shared/fs-atomic-fix.ts:8-9
    The new writer creates the temp file without first ensuring the destination directory exists. The existing writer calls ensureDirForFile(pathname), so wiring this helper in would regress new auth/env writes to ENOENT when the parent directory is missing.
    Confidence: 0.94
  • [P2] Narrow the chmod fallback — src/shared/fs-atomic-fix.ts:14-15
    The catch suppresses every chmodSync failure. If this helper is used for auth/config writes, unexpected permission hardening failures can leave the file written with default permissions while the caller sees success.
    Confidence: 0.84

Overall correctness: patch is incorrect
Overall confidence: 0.96

Security concerns:

  • [medium] Broad chmod failure suppression on credential writes — src/shared/fs-atomic-fix.ts:14
    The helper is intended for config/auth persistence and catches every chmodSync error. A correct fix should scope the fallback to the expected Windows-mounted filesystem condition and keep unexpected permission failures visible.
    Confidence: 0.82

Acceptance criteria:

  • pnpm test src/secrets/apply.test.ts
  • pnpm test src/infra/json-file.test.ts src/infra/json-files.test.ts
  • pnpm exec oxfmt --check --threads=1 src/secrets/shared.ts src/secrets/apply.test.ts CHANGELOG.md
  • pnpm check:changed in Testbox before handoff

What I checked:

Likely related people:

  • joshavant: History shows the audit/configure/apply flow and later SecretRef expansion in the secrets files and docs that define this persistence path. (role: introduced and expanded secrets apply surface; confidence: high; commits: f413e314b923, 806803b7efe2, 0ffcc308f2f1; files: src/secrets/shared.ts, src/secrets/apply.ts, docs/cli/secrets.md)
  • Peter Steinberger: History shows repeated secrets refactors plus the Windows fallback for atomic JSON writes that a correct secrets writer fix should align with. (role: recent maintainer and adjacent atomic-write owner; confidence: high; commits: 003f52db98ab, 18f8393b6c67, c9bb6bd0d868; files: src/secrets/shared.ts, src/secrets/apply.ts, src/infra/json-file.ts)
  • Vincent Koc: Current-main blame points to recent touches across src/secrets/shared.ts and src/secrets/apply.ts, with nearby secrets performance commits in history. (role: recent maintainer on current secrets files; confidence: medium; commits: 30e259b9c565, 90e8bef25311, b4d0d6fcc9a1; files: src/secrets/shared.ts, src/secrets/apply.ts)

Remaining risk / open question:

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

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: found issues before merge.

What this changes:

The PR adds src/shared/fs-atomic-fix.ts with a synchronous temp-file writer that ignores chmodSync failures before renaming the temp file into place.

Maintainer follow-up before merge:

This is an open contributor PR with known blocking defects, and the correct repair touches credential/secrets persistence semantics owned by the security-sensitive path; maintainers should request changes or choose a replacement rather than queue an automated repair from this review.

Review findings:

  • [P1] Wire the helper into the active writers — src/shared/fs-atomic-fix.ts:7
  • [P2] Preserve parent directory creation — src/shared/fs-atomic-fix.ts:8-9
Review details

Best possible solution:

Revise this PR or replace it with a focused change on the active secrets persistence path: preserve existing parent-directory creation, handle the Windows-mounted Docker chmod/rename failure where the current writer is actually called, add targeted regression coverage, and keep credential-file permission failures visible enough for operators and secops review.

Full review comments:

  • [P1] Wire the helper into the active writers — src/shared/fs-atomic-fix.ts:7
    This adds writeTextFileAtomicSync, but nothing imports or calls it. Current secrets apply still uses writeTextFileAtomic in src/secrets/shared.ts, whose chmodSync remains unguarded, so merging this leaves the reported Windows/Docker crash unchanged.
    Confidence: 0.98
  • [P2] Preserve parent directory creation — src/shared/fs-atomic-fix.ts:8-9
    The new writer builds and writes the temp file before ensuring the destination directory exists. The existing writer calls ensureDirForFile(pathname), so using this helper for new auth/env files would regress to ENOENT when the parent directory has not been created yet.
    Confidence: 0.94

Overall correctness: patch is incorrect
Overall confidence: 0.96

Acceptance criteria:

  • pnpm test src/secrets/apply.test.ts
  • pnpm test src/infra/json-file.test.ts src/infra/json-files.test.ts
  • pnpm check:changed in Testbox before handoff if code changes

What I checked:

  • Active secrets writer remains unguarded: Current main's writeTextFileAtomic still creates the parent directory, writes a temp file, then directly calls fs.chmodSync(tempPath, mode) and fs.renameSync(tempPath, pathname) with no Windows/Docker fallback; writeJsonFileSecure also still calls fs.chmodSync directly. (src/secrets/shared.ts:55, e46dccb35374)
  • Secrets apply still uses the active writer: Rollback restore and projected auth/env writes still call writeTextFileAtomic, so failures from chmod or rename remain on the user-facing openclaw secrets apply write path. (src/secrets/apply.ts:743, e46dccb35374)
  • PR helper is orphaned: The PR diff adds writeTextFileAtomicSync in a new helper file, but current main has no writeTextFileAtomicSync, fs-atomic-fix, or atomic-fix references, so the proposed runtime behavior is not wired into any caller. (src/shared/fs-atomic-fix.ts:1, bc3f5681f792)
  • Generic atomic JSON fallback is only partial coverage: Current main has Windows EPERM/EEXIST fallbacks in the generic async and sync JSON helpers, but the secrets-specific text writer used by secrets apply does not call those helpers for auth/env writes. (src/infra/json-files.ts:22, e46dccb35374)
  • Credential-bearing write surface: The secrets CLI docs state that apply may update openclaw.json, auth-profiles.json, legacy auth.json, and ~/.openclaw/.env, so a correct fix belongs on the active credential-file persistence path rather than an unused generic helper. Public docs: docs/cli/secrets.md. (docs/cli/secrets.md:176, e46dccb35374)
  • Security review: The PR changes one new TypeScript helper and does not touch workflows, dependencies, lockfiles, install/build/release scripts, package metadata, or downloaded/generated code. The security-relevant concern is functional: if wired in unchanged, it would silently swallow all chmod failures on credential-bearing writes and omit the existing directory guard. (src/shared/fs-atomic-fix.ts:7, bc3f5681f792)

Likely related people:

  • joshavant: GitHub commit metadata for the secrets audit/configure/apply work and later SecretRef expansion ties this person to src/secrets/apply.ts, src/secrets/shared.ts, and the secrets docs that define this persistence path. (role: introduced and expanded secrets apply surface; confidence: high; commits: f413e314b923, 806803b7efe2; files: src/secrets/shared.ts, src/secrets/apply.ts, docs/cli/secrets.md)
  • steipete: Prior ClawSweeper history and current local blame connect this person to the nearby Windows atomic JSON fallback and current maintenance of the affected write-path area that a correct fix should align with. (role: recent maintainer and adjacent Windows atomic-write hardening; confidence: high; commits: 003f52db98ab, 7f3f108521f4; files: src/infra/json-files.ts, src/infra/json-files.test.ts, src/config/io.ts)
  • vincentkoc: Prior GitHub metadata connects this person to recent secrets apply/preflight and test work, which is the likely regression-test surface for this bug. (role: recent adjacent maintainer; confidence: medium; commits: 90e8bef25311, ad2516b1c876; files: src/secrets/apply.ts, src/secrets/shared.ts, src/secrets/apply.test.ts)

Remaining risk / open question:

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

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant