openclaw - 💡(How to fix) Fix [Bug]: XDG Base Directory env vars are not inherited by subprocesses, breaking XDG-aware tools (gogcli, mcporter, others) [1 pull requests]

Official PRs (…)
ON THIS PAGE

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…

OpenClaw's host-env sanitizer (sanitizeHostExecEnv() in src/infra/host-env-security.ts) strips XDG_CONFIG_HOME and XDG_CONFIG_DIRS from the environment of subprocesses it spawns, even when the parent gateway inherited those vars normally from its own launch environment. This violates the freedesktop.org XDG Base Directory Specification, which is built on the contract that $XDG_CONFIG_HOME (and the other XDG Base Directory vars) propagate to child processes so the whole process tree resolves the same config root. XDG-aware tools spawned by OpenClaw (gogcli, mcporter, and any other CLI that follows the spec) silently fall back to ~/.config/<tool>/ instead of the operator-intended location. Verified against openclaw/openclaw at tag v2026.5.19.

Root Cause

Root cause traced through source:

Fix Action

Fixed

Code Example

sh -c 'echo "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-<unset>}"'

---

blockedInheritedKeys: sortUniqueUppercase([
     ...blockedEverywhereKeys,
     ...blockedOverrideOnlyKeys.filter(
       (value) => !allowedInheritedOverrideOnlyUpper.has(value.toUpperCase()),
     ),
   ]),

---

$ git show v2026.5.19:src/infra/host-env-security-policy.json | jq '
  {
    XDG_CONFIG_HOME_in_blockedOverrideOnlyKeys:
      (.blockedOverrideOnlyKeys | index("XDG_CONFIG_HOME") != null),
    XDG_CONFIG_HOME_in_allowedInheritedOverrideOnlyKeys:
      (.allowedInheritedOverrideOnlyKeys | index("XDG_CONFIG_HOME") != null),
    XDG_DATA_HOME_anywhere:
      (.blockedOverrideOnlyKeys + .allowedInheritedOverrideOnlyKeys + .blockedEverywhereKeys
       | index("XDG_DATA_HOME") != null)
  }'
{
  "XDG_CONFIG_HOME_in_blockedOverrideOnlyKeys": true,
  "XDG_CONFIG_HOME_in_allowedInheritedOverrideOnlyKeys": false,
  "XDG_DATA_HOME_anywhere": false
}

$ # Subprocess view: parent has it, child does not.
$ XDG_CONFIG_HOME=/some/custom/dir openclaw  # start gateway
$ # then from an agent:
$ sh -c 'echo "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-<unset>}"'
XDG_CONFIG_HOME=<unset>

---

"allowedInheritedOverrideOnlyKeys": [
     ...
     "SYSTEMROOT",
     "WINDIR",
+    "XDG_CACHE_HOME",
+    "XDG_CONFIG_DIRS",
+    "XDG_CONFIG_HOME",
+    "XDG_DATA_DIRS",
+    "XDG_DATA_HOME",
+    "XDG_RUNTIME_DIR",
+    "XDG_STATE_HOME",
     "ZDOTDIR"
   ],

---

blockedInheritedKeys: sortUniqueUppercase([
  ...blockedEverywhereKeys,
  ...blockedOverrideOnlyKeys.filter(
    (value) => !allowedInheritedOverrideOnlyUpper.has(value.toUpperCase()),
  ),
]),
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

OpenClaw's host-env sanitizer (sanitizeHostExecEnv() in src/infra/host-env-security.ts) strips XDG_CONFIG_HOME and XDG_CONFIG_DIRS from the environment of subprocesses it spawns, even when the parent gateway inherited those vars normally from its own launch environment. This violates the freedesktop.org XDG Base Directory Specification, which is built on the contract that $XDG_CONFIG_HOME (and the other XDG Base Directory vars) propagate to child processes so the whole process tree resolves the same config root. XDG-aware tools spawned by OpenClaw (gogcli, mcporter, and any other CLI that follows the spec) silently fall back to ~/.config/<tool>/ instead of the operator-intended location. Verified against openclaw/openclaw at tag v2026.5.19.

Steps to reproduce

  1. Start OpenClaw with XDG_CONFIG_HOME=/some/custom/dir set in the gateway's environment (e.g. via container entrypoint, systemd unit, or shell export before openclaw launches).
  2. From an agent, invoke any XDG-aware tool. The simplest is a one-liner subprocess that prints what it sees:
    sh -c 'echo "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-<unset>}"'
  3. Observe: the subprocess prints XDG_CONFIG_HOME=<unset> even though the parent gateway has it exported. Same result for XDG_CONFIG_DIRS. XDG_DATA_HOME, XDG_STATE_HOME, XDG_CACHE_HOME, XDG_RUNTIME_DIR and XDG_DATA_DIRS all pass through correctly.

Concrete failure mode in our deployment: gogcli's only path-control env var is XDG_CONFIG_HOME (see Dir() in internal/config/paths.go at gogcli v0.17.0). With the parent setting it to a persisted-state directory, gogcli's encrypted OAuth keyring should land there. Instead it lands at ~/.config/gogcli/keyring/, which on container restart is wiped, taking refresh tokens with it.

Expected behavior

The XDG Base Directory Spec defines five environment variables — XDG_CONFIG_HOME, XDG_CONFIG_DIRS, XDG_DATA_HOME, XDG_DATA_DIRS, XDG_STATE_HOME, XDG_CACHE_HOME, XDG_RUNTIME_DIR — and they are by-spec process-inheritable. The whole point of the spec is that a parent process can declare, "here is where config / data / state / cache / runtime files live for this process tree," and every descendant tool that follows the spec resolves to the same roots. This is why containers, sandboxes, dev environments, and per-tenant deployments routinely set these vars at the top of the process tree and rely on inheritance to do the rest.

A security policy that strips XDG Base Directory vars from inheritance (as distinct from blocking agent-supplied overrides at spawn time) breaks that contract. Override-blocking is sound — an agent should not be able to redirect a tool's config path per-call. Inheritance-stripping is different: the operator already controls the parent's environment by definition, and stripping the value adds no security boundary the operator didn't already cross when they set the var on the parent.

The same principle is already applied elsewhere in this policy: HOME, KUBECONFIG, AWS_CONFIG_FILE, GOOGLE_APPLICATION_CREDENTIALS, HTTPS_PROXY, etc. are all in blockedOverrideOnlyKeys and allowedInheritedOverrideOnlyKeys — blocked as agent-supplied overrides, allowed through as inherited values. XDG Base Directory vars belong in exactly the same bucket and for exactly the same reason.

Actual behavior

sanitizeHostExecEnvWithDiagnostics() in src/infra/host-env-security.ts iterates the parent baseEnv and skips any key matching isDangerousHostInheritedEnvVarName(key), which consults the derived blockedInheritedKeys set. Two XDG Base Directory vars (XDG_CONFIG_HOME, XDG_CONFIG_DIRS) end up in that set; the other XDG vars do not.

Root cause traced through source:

  1. src/infra/host-env-security-policy.json lists XDG_CONFIG_HOME and XDG_CONFIG_DIRS in blockedOverrideOnlyKeys. Neither is in allowedInheritedOverrideOnlyKeys.
  2. derivePolicyArrays in src/infra/host-env-security-policy.js computes:
    blockedInheritedKeys: sortUniqueUppercase([
      ...blockedEverywhereKeys,
      ...blockedOverrideOnlyKeys.filter(
        (value) => !allowedInheritedOverrideOnlyUpper.has(value.toUpperCase()),
      ),
    ]),
    Because the two XDG vars are in blockedOverrideOnlyKeys and not in the allow-list, they fall through into blockedInheritedKeys.
  3. sanitizeHostExecEnvWithDiagnostics() then drops them from the inherited env going to subprocesses.

The intent of adding XDG vars to blockedOverrideOnlyKeys was almost certainly to harden override handling — they were introduced in PR #51207 ("Exec: harden host env override enforcement and fail closed"). Stripping them from inheritance appears to be an unintended derivation side-effect, not a deliberate policy decision.

Audit of every XDG Base Directory env var's policy membership at OpenClaw v2026.5.19, inheritance effect, and concrete downstream impact on gogcli v0.17.0 and mcporter v0.11.1:

XDG varblockedOverrideOnlyKeysallowedInheritedOverrideOnlyKeysEffect on inheritanceUsed by gogcli?Used by mcporter?Net downstream impact
XDG_CONFIG_HOME✅ yes❌ nostripped✅ config + keyring root (paths.go) — the only path-control knobmcporter.json server list (paths.ts config kind)gogcli broken (keyring lands at ~/.config/gogcli/, lost on restart); mcporter.json falls back to ~/.mcporter/
XDG_CONFIG_DIRS✅ yes❌ nostrippedno current downstream consumer in our stack, but spec-defined and stripped on principle
XDG_DATA_HOME❌ no❌ nopasses through✅ OAuth vault (oauth-vault.tspaths.ts data kind)mcporter vault correctly lands under operator-chosen root ✅
XDG_DATA_DIRS❌ no❌ nopasses throughn/a
XDG_STATE_HOME❌ no❌ nopasses through✅ daemon socket / metadata / log (paths.ts state kind)mcporter daemon state correctly inherits ✅
XDG_CACHE_HOME❌ no❌ nopasses through✅ per-server schema cache (paths.ts cache kind)mcporter cache correctly inherits ✅
XDG_RUNTIME_DIR❌ no❌ nopasses throughn/a (used by gateway install per #54415)

The asymmetry confirms the policy treats two XDG vars one way and the other five another way, with no apparent justification for the split. The XDG spec treats them as a coherent set. Note also the partial-functionality artifact: mcporter's vault (data) works correctly because XDG_DATA_HOME inherits, but its config file (config) is broken because XDG_CONFIG_HOME is stripped — within a single tool, half the XDG surface works and half doesn't, depending only on which kind each file belongs to.

OpenClaw version

v2026.5.19

Operating system

Ubuntu 24.04

Install method

npm global (npm install -g openclaw@${OPENCLAW_VERSION} in a Docker image)

Model

Not applicable — process-spawn-layer bug, model-agnostic.

Provider / routing chain

Not applicable.

Additional provider/model setup details

Not applicable.

Logs, screenshots, and evidence

$ git show v2026.5.19:src/infra/host-env-security-policy.json | jq '
  {
    XDG_CONFIG_HOME_in_blockedOverrideOnlyKeys:
      (.blockedOverrideOnlyKeys | index("XDG_CONFIG_HOME") != null),
    XDG_CONFIG_HOME_in_allowedInheritedOverrideOnlyKeys:
      (.allowedInheritedOverrideOnlyKeys | index("XDG_CONFIG_HOME") != null),
    XDG_DATA_HOME_anywhere:
      (.blockedOverrideOnlyKeys + .allowedInheritedOverrideOnlyKeys + .blockedEverywhereKeys
       | index("XDG_DATA_HOME") != null)
  }'
{
  "XDG_CONFIG_HOME_in_blockedOverrideOnlyKeys": true,
  "XDG_CONFIG_HOME_in_allowedInheritedOverrideOnlyKeys": false,
  "XDG_DATA_HOME_anywhere": false
}

$ # Subprocess view: parent has it, child does not.
$ XDG_CONFIG_HOME=/some/custom/dir openclaw  # start gateway
$ # then from an agent:
$ sh -c 'echo "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-<unset>}"'
XDG_CONFIG_HOME=<unset>

Impact and severity

  • Affected: Any OpenClaw deployment that relies on XDG Base Directory inheritance to redirect XDG-aware subprocess tooling. This includes essentially every containerized / multi-tenant / per-instance-persisted-state deployment topology.
  • Severity: High. In our case the gogcli encrypted OAuth keyring lands at ~/.config/gogcli/keyring/, which is not under the container's persisted state directory and is lost on every container restart. Users have to re-auth via OAuth after every restart. Same failure shape applies to mcporter's mcporter.json (vault is XDG_DATA_HOME-keyed so it survives, but the config file is XDG_CONFIG_HOME-keyed and gets lost).
  • Frequency: Every subprocess invocation, deterministic.
  • Consequence: Silent loss of credentials and config across instance lifecycle events; no diagnostic at the OpenClaw layer (subprocess looks like it succeeded). The failure is invisible until a tool tries to read state that should have been persisted and isn't there.

Additional information

Affected versions / environment notes

Verified at v2026.5.19, latest stable at time of report. Bug present continuously since the policy commit c1da0ddd54 first shipped in v2026.5.4. The XDG_CONFIG_HOME entry in blockedOverrideOnlyKeys was introduced earlier in PR #51207.

The repro environment is a containerized Ubuntu 24.04 OpenClaw deployment. XDG Base Directory Spec is a Linux/freedesktop convention; the behavior is most consequential on Linux but the policy is platform-agnostic, so any environment that sets XDG vars (some macOS dev shells via Homebrew, Nix, etc.) is affected the same way.

Why XDG inheritance is fundamentally different from XDG overrides

The override-blocking property is sound and should be kept:

  • Override = an agent or skill supplies XDG_CONFIG_HOME=/attacker/path in the overrides arg when calling sanitizeHostExecEnv(). This is a real attack surface — the agent could redirect tool config writes to a directory it controls. ✅ block.
  • Inheritance = the operator set XDG_CONFIG_HOME=/some/dir on the parent openclaw process at launch, and the spawn-time env is being constructed from baseEnv = process.env. The value is already in scope of the OpenClaw process; the question is only whether to pass it down. The operator already crossed the trust boundary when they set the value at parent launch.

The XDG Base Directory Spec requires the inheritance side to work, by design. Most XDG-aware tools have no other path-control knob (gogcli is one example; there's no GOG_CONFIG_DIR), so without inheritance there is literally no way to redirect them in a deployment.

Other env vars in the policy that share this property — operator-set at parent launch, dangerous as agent override, safe as inherited value — are already in allowedInheritedOverrideOnlyKeys: HOME, KUBECONFIG, AWS_CONFIG_FILE, GOOGLE_APPLICATION_CREDENTIALS, HTTPS_PROXY, NODE_EXTRA_CA_CERTS, etc. XDG vars belong in the same group.

Proposed fix

Add all seven XDG Base Directory vars to allowedInheritedOverrideOnlyKeys — not just the two currently in blockedOverrideOnlyKeys. This encodes the XDG-spec inheritance contract once, explicitly, instead of relying on each future override-hardening decision to remember to re-assert it.

Minimal diff:

   "allowedInheritedOverrideOnlyKeys": [
     ...
     "SYSTEMROOT",
     "WINDIR",
+    "XDG_CACHE_HOME",
+    "XDG_CONFIG_DIRS",
+    "XDG_CONFIG_HOME",
+    "XDG_DATA_DIRS",
+    "XDG_DATA_HOME",
+    "XDG_RUNTIME_DIR",
+    "XDG_STATE_HOME",
     "ZDOTDIR"
   ],

No code changes outside the policy JSON required — the derivation logic in host-env-security-policy.js picks the change up automatically.

Why all seven, not just the two currently broken. Recall the derivation from host-env-security-policy.js (shown earlier under "Actual behavior"):

blockedInheritedKeys: sortUniqueUppercase([
  ...blockedEverywhereKeys,
  ...blockedOverrideOnlyKeys.filter(
    (value) => !allowedInheritedOverrideOnlyUpper.has(value.toUpperCase()),
  ),
]),

Allow-listing a var that's not currently in blockedOverrideOnlyKeys is a no-op today — the .filter() step has nothing to filter for it, so blockedInheritedKeys doesn't change. But if a future override-hardening PR adds (say) XDG_DATA_HOME to blockedOverrideOnlyKeys, then without the proactive allow-list entry, XDG_DATA_HOME silently drops out of inheritance the same way XDG_CONFIG_HOME does today. Allow-listing the full spec up front means the next override-hardening decision can be made in isolation, without having to re-derive the inheritance side-effect each time.

This matches the existing pattern in the policy: HOME, KUBECONFIG, AWS_CONFIG_FILE, GOOGLE_APPLICATION_CREDENTIALS, HTTPS_PROXY, NODE_EXTRA_CA_CERTS, etc. are all in allowedInheritedOverrideOnlyKeys regardless of whether they're in blockedOverrideOnlyKeys today — the allow-list asserts the inheritance contract as a property, not a reaction.

It's also more truthful to the XDG spec, which defines the seven vars as a coherent set, not as independent knobs.

Optional broader hardening (separate discussion)

The fix above closes the inheritance side of the contract. The maintainer may separately want to consider:

  1. Adding the other XDG Base Directory vars to blockedOverrideOnlyKeys for symmetric override-blocking. Today XDG_DATA_HOME etc. can be set by agents at spawn time because they're absent from the policy entirely — the same "redirect tool data writes" attack that motivated blocking XDG_CONFIG_HOME as an override applies to data and state too. With the proposed fix above already in place, this becomes a single-list edit (blockedOverrideOnlyKeys) that doesn't have to think about inheritance — the inheritance contract is already declared.
  2. Documenting in code comments which vars are operator-controlled (inheritance-safe) vs. agent-controlled (override risk) — the current policy structure has the right shape but the rationale is implicit.

These are independent of the main fix and can be punted to a follow-up.

Related references

Commit/PR that introduced the bug:

  • PR #51207 ("Exec: harden host env override handling across gateway and node") — merged 2026-03-20, commit 7abfff756d. This is where XDG_CONFIG_HOME and XDG_CONFIG_DIRS were first added to blockedOverrideOnlyKeys. The intent (per the PR title and commit message) was override hardening; the inheritance side effect appears to have been unintended.
  • Commit c1da0ddd54 ("fix(security): block workspace env from overriding Windows system root paths") — first stable release containing the current shape of the policy file (v2026.5.4).

Related OpenClaw issues:

  • #79847Mirror-image bug. qmd-manager leaks its own XDG_CONFIG_HOME to all child spawns, including mcporter, breaking the memory.backend: qmd path. Same XDG-Spec-violating shape, opposite symptom: ours is "operator-set XDG vars don't reach the child"; #79847 is "qmd-internal XDG vars do reach a child that shouldn't see them." Together, the two issues frame the underlying root cause: OpenClaw does not have a coherent XDG inheritance model. Fixing #79847 (per-spawn env construction in qmd-manager) and the issue filed here (policy allow-listing for inheritance) is the complete fix; either alone is partial. Closed-as-not-merged PR #79981 ("fix(memory/qmd): scope XDG env vars to qmd spawns; use clean env for mcporter") was an attempt at #79847.
  • #53628${XDG_CONFIG_HOME} placeholder is not expanded when installing a skill via npx clawhub@latest install. Different layer (template expansion vs. env inheritance), but same root user expectation: "when I set XDG_CONFIG_HOME, every OpenClaw-spawned component should respect it." Affects Docker deployments.
  • #63069 — shell completion writes to wrong profile path when ZDOTDIR or XDG_CONFIG_HOME is set. Different code path (getShellProfilePath), same XDG-disrespect pattern. In-flight fix: PR #81298 ("fix(cli): honor ZDOTDIR/XDG_CONFIG_HOME in completion install").
  • #54415 — gateway install fails when XDG_RUNTIME_DIR is not set. Closed; cited here as evidence the codebase already has multiple XDG-handling gaps in different subsystems.
  • #80329 — "Allow per-call sandbox env injection from operator-trusted callers." General request for trusted-source env passing — overlaps with the policy boundary this issue touches.

Sibling-project precedent for the XDG-spec framing:

  • openclaw/mcporter#155 ("Honor XDG Base Directory Spec for config, vault, cache, and daemon paths") — closed as completed, shipped in mcporter v0.10.0. Same spec, same framing, same conclusion: XDG vars are the standard, operator-supplied path-control surface and tooling needs to honor them end-to-end. The mcporter issue closed the producer side (mcporter now writes to XDG paths); this OpenClaw issue closes the inheritance side (so the producer actually receives the operator's chosen path).
  • openclaw/mcporter#184 — interesting consequence of partial XDG support across the stack: after mcporter 0.10.x began honoring XDG_CONFIG_HOME, embedders (including OpenClaw's qmd-manager) that had been setting XDG vars for unrelated downstream tools accidentally redirected mcporter too. Reinforces that XDG inheritance needs to be consistent and intentional from the top of the process tree down — partial honoring causes silent regressions.

Related downstream tools that hit this:

XDG Base Directory Specification:

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

The XDG Base Directory Spec defines five environment variables — XDG_CONFIG_HOME, XDG_CONFIG_DIRS, XDG_DATA_HOME, XDG_DATA_DIRS, XDG_STATE_HOME, XDG_CACHE_HOME, XDG_RUNTIME_DIR — and they are by-spec process-inheritable. The whole point of the spec is that a parent process can declare, "here is where config / data / state / cache / runtime files live for this process tree," and every descendant tool that follows the spec resolves to the same roots. This is why containers, sandboxes, dev environments, and per-tenant deployments routinely set these vars at the top of the process tree and rely on inheritance to do the rest.

A security policy that strips XDG Base Directory vars from inheritance (as distinct from blocking agent-supplied overrides at spawn time) breaks that contract. Override-blocking is sound — an agent should not be able to redirect a tool's config path per-call. Inheritance-stripping is different: the operator already controls the parent's environment by definition, and stripping the value adds no security boundary the operator didn't already cross when they set the var on the parent.

The same principle is already applied elsewhere in this policy: HOME, KUBECONFIG, AWS_CONFIG_FILE, GOOGLE_APPLICATION_CREDENTIALS, HTTPS_PROXY, etc. are all in blockedOverrideOnlyKeys and allowedInheritedOverrideOnlyKeys — blocked as agent-supplied overrides, allowed through as inherited values. XDG Base Directory vars belong in exactly the same bucket and for exactly the same reason.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING