Skip to content

fix: scope localStorage settings key by basePath (#47481)#47932

Merged
steipete merged 1 commit intoopenclaw:mainfrom
bobBot-claw:fix/localstorage-settings-scope
Mar 16, 2026
Merged

fix: scope localStorage settings key by basePath (#47481)#47932
steipete merged 1 commit intoopenclaw:mainfrom
bobBot-claw:fix/localstorage-settings-scope

Conversation

@bobBot-claw
Copy link
Copy Markdown

Fixes #47481 - Control UI localStorage settings conflict across different basePath deployments

Problem

When multiple Gateway instances are deployed under the same domain with different basePaths (e.g., /gateway-a/, /gateway-b/), the Control UI shares a single localStorage key "openclaw.control.settings.v1", causing settings from one Gateway to incorrectly apply to another.

Solution

  • Add settingsKeyForGateway() function to scope settings key by gateway URL
  • Use scoped key format: openclaw.control.settings.v1:https://example.com/gateway-a
  • This matches the existing pattern used for tokens (openclaw.control.token.v1:...)
  • Add migration from legacy static key on load for backward compatibility

Changes

  • ui/src/ui/storage.ts: Modified KEY constant and added settingsKeyForGateway()

…loyment conflicts

- Add settingsKeyForGateway() function similar to tokenSessionKeyForGateway()
- Use scoped key format: openclaw.control.settings.v1:https://example.com/gateway-a
- Add migration from legacy static key on load
- Fixes openclaw#47481
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR correctly fixes a localStorage key collision that occurred when multiple Gateway instances were deployed under the same domain with different basePaths. The settings key is now scoped per gateway URL (openclaw.control.settings.v1:<gatewayUrl>) following the same pattern already used for token storage, and a backward-compatible fallback chain handles migration from the legacy static key.

Findings:

  • The inline comments at lines 195 and 268 describe the lookup fallback order backwards — they say "legacy first, scoped second" but the code checks the scoped key first and the legacy key last.
  • The legacy "openclaw.control.settings.v1" key is never explicitly removed from localStorage after migration. It will linger indefinitely as stale data, though it won't affect behavior once the scoped key is written.
  • The intermediate SETTINGS_KEY_PREFIX + "default" fallback in the lookup chain (openclaw.control.settings.v1:default) is a new-format key that could only have been written by this new code when gatewayUrl is empty, so it is unlikely to aid migration from the old system in practice.

Confidence Score: 4/5

  • PR is safe to merge; the core scoping logic is correct and migration is handled, with only minor style and cleanup concerns.
  • The fix correctly follows the existing token-scoping pattern and migration from the legacy key works as intended. The two identified issues — a backwards comment and missing cleanup of the legacy key — are non-blocking style/best-practice concerns that don't affect correctness.
  • ui/src/ui/storage.ts — review the backwards comment and consider adding explicit legacy key cleanup after migration.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/storage.ts
Line: 196-197

Comment:
**Legacy key never removed after migration**

When `loadSettings` (or `persistSettings`) reads from the legacy key `"openclaw.control.settings.v1"` and successfully migrates to the new scoped key, the old entry is never deleted from `localStorage`. While this is not a functional bug today (the scoped key takes priority), it leaves stale data in storage indefinitely and could cause confusion if the scoped key is somehow absent in the future.

Consider explicitly cleaning up the legacy key after a successful migration write:

```typescript
// After storage?.setItem(scopedKey, ...) in persistSettings:
storage?.removeItem("openclaw.control.settings.v1");
```

Similarly in `loadSettings`, once settings are loaded from the legacy key and `persistSettings` is called (or immediately after the initial load), the legacy key could be removed to avoid it persisting forever.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: ui/src/ui/storage.ts
Line: 195-197

Comment:
**Comment describes lookup order backwards**

The comment says _"First check for legacy key (no scope), then check for scoped key"_, but the actual code does the opposite — it checks the scoped key first and falls back to the legacy key last. The same backwards comment appears at line 268 in `persistSettings`. This will confuse future readers who expect the described order to match the actual precedence.

The comment should read something like: _"First check for scoped key, then fall back to legacy key for migration"_.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 389b83f

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 389b83fc64

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +196 to +197
const scopedKey = settingsKeyForGateway(defaults.gatewayUrl);
const raw = storage?.getItem(scopedKey) ?? storage?.getItem(SETTINGS_KEY_PREFIX + "default") ?? storage?.getItem("openclaw.control.settings.v1");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load settings using current page scope key

Saving now writes to a key derived from next.gatewayUrl, but loading only reads the key derived from defaults.gatewayUrl (the page’s inferred gateway). If a user sets a custom gateway URL (different host/basePath) and refreshes, the saved entry is never read, so loadSettings() falls back to defaults and silently loses the user’s gateway/theme/session preferences. This is reproducible by calling saveSettings() with a non-default gatewayUrl and then loadSettings() on the same page.

Useful? React with 👍 / 👎.

Comment on lines +196 to +197
const scopedKey = settingsKeyForGateway(defaults.gatewayUrl);
const raw = storage?.getItem(scopedKey) ?? storage?.getItem(SETTINGS_KEY_PREFIX + "default") ?? storage?.getItem("openclaw.control.settings.v1");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy key never removed after migration

When loadSettings (or persistSettings) reads from the legacy key "openclaw.control.settings.v1" and successfully migrates to the new scoped key, the old entry is never deleted from localStorage. While this is not a functional bug today (the scoped key takes priority), it leaves stale data in storage indefinitely and could cause confusion if the scoped key is somehow absent in the future.

Consider explicitly cleaning up the legacy key after a successful migration write:

// After storage?.setItem(scopedKey, ...) in persistSettings:
storage?.removeItem("openclaw.control.settings.v1");

Similarly in loadSettings, once settings are loaded from the legacy key and persistSettings is called (or immediately after the initial load), the legacy key could be removed to avoid it persisting forever.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/storage.ts
Line: 196-197

Comment:
**Legacy key never removed after migration**

When `loadSettings` (or `persistSettings`) reads from the legacy key `"openclaw.control.settings.v1"` and successfully migrates to the new scoped key, the old entry is never deleted from `localStorage`. While this is not a functional bug today (the scoped key takes priority), it leaves stale data in storage indefinitely and could cause confusion if the scoped key is somehow absent in the future.

Consider explicitly cleaning up the legacy key after a successful migration write:

```typescript
// After storage?.setItem(scopedKey, ...) in persistSettings:
storage?.removeItem("openclaw.control.settings.v1");
```

Similarly in `loadSettings`, once settings are loaded from the legacy key and `persistSettings` is called (or immediately after the initial load), the legacy key could be removed to avoid it persisting forever.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +195 to +197
// First check for legacy key (no scope), then check for scoped key
const scopedKey = settingsKeyForGateway(defaults.gatewayUrl);
const raw = storage?.getItem(scopedKey) ?? storage?.getItem(SETTINGS_KEY_PREFIX + "default") ?? storage?.getItem("openclaw.control.settings.v1");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment describes lookup order backwards

The comment says "First check for legacy key (no scope), then check for scoped key", but the actual code does the opposite — it checks the scoped key first and falls back to the legacy key last. The same backwards comment appears at line 268 in persistSettings. This will confuse future readers who expect the described order to match the actual precedence.

The comment should read something like: "First check for scoped key, then fall back to legacy key for migration".

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/storage.ts
Line: 195-197

Comment:
**Comment describes lookup order backwards**

The comment says _"First check for legacy key (no scope), then check for scoped key"_, but the actual code does the opposite — it checks the scoped key first and falls back to the legacy key last. The same backwards comment appears at line 268 in `persistSettings`. This will confuse future readers who expect the described order to match the actual precedence.

The comment should read something like: _"First check for scoped key, then fall back to legacy key for migration"_.

How can I resolve this? If you propose a fix, please make it concise.

@steipete steipete merged commit 5ece9af into openclaw:main Mar 16, 2026
6 checks passed
@steipete
Copy link
Copy Markdown
Contributor

Landed via temp rebase onto main.

  • Gate: pnpm check (red on unrelated src/infra/bonjour.ts type error after rebase), pnpm build (pass), pnpm test (red on unrelated cron/outbound/exec-approval suites after rebase), pnpm --dir ui build (pass), pnpm --dir ui exec vitest run --config vitest.node.config.ts src/ui/storage.node.test.ts (pass)
  • Land commit: 6061a6a
  • Merge commit: 5ece9af

Thanks @bobBot-claw!

dustin-olenslager pushed a commit to dustin-olenslager/ironclaw-supreme that referenced this pull request Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Control UI: localStorage settings conflict across different basePath deployments

2 participants