openclaw - 💡(How to fix) Fix [Bug]: Workspace bootstrap files reported [MISSING] when hardlinked (nlink > 1 guard rejects legitimate shared files)

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…

Bootstrap files (such as AGENTS.md, SOUL.md, USER.md) that are shared across multiple agent workspaces via POSIX hardlinks are rejected by nlink > 1 safety guards in several dist/*.js files, so the gateway injects [MISSING] Expected at: <path> into Project Context even though the files exist and are readable.

(Related to but distinct from the two existing symlink-based MISSING reports: #38622 "Workspace file injector does not follow symlinks" and #64472 "Workspace symlinks to ~/.openclaw subdirectories are rejected by boundary path check". Those cover symlinks — the present issue is the hardlink case, enforced by a different code path (nlink > 1 stat guards, not realpath boundary checks), and requires a different remediation. Filing separately so each mechanism stays independently tracked.)

Error Message

  • Severity: Blocks agent behavior. Silently missing AGENTS.md/SOUL.md means the agent loses its workspace rules and persona; the agent produces generic/confused responses without any user-visible error (the MISSING is only in the injected system prompt, not in runtime logs at WARN or higher).

Root Cause

Hardlinks are used because OpenClaw's bootstrap loader does not follow symlinks for shared bootstrap files (documented behavior / known characteristic), so hardlinks are the only POSIX mechanism left for "multiple agents share the same source-of-truth file."

Fix Action

Fix / Workaround

We worked around the issue with a sed patch that zeroes out these guards in the affected bundle files; once applied, bootstrap files load correctly, downstream behavior is otherwise unchanged, and no regressions have surfaced.

After applying the local sed workaround (zeroing the nlink > 1 checks in affected dist files), the same setup loads all bootstrap files successfully. The agent's injected Project Context block then shows full file contents for AGENTS.md and SOUL.md.

import { r as openBoundaryFile } from '/usr/lib/node_modules/openclaw/dist/boundary-file-read-oFRaIDYB.js'; const r = await openBoundaryFile({ absolutePath: '/home/ubuntu/agents/agent-b/AGENTS.md', rootPath: '/home/ubuntu/agents/agent-b', boundaryLabel: 'workspace root', maxBytes: 1024 * 1024, }); // Before patch: r.ok === false, r.reason === 'validation' // After patch: r.ok === true, r.stat.nlink === 6, content is readable.

Code Example

cd ~/agents
   echo "# shared rules" > agent-a/AGENTS.md
   ln agent-a/AGENTS.md agent-b/AGENTS.md
   stat -c 'nlink=%h inode=%i %n' agent-a/AGENTS.md agent-b/AGENTS.md
   # nlink=2 inode=X ...agent-a/AGENTS.md
   # nlink=2 inode=X ...agent-b/AGENTS.md   (same inode — confirmed hardlinked)

---

[MISSING] Expected at: /home/ubuntu/agents/agent-b/AGENTS.md

---

OPENCLAW_DIST=$(npm root -g)/openclaw/dist
grep -rn 'nlink > 1' "$OPENCLAW_DIST"/*.js | grep -vE 'browser-bridges|restart-|selection-|runtime-'
# Expected matches (in 2026.5.6):
#   dist/safe-B_RfWeue.js:          .bstat.nlink > 1
#   dist/server-methods-BvQQUQsB.js: various ...nlink > 1
#   dist/config-Cb6WqTsS.js:        .realStat.nlink > 1
#   dist/safe-guards-Lerjnsz.js:    .preOpenStat.nlink > 1 / .openedStat.nlink > 1 / .candidateLstat.nlink > 1
#   dist/safe-open-sync-BVLkOkpr.js: rejectHardlinks && preOpenStat.isFile() — default true on the openBoundaryFile path

---

Redacted session prompt excerpt when the bug is active:


## Project Context
...
## /home/ubuntu/agents/agent-b/AGENTS.md
[MISSING] Expected at: /home/ubuntu/agents/agent-b/AGENTS.md
## /home/ubuntu/agents/agent-b/SOUL.md
[MISSING] Expected at: /home/ubuntu/agents/agent-b/SOUL.md
## /home/ubuntu/agents/agent-b/IDENTITY.md
# IDENTITY.md
... (this nlink=1 file loads fine)


`stat` before the MISSING, confirming files exist and are readable:


$ stat -c 'nlink=%h inode=%i mode=%a %n' ~/agents/agent-b/{AGENTS.md,SOUL.md,IDENTITY.md}
nlink=6 inode=1769643 mode=664 /home/ubuntu/agents/agent-b/AGENTS.md
nlink=6 inode=1787582 mode=664 /home/ubuntu/agents/agent-b/SOUL.md
nlink=1 inode=2509122 mode=664 /home/ubuntu/agents/agent-b/IDENTITY.md


After applying the local `sed` workaround (zeroing the `nlink > 1` checks in affected dist files), the same setup loads all bootstrap files successfully. The agent's injected Project Context block then shows full file contents for AGENTS.md and SOUL.md.

Direct test in Node REPL using the shipped bundle — hardlinked bootstrap file loads fine once the guard is bypassed:


import { r as openBoundaryFile } from '/usr/lib/node_modules/openclaw/dist/boundary-file-read-oFRaIDYB.js';
const r = await openBoundaryFile({
  absolutePath: '/home/ubuntu/agents/agent-b/AGENTS.md',
  rootPath: '/home/ubuntu/agents/agent-b',
  boundaryLabel: 'workspace root',
  maxBytes: 1024 * 1024,
});
// Before patch: r.ok === false, r.reason === 'validation'
// After patch:  r.ok === true,  r.stat.nlink === 6, content is readable.

---

OPENCLAW_DIST=$(npm root -g)/openclaw/dist
AFFECTED=$(grep -l 'nlink > 1' "$OPENCLAW_DIST"/*.js 2>/dev/null | \
  grep -vE 'browser-bridges|restart-|selection-|runtime-')
for f in $AFFECTED; do
  sed -i 's/\.bstat\.nlink > 1/false/g; s/\.pathStat\.nlink > 1/false/g;
          s/\.realStat\.nlink > 1/false/g; s/\.preOpenStat\.nlink > 1/false/g;
          s/\.openedStat\.nlink > 1/false/g; s/\.candidateLstat\.nlink > 1/false/g;
          s/\.sopenedStat\.nlink > 1/false/g; s/\.params\.postOpenStat\.nlink > 1/false/g' "$f"
done
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

Bootstrap files (such as AGENTS.md, SOUL.md, USER.md) that are shared across multiple agent workspaces via POSIX hardlinks are rejected by nlink > 1 safety guards in several dist/*.js files, so the gateway injects [MISSING] Expected at: <path> into Project Context even though the files exist and are readable.

(Related to but distinct from the two existing symlink-based MISSING reports: #38622 "Workspace file injector does not follow symlinks" and #64472 "Workspace symlinks to ~/.openclaw subdirectories are rejected by boundary path check". Those cover symlinks — the present issue is the hardlink case, enforced by a different code path (nlink > 1 stat guards, not realpath boundary checks), and requires a different remediation. Filing separately so each mechanism stays independently tracked.)

Steps to reproduce

  1. Install OpenClaw 2026.5.6 globally, configure two agents with separate workspaces (e.g. agents.list: [{id: "agent-a", workspace: "~/agents/agent-a"}, {id: "agent-b", workspace: "~/agents/agent-b"}]).
  2. Create a shared AGENTS.md in one workspace, then hardlink it into the other:
    cd ~/agents
    echo "# shared rules" > agent-a/AGENTS.md
    ln agent-a/AGENTS.md agent-b/AGENTS.md
    stat -c 'nlink=%h inode=%i %n' agent-a/AGENTS.md agent-b/AGENTS.md
    # nlink=2 inode=X ...agent-a/AGENTS.md
    # nlink=2 inode=X ...agent-b/AGENTS.md   (same inode — confirmed hardlinked)
  3. Start or restart the gateway, open a session on agent-b, and inspect the system prompt (e.g. via /acp doctor, or extract it with the upstream-provided extract tooling).
  4. The ## Project Context block shows [MISSING] Expected at: ~/agents/agent-b/AGENTS.md.
  5. Read tool / cat agent-b/AGENTS.md from the same process works fine — the file is present and readable. Only the bootstrap loader rejects it.

Hardlinks are used because OpenClaw's bootstrap loader does not follow symlinks for shared bootstrap files (documented behavior / known characteristic), so hardlinks are the only POSIX mechanism left for "multiple agents share the same source-of-truth file."

Expected behavior

Bootstrap files should load regardless of their nlink count. A hardlink is a legitimate filesystem mechanism with no TOCTOU or privilege-escalation risk comparable to symlinks — the file at <workspace>/<name> is, by st_ino/st_dev identity, the same inode the user intended to share.

The current behavior differs from historical OpenClaw versions (prior to the introduction of the nlink > 1 guard in dist/safe-*.js) where hardlinked bootstrap files loaded correctly.

Actual behavior

openBoundaryFile(...)openVerifiedFileSync(...) returns ok: false, reason: "validation" for any bootstrap file with st_nlink > 1. readWorkspaceFileWithGuards in dist/workspace-*.js treats any non-ok result as "file missing" and returns {missing: true}. Downstream, buildBootstrapContextFiles in dist/pi-embedded-helpers-CQuDqiJN.js (line ~180) emits:

[MISSING] Expected at: /home/ubuntu/agents/agent-b/AGENTS.md

into the Project Context block of the system prompt.

Grep for the guard (2026.5.6 filenames):

OPENCLAW_DIST=$(npm root -g)/openclaw/dist
grep -rn 'nlink > 1' "$OPENCLAW_DIST"/*.js | grep -vE 'browser-bridges|restart-|selection-|runtime-'
# Expected matches (in 2026.5.6):
#   dist/safe-B_RfWeue.js:          .bstat.nlink > 1
#   dist/server-methods-BvQQUQsB.js: various ...nlink > 1
#   dist/config-Cb6WqTsS.js:        .realStat.nlink > 1
#   dist/safe-guards-Lerjnsz.js:    .preOpenStat.nlink > 1 / .openedStat.nlink > 1 / .candidateLstat.nlink > 1
#   dist/safe-open-sync-BVLkOkpr.js: rejectHardlinks && preOpenStat.isFile() — default true on the openBoundaryFile path

We worked around the issue with a sed patch that zeroes out these guards in the affected bundle files; once applied, bootstrap files load correctly, downstream behavior is otherwise unchanged, and no regressions have surfaced.

OpenClaw version

2026.5.6 (npm [email protected], daemon-cli build). The same issue was also present in 2026.5.3-1 through 2026.5.5 with the same bundle-file fingerprints.

Operating system

Ubuntu 24.04 LTS, x86_64. Also reproduced on aarch64 and under darwin 24.x (macOS) where the same dist files are loaded.

Install method

npm install -g openclaw on a Node.js 24.13.0 runtime, gateway launched as a user-mode systemd service. Gateway process owns the bundle files (no privilege boundary between the user editing the workspace and the gateway reading bootstrap files).

Model

Not model-specific.

Provider / routing chain

Not relevant — the issue is in the bootstrap loader, which runs before provider routing.

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Redacted session prompt excerpt when the bug is active:


## Project Context
...
## /home/ubuntu/agents/agent-b/AGENTS.md
[MISSING] Expected at: /home/ubuntu/agents/agent-b/AGENTS.md
## /home/ubuntu/agents/agent-b/SOUL.md
[MISSING] Expected at: /home/ubuntu/agents/agent-b/SOUL.md
## /home/ubuntu/agents/agent-b/IDENTITY.md
# IDENTITY.md
... (this nlink=1 file loads fine)


`stat` before the MISSING, confirming files exist and are readable:


$ stat -c 'nlink=%h inode=%i mode=%a %n' ~/agents/agent-b/{AGENTS.md,SOUL.md,IDENTITY.md}
nlink=6 inode=1769643 mode=664 /home/ubuntu/agents/agent-b/AGENTS.md
nlink=6 inode=1787582 mode=664 /home/ubuntu/agents/agent-b/SOUL.md
nlink=1 inode=2509122 mode=664 /home/ubuntu/agents/agent-b/IDENTITY.md


After applying the local `sed` workaround (zeroing the `nlink > 1` checks in affected dist files), the same setup loads all bootstrap files successfully. The agent's injected Project Context block then shows full file contents for AGENTS.md and SOUL.md.

Direct test in Node REPL using the shipped bundle — hardlinked bootstrap file loads fine once the guard is bypassed:


import { r as openBoundaryFile } from '/usr/lib/node_modules/openclaw/dist/boundary-file-read-oFRaIDYB.js';
const r = await openBoundaryFile({
  absolutePath: '/home/ubuntu/agents/agent-b/AGENTS.md',
  rootPath: '/home/ubuntu/agents/agent-b',
  boundaryLabel: 'workspace root',
  maxBytes: 1024 * 1024,
});
// Before patch: r.ok === false, r.reason === 'validation'
// After patch:  r.ok === true,  r.stat.nlink === 6, content is readable.

Impact and severity

  • Affected users/systems: Any OpenClaw deployment that uses hardlinks to share bootstrap files across multiple agents in the same workspace tree. This is the documented workaround for "bootstrap loader does not follow symlinks" in multi-agent setups. We run 8 agents, 6 of them form a family that shares AGENTS.md/SOUL.md/USER.md via hardlinks.
  • Severity: Blocks agent behavior. Silently missing AGENTS.md/SOUL.md means the agent loses its workspace rules and persona; the agent produces generic/confused responses without any user-visible error (the MISSING is only in the injected system prompt, not in runtime logs at WARN or higher).
  • Frequency: Always, on every session start, for every agent using hardlinked bootstrap files. Deterministic.
  • Consequence: Agents regress to generic Claude/bedrock behavior until the operator manually diagnoses that the system prompt is missing context. Root-cause investigation requires grep'ing bundle source. No log line at any level mentions nlink or "rejected hardlink" at the failure point.

Additional information

Suggested fix (two options, either would resolve):

  1. Default rejectHardlinks: false on the openBoundaryFile / openVerifiedFileSync call path when reading workspace bootstrap files. The callers that need hardlink rejection (e.g. queued log file writes, credential files) can opt-in explicitly via rejectHardlinks: true.
  2. Remove the nlink > 1 checks entirely from these files — hardlinks are a legitimate filesystem mechanism that does not introduce the TOCTOU/symlink-race concerns that O_NOFOLLOW already protects against. The file identity re-check via sameFileIdentity(preOpenStat, openedStat) already protects against rename-race attacks.

Workaround currently in place locally (not upstreamable as-is because it zeroes the checks rather than making them opt-in):

OPENCLAW_DIST=$(npm root -g)/openclaw/dist
AFFECTED=$(grep -l 'nlink > 1' "$OPENCLAW_DIST"/*.js 2>/dev/null | \
  grep -vE 'browser-bridges|restart-|selection-|runtime-')
for f in $AFFECTED; do
  sed -i 's/\.bstat\.nlink > 1/false/g; s/\.pathStat\.nlink > 1/false/g;
          s/\.realStat\.nlink > 1/false/g; s/\.preOpenStat\.nlink > 1/false/g;
          s/\.openedStat\.nlink > 1/false/g; s/\.candidateLstat\.nlink > 1/false/g;
          s/\.sopenedStat\.nlink > 1/false/g; s/\.params\.postOpenStat\.nlink > 1/false/g' "$f"
done

Happy to open a PR if the team agrees on Option 1 (scoped opt-in).

The bootstrap-loader behavior of not following symlinks has a valid security rationale (avoids symlink races for workspace escape). Hardlinks do not share that risk because they cannot cross filesystem boundaries and the resolved inode is already the user's file — the guard appears overcautious in this specific context.

Cross-reference for triage. This is the third distinct "shared bootstrap file shows as MISSING" report:

  • #38622 — symlinks across sibling workspace dirs (realpath resolves, but the loader still refuses to follow the symlink at its entry point)
  • #64472 — symlinks into ~/.openclaw/kb/ (realpath resolves outside the workspace root, rejected by the boundary-path security check)
  • this issue — hardlinks (no symlink at all, realpath is in-root, but nlink > 1 stat guard rejects)

Together they describe the full set of "file sharing mechanisms between agents" (symlinks, cross-root symlinks, hardlinks), and each hits a different guard. A coordinated fix that treats the three as a single "legitimate shared-file" story (with workspace-boundary checks applied to the resolved canonical path, and no additional rejection for nlink > 1) would unblock all three cases at once; but each is also fixable independently.

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

Bootstrap files should load regardless of their nlink count. A hardlink is a legitimate filesystem mechanism with no TOCTOU or privilege-escalation risk comparable to symlinks — the file at <workspace>/<name> is, by st_ino/st_dev identity, the same inode the user intended to share.

The current behavior differs from historical OpenClaw versions (prior to the introduction of the nlink > 1 guard in dist/safe-*.js) where hardlinked bootstrap files loaded correctly.

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]: Workspace bootstrap files reported [MISSING] when hardlinked (nlink > 1 guard rejects legitimate shared files)