diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 232e8239e24c020cbbead967a7b984bdd8d59e8c..998f6d9a2b18193c95f88619a54b4a74756cc05e 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -610,10 +610,6 @@ Set this to `"enabled"` to have Renovate maintain a JSON file cache per-reposito
 Set to `"reset"` if you ever need to bypass the cache and have it overwritten.
 JSON files will be stored inside the `cacheDir` beside the existing file-based package cache.
 
-<!-- prettier-ignore -->
-!!! warning
-    This is an experimental feature and may be modified or removed in a future non-major release.
-
 ## requireConfig
 
 By default, Renovate needs a Renovate config file in each repository where it runs before it will propose any dependency updates.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index a5b945ad1ce9dac8542d23b3389709f1696c831b..aabe34a19550138a8a0aaf91a20f31c42f6ed47d 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -223,6 +223,7 @@ const options: RenovateOptions[] = [
     allowedValues: ['disabled', 'enabled', 'reset'],
     stage: 'repository',
     default: 'disabled',
+    experimental: true,
   },
   {
     name: 'force',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 9fde0ac3300c7a8ae9198bab4a36c16e2a535044..186b3cb50decc8311300739f63d4b2b335f2edda 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -341,9 +341,13 @@ export interface RenovateOptionBase {
   // used by tests
   relatedOptions?: string[];
 
-  releaseStatus?: 'alpha' | 'beta' | 'unpublished';
-
   stage?: RenovateConfigStage;
+
+  experimental?: boolean;
+
+  experimentalDescription?: string;
+
+  experimentalIssues?: number[];
 }
 
 export interface RenovateArrayOption<
diff --git a/test/website-docs.spec.ts b/test/website-docs.spec.ts
index 3895146656303c1c8eb6f62bdeaf78a1929d4bde..b398d2bb6471eed27831b185c95b22a793464c23 100644
--- a/test/website-docs.spec.ts
+++ b/test/website-docs.spec.ts
@@ -33,7 +33,6 @@ describe('website-docs', () => {
     .match(/\n## (.*?)\n/g)
     ?.map((match) => match.substring(4, match.length - 1));
   const expectedOptions = options
-    .filter((option) => option.releaseStatus !== 'unpublished')
     .filter((option) => !option.globalOnly)
     .filter((option) => !option.parent)
     .filter((option) => !option.autogenerated)
diff --git a/tools/docs/config.ts b/tools/docs/config.ts
index 7a2a412424bc461fd54706d377f0c30cb7fe5ddb..110773c666440decf53b7f56d0f56765b7845b0d 100644
--- a/tools/docs/config.ts
+++ b/tools/docs/config.ts
@@ -1,10 +1,12 @@
 import stringify from 'json-stringify-pretty-compact';
 import { getOptions } from '../../lib/config/options';
+import { getManagerList } from '../../lib/modules/manager';
 import { getCliName } from '../../lib/workers/global/config/parse/cli';
 import { getEnvName } from '../../lib/workers/global/config/parse/env';
 import { readFile, updateFile } from '../utils';
 
 const options = getOptions();
+const managers = new Set(getManagerList());
 
 /**
  * Merge string arrays one by one
@@ -86,6 +88,9 @@ function genTable(obj: [string, string][], type: string, def: any): string {
     'allowString',
     'admin',
     'globalOnly',
+    'experimental',
+    'experimentalDescription',
+    'experimentalIssues',
   ];
   obj.forEach(([key, val]) => {
     const el = [key, val];
@@ -142,13 +147,59 @@ function genTable(obj: [string, string][], type: string, def: any): string {
 }
 
 function stringifyArrays(el: Record<string, any>): void {
+  const ignoredKeys = ['default', 'experimentalIssues'];
+
   for (const [key, value] of Object.entries(el)) {
-    if (key !== 'default' && Array.isArray(value)) {
+    if (!ignoredKeys.includes(key) && Array.isArray(value)) {
       el[key] = value.join(', ');
     }
   }
 }
 
+function genExperimentalMsg(el: Record<string, any>): string {
+  const ghIssuesUrl = 'https://github.com/renovatebot/renovate/issues/';
+  let warning =
+    '\n<!-- prettier-ignore -->\n!!! warning "This feature is flagged as experimental"\n';
+
+  if (el.experimentalDescription) {
+    warning += indent`${2}${el.experimentalDescription}`;
+  } else {
+    warning += indent`${2}Experimental features might be changed or even removed at any time.`;
+  }
+
+  const issues = el.experimentalIssues ?? [];
+  if (issues.length > 0) {
+    warning += `<br>To track this feature visit the following GitHub ${
+      issues.length > 1 ? 'issues' : 'issue'
+    } `;
+    warning +=
+      (issues
+        .map((issue: number) => `[#${issue}](${ghIssuesUrl}${issue})`)
+        .join(', ') as string) + '.';
+  }
+
+  return warning + '\n';
+}
+
+function indexMarkdown(lines: string[]): Record<string, [number, number]> {
+  const indexed: Record<string, [number, number]> = {};
+
+  let optionName = '';
+  let start = 0;
+  for (const [i, line] of lines.entries()) {
+    if (line.startsWith('## ') || line.startsWith('### ')) {
+      if (optionName) {
+        indexed[optionName] = [start, i - 1];
+      }
+      start = i;
+      optionName = line.split(' ')[1];
+    }
+  }
+  indexed[optionName] = [start, lines.length - 1];
+
+  return indexed;
+}
+
 export async function generateConfig(dist: string, bot = false): Promise<void> {
   let configFile = `configuration-options.md`;
   if (bot) {
@@ -159,15 +210,24 @@ export async function generateConfig(dist: string, bot = false): Promise<void> {
     '\n'
   );
 
+  const indexed = indexMarkdown(configOptionsRaw);
+
   options
-    .filter((option) => option.releaseStatus !== 'unpublished')
+    .filter(
+      (option) => !!option.globalOnly === bot && !managers.has(option.name)
+    )
     .forEach((option) => {
-      // TODO: fix types (#9610)
+      // TODO: fix types (#7154,#9610)
       const el: Record<string, any> = { ...option };
-      let headerIndex = configOptionsRaw.indexOf(`## ${option.name}`);
-      if (headerIndex === -1) {
-        headerIndex = configOptionsRaw.indexOf(`### ${option.name}`);
+
+      if (!indexed[option.name]) {
+        throw new Error(
+          `Config option "${option.name}" is missing an entry in ${configFile}`
+        );
       }
+
+      const [headerIndex, footerIndex] = indexed[option.name];
+
       el.cli = getCliName(option);
       el.env = getEnvName(option);
       stringifyArrays(el);
@@ -175,6 +235,10 @@ export async function generateConfig(dist: string, bot = false): Promise<void> {
       configOptionsRaw[headerIndex] +=
         `\n${option.description}\n\n` +
         genTable(Object.entries(el), option.type, option.default);
+
+      if (el.experimental) {
+        configOptionsRaw[footerIndex] += genExperimentalMsg(el);
+      }
     });
 
   await updateFile(`${dist}/${configFile}`, configOptionsRaw.join('\n'));