Skip to content

Feature: configPatch in plugin manifest — automatic config setup on install #6792

Description

@LePetitPince

Problem

Installing an OpenClaw plugin that requires config changes is a multi-step, error-prone process:

# Step 1: Install the package
npm install -g claude-search-proxy

# Step 2: Register the plugin
openclaw plugins install --link $(npm root -g)/claude-search-proxy

# Step 3: Manually edit openclaw.json (the painful part)
# User must read docs, find the right JSON structure, paste it correctly

Step 3 creates real friction:

  • Users forget to do it → plugin silently doesn't work
  • Users get the JSON wrong → confusing validation errors
  • The config snippet lives in a README, not in the plugin itself → easy to miss
  • Every plugin that needs config has to re-explain this in their docs

This affects any plugin that needs more than just plugins.entries.<id>.enabled: true — search providers, webhook configs, tool settings, service endpoints, etc.

Proposal

Allow plugins to declare a configPatch field in their openclaw.plugin.json manifest. When openclaw plugins install runs, it deep-merges the patch into the user's config automatically.

Manifest example

{
  "id": "claude-search-proxy",
  "services": ["claude-search-proxy"],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  },
  "configPatch": {
    "tools": {
      "web": {
        "search": {
          "provider": "perplexity",
          "perplexity": {
            "baseUrl": "http://localhost:52480",
            "apiKey": "not-needed"
          }
        }
      }
    }
  }
}

Install behavior (proposed)

$ openclaw plugins install --link /usr/lib/node_modules/claude-search-proxy

  Linked plugin path: /usr/lib/node_modules/claude-search-proxy
  Applied config patch from plugin manifest:
    tools.web.search.provider = "perplexity"
    tools.web.search.perplexity.baseUrl = "http://localhost:52480"
    tools.web.search.perplexity.apiKey = "not-needed"
  Restart the gateway to load plugins.

The user sees exactly what changed. One command does everything.

Merge semantics

Deep merge, not replace. A plugin adding tools.web.search must not blow away an existing tools.web.fetch config:

// Pseudocode for the merge logic
import { merge } from 'deepmerge'; // or equivalent

function applyConfigPatch(currentConfig, pluginManifest) {
  if (!pluginManifest.configPatch) return currentConfig;
  
  const patched = deepMerge(currentConfig, pluginManifest.configPatch, {
    // Arrays: replace, don't concatenate (for predictability)
    arrayMerge: (_target, source) => source,
  });
  
  return patched;
}

Example merge behavior:

// Existing config
{
  "tools": {
    "web": {
      "fetch": { "enabled": true },
      "search": { "provider": "brave", "apiKey": "old-key" }
    }
  }
}

// Plugin's configPatch
{
  "tools": {
    "web": {
      "search": {
        "provider": "perplexity",
        "perplexity": { "baseUrl": "http://localhost:52480", "apiKey": "not-needed" }
      }
    }
  }
}

// Result after merge
{
  "tools": {
    "web": {
      "fetch": { "enabled": true },          // ← preserved
      "search": {
        "provider": "perplexity",             // ← overwritten
        "apiKey": "old-key",                  // ← preserved (not in patch)
        "perplexity": {                       // ← added
          "baseUrl": "http://localhost:52480",
          "apiKey": "not-needed"
        }
      }
    }
  }
}

Key design decisions

Decision Recommendation Rationale
When to apply On install only Not on every gateway start — user edits must stick
Overwrite existing keys? Yes, for keys in the patch Plugin knows what it needs; user can override after
Undo on remove? Optional (warn, don't auto-undo) Config may have been edited since install
Validation Run normal config validation after merge Catch invalid patches before writing
User confirmation Show diff, apply automatically Or add --no-patch flag to skip
Nested depth limit No limit (follow JSON structure) Config is already deeply nested

Removal behavior (optional)

$ openclaw plugins remove claude-search-proxy

  Removed plugin: claude-search-proxy
  ⚠ This plugin applied config changes during install:
    tools.web.search.provider = "perplexity"
    tools.web.search.perplexity.baseUrl = "http://localhost:52480"
  Review your config if you want to revert these.

Prior art within OpenClaw

The registerProvider auth flow already returns a configPatch object. Here's how copilot-proxy does it today:

// From extensions/copilot-proxy/index.ts
api.registerProvider({
  id: "copilot-proxy",
  auth: [{
    kind: "custom",
    run: async (ctx) => {
      // ... interactive prompts ...
      return {
        profiles: [/* ... */],
        configPatch: {                    // ← This already exists!
          models: {
            providers: {
              "copilot-proxy": {
                baseUrl,
                apiKey: DEFAULT_API_KEY,
                models: modelIds.map(buildModelDefinition),
              },
            },
          },
        },
        defaultModel: defaultModelRef,
      };
    },
  }],
});

This works great for LLM providers because they go through the auth flow. But plugins that provide services (not models) have no equivalent path. The manifest-level configPatch fills that gap.

Real-world use case

claude-search-proxy is an OpenClaw extension that runs a managed HTTP proxy as a service — turning any Claude subscription's WebSearch into an OpenAI-compatible API. The extension starts/stops the proxy with the gateway, monitors health, and handles graceful shutdown.

The proxy works immediately after install. But OpenClaw's web_search tool doesn't route through it until the user manually adds the search config. Currently the extension detects this and logs the config snippet needed — but a configPatch would eliminate the problem entirely.

Current experience (3 steps):

npm install -g claude-search-proxy
openclaw plugins install --link $(npm root -g)/claude-search-proxy
# Then manually edit openclaw.json... hope you read the README

Proposed experience (2 steps):

npm install -g claude-search-proxy
openclaw plugins install --link $(npm root -g)/claude-search-proxy
# Done. Config patched automatically. Restart gateway.

Other plugins that would benefit

Any plugin that needs config beyond being enabled:

  • Search provider plugins (this use case)
  • Webhook endpoint plugins that need hooks config
  • Tool plugins that need tools.* settings
  • Service plugins that need port/host configuration
  • Plugins that set up default models or aliases

Implementation sketch

The change is localized to the plugin install CLI command (cli/plugins-cli.js):

// In the install command handler, after copying/linking files:

// 1. Read the plugin manifest
const manifest = JSON.parse(readFileSync(join(pluginDir, 'openclaw.plugin.json'), 'utf-8'));

// 2. If configPatch exists, apply it
if (manifest.configPatch && typeof manifest.configPatch === 'object') {
  const currentConfig = readConfigFile();
  const patched = deepMerge(currentConfig, manifest.configPatch);
  
  // 3. Validate the result
  const validation = validateConfig(patched);
  if (!validation.valid) {
    console.warn('Plugin config patch failed validation:', validation.errors);
    console.warn('Skipping config patch. You may need to configure manually.');
  } else {
    // 4. Write and show what changed
    writeConfigFile(patched);
    console.log('Applied config patch from plugin manifest:');
    for (const [path, value] of flattenDiff(currentConfig, patched)) {
      console.log(`  ${path} = ${JSON.stringify(value)}`);
    }
  }
}

Happy to contribute a PR if this direction looks right. Thanks for considering it!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.enhancementNew feature or requestimpact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maturity:stableIssue affects a taxonomy feature currently scored M4/M5.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions