openclaw - 💡(How to fix) Fix Outbound channel media uploads silently fail with bare `500: Internal Server Error` for files at `0o600` (write-side leak in sandbox FS bridge + read-side hides the actual reason)

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…

Channel-tool media uploads (Discord upload-file, send with filePath, etc.) return a generic 500: Internal Server Error with no body when the source file is mode 0o600. Any non-trivial bot flow that takes a browser screenshot and tries to send it to the user hits this. The error is silent and undebuggable from the agent side.

This is a write-side + read-side interaction:

  • The gateway's macOS LaunchAgent sets Umask=63 (0o077), and at least one file-write helper hardcodes 0o600 for new files.
  • The 2026.5.7 safe-FS rewrite then refuses to read those files for outbound channel sends.
  • The reject surfaces as a bare HTTP 500 to the tool layer, with no useful message.

Error Message

def create_temp_file(parent_fd, basename): prefix = '.openclaw-write-' + basename + '.' for _ in range(128): candidate = prefix + secrets.token_hex(6) try: fd = os.open(candidate, WRITE_FLAGS, 0o600, dir_fd=parent_fd) # ← hardcoded return candidate, fd except FileExistsError: continue

def write_atomic(parent_fd, basename, stdin_buffer): target_mode = existing_regular_file_mode(parent_fd, basename) # ... if target_mode is not None: os.fchmod(temp_fd, target_mode) # only restores mode if file already existed # else: no chmod, the new file inherits the temp's hardcoded 0o600

Root Cause

Root cause

Fix Action

Fix / Workaround

VersionChange
2026.3.2Umask=077 added to gateway LaunchAgent template (#31919, fixes #31905). Intentional hardening for daemon-private state.
2026.3.24-beta.1"Media/store: enforce the intended media file mode after writes and redirect downloads so restrictive umasks do not silently narrow saved media permissions." Partial — saveMediaBuffer only.
2026.5.2"Sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch." Fixes #44077.
2026.5.7Safe-FS rewrite (readLocalFileSafely, plugin SDK media-runtime) — exposes the leak as a hard 500 in the outbound path.

Workaround (for users hitting this today)

Code Example

# 1) Agent uses browser tool to take a screenshot, OR exec subprocess writes a PNG
#    (any path that inherits the gateway's 0o077 umask).
# 2) File ends up at 0o600:
ls -la ~/.openclaw/media/<screenshot>.png
# -rw-------  1 user  staff  64682 May  8 15:37 screenshot.png

# 3) Try to upload it via the message tool:
openclaw message upload-file --channel discord --target user:<id> \
  --filePath ~/.openclaw/media/<screenshot>.png
# → {"error": "500: Internal Server Error"}    (no body, no stack)

# 4) chmod 644 the file and retry:
chmod 644 ~/.openclaw/media/<screenshot>.png
openclaw message upload-file --channel discord --target user:<id> \
  --filePath ~/.openclaw/media/<screenshot>.png
# → success

---

def create_temp_file(parent_fd, basename):
    prefix = '.openclaw-write-' + basename + '.'
    for _ in range(128):
        candidate = prefix + secrets.token_hex(6)
        try:
            fd = os.open(candidate, WRITE_FLAGS, 0o600, dir_fd=parent_fd)  # ← hardcoded
            return candidate, fd
        except FileExistsError:
            continue

def write_atomic(parent_fd, basename, stdin_buffer):
    target_mode = existing_regular_file_mode(parent_fd, basename)
    # ...
    if target_mode is not None:
        os.fchmod(temp_fd, target_mode)   # only restores mode if file already existed
    # else: no chmod, the new file inherits the temp's hardcoded 0o600

---

def write_atomic(parent_fd, basename, stdin_buffer):
    target_mode = existing_regular_file_mode(parent_fd, basename)
    # ...
    if target_mode is not None:
        os.fchmod(temp_fd, target_mode)
    else:
        # New file: temp was opened at the hardcoded 0o600. Outbound media must be
        # readable by the same process AND survive the safe-FS outbound check.
        # Apply the documented MEDIA_FILE_MODE (0o644) so saved screenshots/downloads
        # behave like the existing saveMediaBuffer fix path (2026.3.24-beta.1).
        os.fchmod(temp_fd, 0o644)

---

OUTBOUND_MEDIA_PERMS_TOO_RESTRICTIVE
File <path> has mode 0600 and cannot be sent over an outbound channel.
Either chmod 644 the file, or write it via saveMediaBuffer (which sets 0o644).
RAW_BUFFERClick to expand / collapse

Summary

Channel-tool media uploads (Discord upload-file, send with filePath, etc.) return a generic 500: Internal Server Error with no body when the source file is mode 0o600. Any non-trivial bot flow that takes a browser screenshot and tries to send it to the user hits this. The error is silent and undebuggable from the agent side.

This is a write-side + read-side interaction:

  • The gateway's macOS LaunchAgent sets Umask=63 (0o077), and at least one file-write helper hardcodes 0o600 for new files.
  • The 2026.5.7 safe-FS rewrite then refuses to read those files for outbound channel sends.
  • The reject surfaces as a bare HTTP 500 to the tool layer, with no useful message.

Reproduction

# 1) Agent uses browser tool to take a screenshot, OR exec subprocess writes a PNG
#    (any path that inherits the gateway's 0o077 umask).
# 2) File ends up at 0o600:
ls -la ~/.openclaw/media/<screenshot>.png
# -rw-------  1 user  staff  64682 May  8 15:37 screenshot.png

# 3) Try to upload it via the message tool:
openclaw message upload-file --channel discord --target user:<id> \
  --filePath ~/.openclaw/media/<screenshot>.png
# → {"error": "500: Internal Server Error"}    (no body, no stack)

# 4) chmod 644 the file and retry:
chmod 644 ~/.openclaw/media/<screenshot>.png
openclaw message upload-file --channel discord --target user:<id> \
  --filePath ~/.openclaw/media/<screenshot>.png
# → success

Reproducible with any 0o600 file under an allowed media root (DM and guild channel targets, with and without caption, with path/filePath/media/MEDIA: directives).

Root cause

Write side — 0o600 hardcoded in the sandbox FS bridge mutation helper

src/agents/sandbox/fs-bridge-mutation-helper.ts, embedded Python (compiled into dist/browser-bridges-*.js):

def create_temp_file(parent_fd, basename):
    prefix = '.openclaw-write-' + basename + '.'
    for _ in range(128):
        candidate = prefix + secrets.token_hex(6)
        try:
            fd = os.open(candidate, WRITE_FLAGS, 0o600, dir_fd=parent_fd)  # ← hardcoded
            return candidate, fd
        except FileExistsError:
            continue

def write_atomic(parent_fd, basename, stdin_buffer):
    target_mode = existing_regular_file_mode(parent_fd, basename)
    # ...
    if target_mode is not None:
        os.fchmod(temp_fd, target_mode)   # only restores mode if file already existed
    # else: no chmod, the new file inherits the temp's hardcoded 0o600

So first-time writes always land at 0o600, regardless of process umask. Overwrites are correct (preserve existing mode).

The MEDIA_FILE_MODE = 0o644 policy in src/media/store.ts is the right intent — but it lives only in the saveMediaBuffer / fileStore path. Writes routed through other paths (sandbox helper, exec subprocesses inheriting the daemon's Umask=077, possibly other write tools) miss it.

Read side — bare 500 from outbound safe-FS

The 2026.5.7 safe-FS / media-runtime rewrite added gating that rejects (or fails to read?) 0o600 files in the outbound flow. Whatever the exact mechanism, the error to the tool layer is 500: Internal Server Error with no body — no EACCES, no path, no reason. Same file at 0o644 works.

Timeline (from CHANGELOG)

VersionChange
2026.3.2Umask=077 added to gateway LaunchAgent template (#31919, fixes #31905). Intentional hardening for daemon-private state.
2026.3.24-beta.1"Media/store: enforce the intended media file mode after writes and redirect downloads so restrictive umasks do not silently narrow saved media permissions." Partial — saveMediaBuffer only.
2026.5.2"Sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch." Fixes #44077.
2026.5.7Safe-FS rewrite (readLocalFileSafely, plugin SDK media-runtime) — exposes the leak as a hard 500 in the outbound path.

The 0600 collapse class of bug has been chased across multiple paths over the last year. The browser/sandbox pinned-write helper was missed every time. 2026.5.7 turned a silent perms drift into a loud (but undebuggable) failure.

Proposed fix

1. Write side — fchmod the temp fd to 0o644 before atomic publish (when the target is a new file)

In fs-bridge-mutation-helper.ts (and any equivalent helper in the browser bridge bundle), after the bytes are written and before os.replace(...):

def write_atomic(parent_fd, basename, stdin_buffer):
    target_mode = existing_regular_file_mode(parent_fd, basename)
    # ...
    if target_mode is not None:
        os.fchmod(temp_fd, target_mode)
    else:
        # New file: temp was opened at the hardcoded 0o600. Outbound media must be
        # readable by the same process AND survive the safe-FS outbound check.
        # Apply the documented MEDIA_FILE_MODE (0o644) so saved screenshots/downloads
        # behave like the existing saveMediaBuffer fix path (2026.3.24-beta.1).
        os.fchmod(temp_fd, 0o644)

Why this approach (vs alternatives):

  • os.open(..., mode=0o644) directly: doesn't work — the mode is masked by the process umask 0o077, so the file still lands at 0o600.
  • Reset umask around the write: race condition between threads, unsafe.
  • Drop the LaunchAgent Umask=077: removes deliberate hardening for daemon-private state files.
  • os.fchmod(temp_fd, 0o644) before os.replace: atomic via fd, single syscall, no race window. Same family as the existing os.fchmod(temp_fd, target_mode) line right above it. ✅

2. Read side — typed error instead of bare 500

The outbound rejection path should propagate a structured error like:

OUTBOUND_MEDIA_PERMS_TOO_RESTRICTIVE
File <path> has mode 0600 and cannot be sent over an outbound channel.
Either chmod 644 the file, or write it via saveMediaBuffer (which sets 0o644).

A bare HTTP 500 for a deterministic, application-level rejection is a UX regression that makes this bug effectively undebuggable from the agent's perspective. Any agent author who hits it has no idea what to fix.

3. Optional — config knob for operators

tools.message.outboundMediaPolicy.allowOwnerOnly: boolean (default false to keep the current security stance), so operators with their own defenses can opt out without forking. Currently there is no escape hatch.

4. Doc — the contract

docs/channels/*.md and docs/tools/message.md should state explicitly:

  • Files passed to channel send / upload-file must be group-readable (0o644+).
  • The OpenClaw-managed media dir contract is: gateway-managed writers (saveMediaBuffer, sandbox helpers, etc.) must produce media files at 0o644 so they survive both the outbound safe-FS check and any reload/restart shimmying.

Open question for maintainers

What's the recommended path for an agent to send a screenshot taken via the browser tool to the user?

Right now the implicit answer seems to be "browser tool → saveMediaBufferMEDIA: reference → channel send." That works in theory, but:

  • The bare 500 on upload-file with a path/filePath param suggests the perms gate is on the read side regardless of how the file got there.
  • Agents authoring real workflows often need to take a screenshot, do post-processing (annotate, crop, compose into a multi-image grid), then send. The post-processing step often writes via fs.writeFile or shell exec, both of which inherit the daemon's Umask=077 and produce 0o600 files.

Is the intent that:

  1. All outbound media must go through saveMediaBuffer? If so, this should be loud — upload-file with a raw filesystem path should be deprecated, or all internal write helpers should be routed through saveMediaBuffer so the perms are correct by construction.
  2. Or is upload-file <localPath> a supported public contract? Then the perms gate should either be relaxed for same-uid reads, or the write-side leak should be plugged everywhere it exists.

Happy to PR option 1 (fchmod fix in the sandbox helper) immediately if maintainers agree direction. Option 2 (typed error from the outbound reject) is a separate small PR. Option 3 (config knob) is direction-setting only.

Workaround (for users hitting this today)

Event-driven WatchPaths LaunchAgent watching ~/.openclaw/media/, ~/.openclaw/tmp/, ~/.openclaw/canvas/ that runs find -perm 600 -o -perm 640 -exec chmod 644 {} \;. Sub-second latency, zero-CPU when idle. Works around the leak until the proper fix lands. Plist + script available on request.

Environment

  • macOS 14 / Apple Silicon, OpenClaw 2026.5.7 via Homebrew global install
  • Gateway via ai.openclaw.gateway.plist LaunchAgent with <integer>63</integer> (= 0o077) Umask
  • Discord plugin: @openclaw/discord 2026.5.7
  • Reproducible across DM and guild channel targets
  • Plain text sends unaffected; only file-attached sends fail

TL;DR

  • Umask=077 on the gateway: deliberate, fine.
  • Browser/sandbox pinned-write helper opens new files at 0o600 and never chmods them: unintentional, never fully fixed.
  • 2026.5.7 outbound safe-FS now hard-rejects 0o600 reads with no actionable error: hard regression in debuggability.
  • Fix: os.fchmod(temp_fd, 0o644) before os.replace when target is new, plus a typed error from the outbound reject path.

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…

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 - 💡(How to fix) Fix Outbound channel media uploads silently fail with bare `500: Internal Server Error` for files at `0o600` (write-side leak in sandbox FS bridge + read-side hides the actual reason)