Skip to content

[Bug]: write tool intermittently fails to follow symlinks — "directory component must be a directory" #84696

@garbagenetwork

Description

@garbagenetwork

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

The write tool intermittently fails when the target path traverses a symlink that resolves to a directory. The error message is directory component must be a directory, indicating the path validator sees the symlink as a file instead of resolving it.

Steps to reproduce

Create a symlink in the workspace: ln -s oc_system/memory /home/master/dev/memory
Target is a real directory: /home/master/dev/oc_system/memory/ exists and is drwxrwxr-x
Attempt write to memory/2026-05-20.md — intermittently fails with:

directory component must be a directory

Same call succeeds minutes later, or after writing to a nearby path first

Expected behavior

Expected Behavior:

When the write tool receives a path containing a symlink component that resolves to a directory, it should follow the symlink and write to the target — identical to how read, exec, and the OS itself treat symlinks. Specifically:

  • write("memory/2026-05-20.md") where memory/ is a symlink → oc_system/memory/ should behave identically to write("oc_system/memory/2026-05-20.md")
  • The path validator should resolve each component via realpath() / os.path.realpath() before checking isDirectory(), not stat() the symlink entry directly
  • Behavior should be deterministic: same path, same result, every call — no cold-cache dependency

In short: symlinks should be transparent. The tool should never care whether a directory component is a real directory or a symlink resolving to one.

Actual behavior

Actual Behavior:

On cold cache (first call after session start or context compaction), the write tool's path validator stat()s the symlink entry itself rather than resolving it, sees S_IFLNK instead of S_IFDIR, and rejects the write with:

directory component must be a directory

The failure is intermittent — after successful writes to nearby paths warm the sandbox's internal path cache, subsequent calls to the same symlink path resolve correctly and succeed. This creates inconsistent behavior where:

  • write("memory/2026-05-20.md") → ❌ fails immediately after compaction
  • write("memory/2026-05-20.md") → ✅ succeeds minutes later
  • write("memory/test_new_file.md") → ✅ creating new files through symlink is more reliable
  • write("/home/master/dev/oc_system/memory/2026-05-20.md") → ✅ always works (bypasses symlink)
  • exec("cat > /home/master/dev/memory/2026-05-20.md") → ✅ always works (shell resolves symlinks natively)

Same filesystem, same target, same permissions — the only variable is whether the sandbox path cache has been warmed.

OpenClaw version

2026.5.x (current)

Operating system

Linux 6.8.0-111-generic (x64) - Ubuntu 22 LTS

Install method

npm global

Model

gemma4:31b

Provider / routing chain

ollama loalhost

Additional provider/model setup details

No response

Logs, screenshots, and evidence

The symlink is valid:
lrwxrwxrwx 1 master master 16 May 11 21:24 /home/master/dev/memory -> oc_system/memory

Writing to the real path ALWAYS works
drwxrwxr-x 2 master master 4096 May 20 18:24 /home/master/dev/oc_system/memory/

Writing through symlinks always works
write("/home/master/dev/oc_system/memory/2026-05-20.md") → ✅

WRiting through the symlink works intermittently
write("memory/2026-05-20.md") → ❌ (fails after context compaction or session start)
write("memory/2026-05-20.md") → ✅ (works after a few successful writes to nearby paths)
write("memory/test.md") → ✅ (creating NEW files through symlink seems more reliable)

other symlink writes work fine:
write("memory_test_symlink/test.md") → ✅ (different symlink to same target works)

Impact and severity

Severity: Low
Impact: Low

Here's the reasoning:

Impact — Low:

  • Only affects workspaces that use symlinks under the memory/ path (which is an OpenClaw convention, not a requirement)
  • The exec fallback works 100% of the time — no data loss possible
  • The tool doesn't corrupt data, it just refuses the write
  • Only the write tool is affected; read and exec resolve symlinks fine
  • The failure self-heals after cache warming

Severity — Low:

  • Intermittent, not deterministic — can't reliably reproduce on every call
  • Has a trivial workaround (retry, or use exec to write to the real path)
  • No security implication (symlink already points to an allowed path)
  • No data loss or corruption risk
  • Doesn't affect the memory/ read path at all (session context loads fine)
  • The only real annoyance is burning extra turns on retry or falling back to exec

Why not Medium:

  • It's not a regression from expected behavior — the memory/ symlink setup is non-standard (pointing into oc_system/)
  • Standard workspace setups with real memory/ directories would never hit this
  • The failure mode is noisy but harmless

The case for tracking it at all: Even though it's low/low, it's a real sandbox correctness bug — the path validator should resolve symlinks before checking isDirectory(). If someone later adds symlinks for other reasons (shared configs, cross-workspace references, Docker volume mounts), this will bite again. Worth fixing, not worth rushing.

Additional information

Suspected Root Cause

The write tool's path sandbox validates each directory component before writing. On cold cache (first call after compaction/session start), it appears to stat() the symlink entry itself rather than os.path.realpath() / os.path.realpath(), getting S_IFLNK instead of S_IFDIR, and rejecting the path.

After successful writes warm the sandbox's path cache, subsequent calls resolve correctly.
Suggested Fix

In the path validator, ensure symlink components are resolved with os.path.realpath() or fs.realpathSync() before checking isDirectory(). The fix should be in whatever function walks path components and validates each one is a directory.

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.bugSomething isn't workingclawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions