openclaw - 💡(How to fix) Fix [Bug]: `write` tool has no append mode; append-only wrapper is scoped to a memory-flush subsystem and unavailable to general agent runs (cron skills, monitor-and-react workflows) [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#75321Fetched 2026-05-01 05:35:15
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
1
Author
Timeline (top)
closed ×1commented ×1labeled ×1

The standard write tool unconditionally calls fs.writeFile, overwriting any existing content. Append capability exists in OpenClaw via the wrapToolMemoryFlushAppendOnlyWrite wrapper but is reachable only during a special "memory flush run" against one fixed relativePath; cron-driven monitor-and-react skills (the canonical "fetch → match → append-to-log" pattern) have no native way to append to a log/alert/journal file.

Error Message

throw new Error(Memory flush writes are restricted to ${options.relativePath}; use that path only.);

Root Cause

The standard write tool unconditionally calls fs.writeFile, overwriting any existing content. Append capability exists in OpenClaw via the wrapToolMemoryFlushAppendOnlyWrite wrapper but is reachable only during a special "memory flush run" against one fixed relativePath; cron-driven monitor-and-react skills (the canonical "fetch → match → append-to-log" pattern) have no native way to append to a log/alert/journal file.

Fix Action

Fix / Workaround

After N firings, /tmp/append-test.log contains exactly 1 line — only the most recent firing's content. Each write tool call overwrites the previous file. Each cron run reports successful tool dispatch and "wrote N bytes" confirmation in the session jsonl, but file history does not accumulate.

NousResearch/Hermes-3-Llama-3.1-8B (model is irrelevant — the bug is in the tool's filesystem semantics, not the model's tool-call dispatch)

Severity: medium-high. There's a workaround (use exec with shell redirection: exec("echo '<line>' >> /path")), but it (a) requires the exec tool in the allowlist — broader policy surface than write, particularly relevant in sandboxed deployments where exec is more restricted, (b) requires every monitor-and-react skill author to know this gotcha, and (c) silently produces correct-looking behavior without it (each cron firing reports success; the data loss is invisible until the user inspects the file).

Code Example

openclaw cron add \
     --name append-test \
     --every 5m \
     --message "Write a single line containing the current timestamp to /tmp/append-test.log using the write tool. The file may already exist; if it does, your line should be appended below the existing content." \
     --session isolated \
     --tools write,session_status

---

function wrapToolMemoryFlushAppendOnlyWrite(tool, options) {
    const allowedAbsolutePath = path.resolve(options.root, options.relativePath);
    return {
        ...tool,
        description: `${tool.description} During memory flush, this tool may only append to ${options.relativePath}.`,
        execute: async (toolCallId, args, signal, onUpdate) => {
            ...
            if (resolveToolPathAgainstWorkspaceRoot(...) !== allowedAbsolutePath)
                throw new Error(`Memory flush writes are restricted to ${options.relativePath}; use that path only.`);
            await appendMemoryFlushContent({ ... });
            ...

---

Concrete repro from a `bsg-monitor` cron job firing at 5m cadence:

Cron run history (3 successful runs marked `summary: "fetched (HTTP 200), wrote summary to memory/2026-04-30.md, alert written"`):


{
  "entries": [
    {"ts":1777592905558,"status":"...","summary":"bsg-monitor: fetched (HTTP 200), wrote summary to memory/2026-04-30.md, alert written"},
    {"ts":1777592572600,"status":"ok","summary":"bsg-monitor: fetched (HTTP 200), wrote summary to memory/2026-04-30.md, alert written"},
    {"ts":1777592268963,"status":"...","summary":"bsg-monitor: fetched (HTTP 200), wrote summary to memory/2026-04-30.md, no alert written"}
  ]
}


Resulting alert file after 3 successful append-intent firings:


$ wc -l ~/.openclaw/workspace/alerts/bsg-alerts.md
1 ~/.openclaw/workspace/alerts/bsg-alerts.md

$ cat ~/.openclaw/workspace/alerts/bsg-alerts.md
[2026-04-30 23:47 UTC] HIT keywords=Cylon,Galactica,Adama source=https://en.wikipedia.org/api/rest_v1/page/summary/Battlestar_Galactica_(1978_TV_series)


Single line — only the latest firing. Identical pattern on the memory file.

Source-level confirmation in `dist/pi-embedded-Vw-lS5ti.js`:
- Line ~17579: `await fs$1.writeFile(params.absolutePath, next, "utf-8");` — standard write path is unconditional overwrite.
- Line 17581: `function wrapToolMemoryFlushAppendOnlyWrite(tool, options)` — append wrapper exists.
- Line 18235: `if (tool.name === "write") return [wrapToolMemoryFlushAppendOnlyWrite(...)];` — wrapper is *only* applied inside the memory-flush gate (`isMemoryFlushRun && memoryFlushWritePath`).
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

The standard write tool unconditionally calls fs.writeFile, overwriting any existing content. Append capability exists in OpenClaw via the wrapToolMemoryFlushAppendOnlyWrite wrapper but is reachable only during a special "memory flush run" against one fixed relativePath; cron-driven monitor-and-react skills (the canonical "fetch → match → append-to-log" pattern) have no native way to append to a log/alert/journal file.

Steps to reproduce

  1. Install OpenClaw 2026.4.9 standalone on a host.
  2. Author a cron job that invokes a procedure-style prompt asking the agent to write to a fixed log file:
    openclaw cron add \
      --name append-test \
      --every 5m \
      --message "Write a single line containing the current timestamp to /tmp/append-test.log using the write tool. The file may already exist; if it does, your line should be appended below the existing content." \
      --session isolated \
      --tools write,session_status
  3. Wait for 2-3 firings (~10-15 minutes). Inspect /tmp/append-test.log after each firing.

Expected behavior

After 3 firings, /tmp/append-test.log should contain 3 timestamped lines (one per firing). The cron pattern is monitor-and-react; the canonical use case is accumulating an alert/audit log over time.

Actual behavior

After N firings, /tmp/append-test.log contains exactly 1 line — only the most recent firing's content. Each write tool call overwrites the previous file. Each cron run reports successful tool dispatch and "wrote N bytes" confirmation in the session jsonl, but file history does not accumulate.

Observed concretely on 2026-04-30 with a Wikipedia-monitor cron skill: 3 firings at 5m cadence each successfully fetched, summarized, and "wrote" to ~/.openclaw/workspace/alerts/bsg-alerts.md and ~/.openclaw/workspace/memory/2026-04-30.md — but each file held only the latest run's content. Inspection of dist/pi-embedded-Vw-lS5ti.js confirms the standard write tool path uses fs.writeFile (overwrite); the only append code path is the memory-flush wrapper:

function wrapToolMemoryFlushAppendOnlyWrite(tool, options) {
    const allowedAbsolutePath = path.resolve(options.root, options.relativePath);
    return {
        ...tool,
        description: `${tool.description} During memory flush, this tool may only append to ${options.relativePath}.`,
        execute: async (toolCallId, args, signal, onUpdate) => {
            ...
            if (resolveToolPathAgainstWorkspaceRoot(...) !== allowedAbsolutePath)
                throw new Error(`Memory flush writes are restricted to ${options.relativePath}; use that path only.`);
            await appendMemoryFlushContent({ ... });
            ...

The wrapper is gated by isMemoryFlushRun && memoryFlushWritePath, with MEMORY_FLUSH_ALLOWED_TOOL_NAMES = new Set(["read", "write"]) further restricting which tools survive into that subsystem. Cron jobs and normal agent runs never enter this code path.

OpenClaw version

2026.4.9 (build 0512059)

Operating system

Ubuntu 24.04 LTS aarch64 (Linux 6.17.0-1014-nvidia)

Install method

npm global (npm install -g [email protected]), Node v22.22.2 via nvm

Model

NousResearch/Hermes-3-Llama-3.1-8B (model is irrelevant — the bug is in the tool's filesystem semantics, not the model's tool-call dispatch)

Provider / routing chain

openclaw (standalone host gateway) → vLLM (http://127.0.0.1:8002/v1) → Hermes-3-Llama-3.1-8B

Additional provider/model setup details

Standalone OpenClaw on a host (no NemoClaw sandbox). vLLM 0.19.1 Docker container at :8002 with --enable-auto-tool-choice --tool-call-parser hermes. ~/.openclaw/openclaw.json gateway.mode=local, primary model inference/hermes-3-llama-3.1-8b. Cron uses --session isolated so each fire is a fresh session.

Logs, screenshots, and evidence

Concrete repro from a `bsg-monitor` cron job firing at 5m cadence:

Cron run history (3 successful runs marked `summary: "fetched (HTTP 200), wrote summary to memory/2026-04-30.md, alert written"`):


{
  "entries": [
    {"ts":1777592905558,"status":"...","summary":"bsg-monitor: fetched (HTTP 200), wrote summary to memory/2026-04-30.md, alert written"},
    {"ts":1777592572600,"status":"ok","summary":"bsg-monitor: fetched (HTTP 200), wrote summary to memory/2026-04-30.md, alert written"},
    {"ts":1777592268963,"status":"...","summary":"bsg-monitor: fetched (HTTP 200), wrote summary to memory/2026-04-30.md, no alert written"}
  ]
}


Resulting alert file after 3 successful append-intent firings:


$ wc -l ~/.openclaw/workspace/alerts/bsg-alerts.md
1 ~/.openclaw/workspace/alerts/bsg-alerts.md

$ cat ~/.openclaw/workspace/alerts/bsg-alerts.md
[2026-04-30 23:47 UTC] HIT keywords=Cylon,Galactica,Adama source=https://en.wikipedia.org/api/rest_v1/page/summary/Battlestar_Galactica_(1978_TV_series)


Single line — only the latest firing. Identical pattern on the memory file.

Source-level confirmation in `dist/pi-embedded-Vw-lS5ti.js`:
- Line ~17579: `await fs$1.writeFile(params.absolutePath, next, "utf-8");` — standard write path is unconditional overwrite.
- Line 17581: `function wrapToolMemoryFlushAppendOnlyWrite(tool, options)` — append wrapper exists.
- Line 18235: `if (tool.name === "write") return [wrapToolMemoryFlushAppendOnlyWrite(...)];` — wrapper is *only* applied inside the memory-flush gate (`isMemoryFlushRun && memoryFlushWritePath`).

Impact and severity

Affected: every user authoring cron-driven monitor-and-react skills that need to accumulate state on disk — alert/audit logs, daily journals, append-only memory artifacts, time-series-of-fetched-content. This is a foundational pattern for the use case OpenClaw most-prominently markets ("dozens of agentic tasks running on a schedule").

Severity: medium-high. There's a workaround (use exec with shell redirection: exec("echo '<line>' >> /path")), but it (a) requires the exec tool in the allowlist — broader policy surface than write, particularly relevant in sandboxed deployments where exec is more restricted, (b) requires every monitor-and-react skill author to know this gotcha, and (c) silently produces correct-looking behavior without it (each cron firing reports success; the data loss is invisible until the user inspects the file).

Frequency: deterministic on every cron firing that uses write for accumulation.

Consequence: silent history loss on monitor-and-react workflows. Skill authors who don't know about the limitation will think their cron jobs are accumulating state — and may not discover otherwise until they need to query the history of alerts, leading to misdiagnosis of agent-loop reliability when the actual issue is tool-level filesystem semantics.

Additional information

Suggested fixes (any one materially helps; combining is best):

  1. Add mode: "overwrite" | "append" argument to the standard write tool, defaulting to "overwrite" for backwards compatibility. Single-tool change, no policy implications, smallest blast radius. Schema documents the option; existing callers continue working unchanged.
  2. Generalize wrapToolMemoryFlushAppendOnlyWrite to be available outside the memory-flush subsystem when invoked with explicit append intent. The wrapper already implements append-with-path-validation correctly; just expose it to general callers via a config or per-tool-call argument.
  3. Document exec shell-redirect (>>) as the canonical workaround in OpenClaw's "writing skills" docs, plus a recipe for monitor-and-react append patterns. Lower-impact fix that at least surfaces the gotcha to skill authors.
  4. Provide a separate append tool (tools/append, distinct from write). More duplication of code paths, but cleaner separation for tool-allowlist policy decisions.

Recommended ordering: ship #3 immediately as a docs PR (zero code risk); ship #1 in the next minor release (smallest functional change); revisit #2 / #4 only if #1 surfaces design pressure.

Related issues:

  • Adjacent context: many of the open OpenClaw issues around bootstrap-bloat / tool-call reliability (#22438, #41304, #66060, plus our own #75184 / #75187 / #75189) operate under an unstated assumption that monitor-and-react cron workflows can durably accumulate state. This bug shows that assumption doesn't hold today without exec.
  • #62182 (closed)bootstrapMaxCharsPerFile rejected; the closing comment confirmed agents.defaults.bootstrapMaxChars and bootstrapTotalMaxChars as the supported surface for bootstrap config. By analogy, this bug asks: what is the supported surface for "append to a workspace file from a cron job?" — and right now the answer is "exec with shell redirection," which is undocumented and forces a broader policy surface than is necessary.

extent analysis

TL;DR

The most likely fix for the issue is to add an mode argument to the standard write tool, allowing users to specify whether to overwrite or append to a file.

Guidance

  • The root cause of the issue is that the standard write tool unconditionally overwrites any existing content, rather than appending to it.
  • To mitigate this issue, users can use the exec tool with shell redirection (>>) as a workaround, but this requires the exec tool to be in the allowlist and may have broader policy implications.
  • A more permanent fix would be to add a mode argument to the write tool, allowing users to specify whether to overwrite or append to a file.
  • Another possible solution is to generalize the wrapToolMemoryFlushAppendOnlyWrite wrapper to be available outside the memory-flush subsystem when invoked with explicit append intent.

Example

// Example of how the `write` tool could be modified to accept a `mode` argument
function writeTool(options) {
  const { mode = 'overwrite', filePath, content } = options;
  if (mode === 'overwrite') {
    await fs.writeFile(filePath, content, 'utf-8');
  } else if (mode === 'append') {
    await fs.appendFile(filePath, content, 'utf-8');
  } else {
    throw new Error(`Invalid mode: ${mode}`);
  }
}

Notes

  • The issue affects all users who author cron-driven monitor-and-react skills that need to accumulate state on disk.
  • The severity of the issue is medium-high, as it can cause silent history loss on monitor-and-react workflows.
  • The recommended ordering for fixes is to ship a documentation update immediately, followed by a minor release with the mode argument added to the write tool.

Recommendation

Apply the workaround using exec with shell redirection (>>) until a more

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 3 firings, /tmp/append-test.log should contain 3 timestamped lines (one per firing). The cron pattern is monitor-and-react; the canonical use case is accumulating an alert/audit log over time.

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 [Bug]: `write` tool has no append mode; append-only wrapper is scoped to a memory-flush subsystem and unavailable to general agent runs (cron skills, monitor-and-react workflows) [1 comments, 2 participants]