openclaw - ✅(Solved) Fix [Bug]: `openclaw.json` written with mode 0664 during gateway hot-save on Linux — chmod 0o600 not applied, exposes API keys group-readable [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68709Fetched 2026-04-19 15:08:23
View on GitHub
Comments
1
Participants
2
Timeline
9
Reactions
0
Author
Participants
Timeline (top)
referenced ×3cross-referenced ×2labeled ×2closed ×1

On Linux systemd-managed deployments, openclaw.json ends up with mode 0664 (group-writable, world-readable) after the gateway performs a runtime hot-save (e.g., plugins.entries.<provider>.config.* or agents.list is mutated via openclaw config set or the internal config API). Since openclaw.json contains API keys, tools.exec.security policy, agent model routing, and other auth-adjacent settings, group-readable (and group-writable!) defeats the security model. OpenClaw's own security audit catches this as the fs.config.perms_writable critical finding — which means upstream already considers this state a bug; the writer just doesn't enforce it.

The models-specific writer path already does this correctly:

// dist/models-config-*.js
await fs.chmod(pathname, 384).catch(() => {});  // 384 = 0o600

…but the main-config writer path (that handles mutations to plugins.entries.*, agents.list, etc. in openclaw.json) does not appear to chmod the target after write, OR the chmod is happening and the .catch(() => {}) is silently swallowing a real failure.

Error Message

  1. Make the chmod mandatory and loud. In the main openclaw.json writer, replace await fs.chmod(pathname, 0o600).catch(() => {}) with a version that logs the failure at warn or higher. Silent swallow is the problem; a logged failure lets operators see it. Suggested form: logger.warn({ err, pathname }, 'Failed to chmod config to 0o600 — file may be readable by other users');

Root Cause

  • #56263 — "Allow configurable file permissions (chmod 0o640/0o750) for multi-user setups." This feature request's loosening direction is opposite to this bug's tightening direction, but it's illuminating because it asserts the chmod IS already in place — which contradicts our observed mode. One of us is wrong, probably us (our observation is behavior under silent-failure, not in-code semantics).
  • #51660 — "Configurable umask / file permissions for agent-created user-facing files." Adjacent but covers agent-written files (via exec/write tools), not the main config file.
  • #57971 — "fix(macOS): write gateway LaunchAgent plist with 0600." Exact same anti-pattern on a different file and platform. Suggests there's a broader sweep-needed here: any OpenClaw-written config/credential/policy file should land 0o600 by default.
  • #48295 — "config.apply writes partial/truncated config after validation failure." Different bug, same write path — worth a look for whoever fixes this.

Fix Action

Fix / Workaround

  1. Documentation + systemd unit template. Ship the installed systemd unit with UMask=0077 in [Service] by default. Belt-and-suspenders: even if a future code path forgets the chmod, umask alone gets us to 0600. This is what we ended up doing locally as a workaround.

Local workaround

PR fix notes

PR #68722: fix: chmod openclaw.json to 0o600 after atomic rename on config write

Description (problem / solution / changelog)

Fixes #68709

Problem

The main config writer in src/config/io.ts writes a tmp file with mode: 0o600, then atomically renames it over the target. On the rename success path (Linux), no chmod was applied afterward. Under a permissive process umask (e.g. systemd default UMask=0002), the effective file mode could end up as 0664, exposing API keys and security policy as group-readable.

The copy-fallback path (Windows) already had a chmod, but silently swallowed failures.

The models config writer (src/agents/models-config.ts) already does this correctly.

Fix

  • Add chmod(configPath, 0o600) after successful atomic rename (belt-and-suspenders)
  • Upgrade both rename and copy-fallback chmod to log a warning on failure instead of silently swallowing
  • Add regression test that widens permissions between writes and asserts they are tightened back to 0o600

Test

npx vitest run src/config/io.write-config.test.ts
# 8 passed (8)

The new test:

  1. Writes config (verifies 0o600)
  2. Manually chmod 0o664 to simulate permissive umask
  3. Writes again (verifies chmod restores 0o600)

Changed files

  • src/config/io.ts (modified, +14/-2)
  • src/config/io.write-config.test.ts (modified, +30/-0)

PR #68748: fix: enforce chmod 0o600 after atomic rename of config file

Description (problem / solution / changelog)

Problem

Closes #68709

On Linux, the atomic config write creates a temp file with mode: 0o600, then calls rename() to replace the target. However, rename(2) preserves the existing inode permissions, so the resulting file ends up with the old mode (typically 0664 under systemd default UMask=0002).

This leaves openclaw.json group-readable, exposing API keys, exec security policies, and other secrets to any user in the same group.

Fix

Add chmod(configPath, 0o600) after the successful rename() on the Linux/POSIX path, matching the Windows fallback that already calls chmod(configPath, 0o600) at line 1682.

The chmod is best-effort (wrapped in .catch()) to avoid breaking on read-only filesystems or permission-constrained environments.

Testing

  • Verified the change is on the same pattern as the existing Windows fallback at line 1682
  • rename() path now has parity with copyFile() fallback for permission enforcement

Changed files

  • src/config/io.ts (modified, +3/-0)

Code Example

// dist/models-config-*.js
await fs.chmod(pathname, 384).catch(() => {});  // 384 = 0o600

---

$ systemctl --user show openclaw-gateway -p UMask
   UMask=0002

---

$ openclaw config set plugins.entries.google.config.webSearch.enabled true

---

[reload] config change detected; evaluating reload (agents.list, plugins.entries.google.config.webSearch)
   [reload] config change requires gateway restart (plugins.entries.google.config.webSearch)

---

$ stat -c '%a %n' ~/.openclaw/openclaw.json
   664 /home/<user>/.openclaw/openclaw.json

---

$ openclaw security audit

---

{
     "checkId": "fs.config.perms_writable",
     "severity": "critical",
     "title": "Config file is writable by others",
     "detail": "/home/<user>/.openclaw/openclaw.json mode=664; another user could change gateway/auth/tool policies.",
     "remediation": "chmod 600 /home/<user>/.openclaw/openclaw.json"
   }

---

try {
     await fs.chmod(pathname, 0o600);
   } catch (err) {
     logger.warn({ err, pathname }, 'Failed to chmod config to 0o600 — file may be readable by other users');
   }

---

# ~/.config/systemd/user/openclaw-gateway.service
[Service]
UMask=0077
ExecStart=/path/to/openclaw gateway ...

---

systemctl --user daemon-reload
systemctl --user restart openclaw-gateway

---

systemctl --user show openclaw-gateway -p UMask   # -> UMask=0077
openclaw security audit                            # -> 0 critical

---
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

On Linux systemd-managed deployments, openclaw.json ends up with mode 0664 (group-writable, world-readable) after the gateway performs a runtime hot-save (e.g., plugins.entries.<provider>.config.* or agents.list is mutated via openclaw config set or the internal config API). Since openclaw.json contains API keys, tools.exec.security policy, agent model routing, and other auth-adjacent settings, group-readable (and group-writable!) defeats the security model. OpenClaw's own security audit catches this as the fs.config.perms_writable critical finding — which means upstream already considers this state a bug; the writer just doesn't enforce it.

The models-specific writer path already does this correctly:

// dist/models-config-*.js
await fs.chmod(pathname, 384).catch(() => {});  // 384 = 0o600

…but the main-config writer path (that handles mutations to plugins.entries.*, agents.list, etc. in openclaw.json) does not appear to chmod the target after write, OR the chmod is happening and the .catch(() => {}) is silently swallowing a real failure.

Steps to reproduce

Tested on:

  • OpenClaw 2026.4.15 (Linuxbrew npm install)
  • Linux 6.8.0-110-generic (Ubuntu 24.04)
  • systemd user service (openclaw-gateway.service) with no explicit UMask= directive → systemd default UMask=0002

Steps:

  1. Install OpenClaw as a systemd user service with no UMask= override.
  2. Verify the service's effective umask:
    $ systemctl --user show openclaw-gateway -p UMask
    UMask=0002
  3. Start the gateway and confirm openclaw.json is mode 0600 at rest (freshly created by the installer or by prior Claude Code edits).
  4. Trigger any runtime config mutation that touches paths inside openclaw.json, e.g.:
    $ openclaw config set plugins.entries.google.config.webSearch.enabled true
    (We hit this via an internal mutation of plugins.entries.google.config.webSearch and agents.list — exact UX path in our case was not reconstructed, but the gateway journal confirms a hot-save occurred.)
  5. Observe the gateway log line:
    [reload] config change detected; evaluating reload (agents.list, plugins.entries.google.config.webSearch)
    [reload] config change requires gateway restart (plugins.entries.google.config.webSearch)
  6. Check the file mode:
    $ stat -c '%a %n' ~/.openclaw/openclaw.json
    664 /home/<user>/.openclaw/openclaw.json
  7. Run the built-in audit:
    $ openclaw security audit
    Observed output:
    {
      "checkId": "fs.config.perms_writable",
      "severity": "critical",
      "title": "Config file is writable by others",
      "detail": "/home/<user>/.openclaw/openclaw.json mode=664; another user could change gateway/auth/tool policies.",
      "remediation": "chmod 600 /home/<user>/.openclaw/openclaw.json"
    }

Related issues

  • #56263 — "Allow configurable file permissions (chmod 0o640/0o750) for multi-user setups." This feature request's loosening direction is opposite to this bug's tightening direction, but it's illuminating because it asserts the chmod IS already in place — which contradicts our observed mode. One of us is wrong, probably us (our observation is behavior under silent-failure, not in-code semantics).
  • #51660 — "Configurable umask / file permissions for agent-created user-facing files." Adjacent but covers agent-written files (via exec/write tools), not the main config file.
  • #57971 — "fix(macOS): write gateway LaunchAgent plist with 0600." Exact same anti-pattern on a different file and platform. Suggests there's a broader sweep-needed here: any OpenClaw-written config/credential/policy file should land 0o600 by default.
  • #48295 — "config.apply writes partial/truncated config after validation failure." Different bug, same write path — worth a look for whoever fixes this.

Suggested fix

Three layers, pick one or do all three:

  1. Make the chmod mandatory and loud. In the main openclaw.json writer, replace await fs.chmod(pathname, 0o600).catch(() => {}) with a version that logs the failure at warn or higher. Silent swallow is the problem; a logged failure lets operators see it. Suggested form:

    try {
      await fs.chmod(pathname, 0o600);
    } catch (err) {
      logger.warn({ err, pathname }, 'Failed to chmod config to 0o600 — file may be readable by other users');
    }
  2. Add an integration/invariant test. After any openclaw config set or gateway hot-save, assert stat(openclaw.json).mode & 0o777 === 0o600. One test, catches regressions for all writer paths.

  3. Documentation + systemd unit template. Ship the installed systemd unit with UMask=0077 in [Service] by default. Belt-and-suspenders: even if a future code path forgets the chmod, umask alone gets us to 0600. This is what we ended up doing locally as a workaround.

Local workaround

For anyone hitting this before the fix lands:

# ~/.config/systemd/user/openclaw-gateway.service
[Service]
UMask=0077
ExecStart=/path/to/openclaw gateway ...

Then:

systemctl --user daemon-reload
systemctl --user restart openclaw-gateway

Verify:

systemctl --user show openclaw-gateway -p UMask   # -> UMask=0077
openclaw security audit                            # -> 0 critical

Gateway restart takes ~60s in our deployment; transient downtime.

Expected behavior

After any OpenClaw-initiated write to openclaw.json, the file should be mode 0600 regardless of:

  • The service's (or shell's) inherited umask
  • Whether the writer path originated in CLI, gateway hot-save, or plugin-sdk code

openclaw security audit should not trip fs.config.perms_writable immediately after a fresh gateway-initiated config mutation.

Actual behavior

  • openclaw.json mode flips from 06000664 after the first gateway hot-save on a systemd service with default UMask=0002.
  • The fs.config.perms_writable critical finding appears in openclaw security audit.
  • Any other local user in the openclaw group can now both read AND modify the config — including API keys in Environment= equivalents and, more concerningly, tools.exec.security policy and the exec allowlist boundaries.

OpenClaw version

2026.4.15

Operating system

Ubuntu 24.04, Linux 6.8.0-110-generic

Install method

No response

Model

n/a

Provider / routing chain

n/a

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

  • Security: openclaw.json contains bot tokens (via tokenFile references), API keys in Environment= lines (for the systemd unit), and — most critically — the tools.exec.security allowlist boundary. Another local user in the same group can pivot to arbitrary exec by modifying the allowlist. For single-user deployments this is a non-issue; for shared hosts (multi-user Linux, shared CI runners, etc.) this is a real boundary break.
  • Operational: Every security audit run after a gateway hot-save reports a false-looking critical (it's not false — the file is genuinely exposed — but it's "fixed then re-broken" each time the gateway hot-saves). Operators have no stable baseline.
  • Reproducibility: Not hypothetical — reproduced deterministically in our 2026.4.15 deployment. Journal timestamp 13:20:07 MDT; audit caught it within the same hour.

Additional information

No response

extent analysis

TL;DR

To fix the issue of openclaw.json having incorrect permissions after a runtime hot-save, modify the main-config writer to ensure the file is chmodded to 0600 after write, and consider setting UMask=0077 in the systemd unit file.

Guidance

  • Verify that the fs.chmod call in the main-config writer is executed successfully and logs any failures, rather than silently swallowing errors.
  • Consider adding an integration test to assert the correct file mode after any openclaw config set or gateway hot-save.
  • Set UMask=0077 in the systemd unit file to ensure the file is created with the correct permissions, even if the code path forgets to chmod.
  • Check the file mode after a hot-save using stat -c '%a %n' ~/.openclaw/openclaw.json to verify the fix.

Example

try {
  await fs.chmod(pathname, 0o600);
} catch (err) {
  logger.warn({ err, pathname }, 'Failed to chmod config to 0o600 — file may be readable by other users');
}

Notes

The issue is specific to Linux systemd-managed deployments and may not affect other environments. The suggested fix focuses on ensuring the correct file mode is set after a hot-save, and setting UMask=0077 in the systemd unit file provides an additional layer of protection.

Recommendation

Apply the workaround by setting UMask=0077 in the systemd unit file, as this provides a simple and effective solution to ensure the correct file mode, even if the code path forgets to chmod. This change can be made while waiting for a more permanent fix to be implemented in the code.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

After any OpenClaw-initiated write to openclaw.json, the file should be mode 0600 regardless of:

  • The service's (or shell's) inherited umask
  • Whether the writer path originated in CLI, gateway hot-save, or plugin-sdk code

openclaw security audit should not trip fs.config.perms_writable immediately after a fresh gateway-initiated config mutation.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: `openclaw.json` written with mode 0664 during gateway hot-save on Linux — chmod 0o600 not applied, exposes API keys group-readable [2 pull requests, 1 comments, 2 participants]