diff --git a/docs/usage/config-validation.md b/docs/usage/config-validation.md
index e5bc4755c36f0aefc240f85c3bf9e66035ed7a6a..f6b6f36a362f11d300a616f9f399d44836d29b10 100644
--- a/docs/usage/config-validation.md
+++ b/docs/usage/config-validation.md
@@ -48,3 +48,20 @@ $ renovate-config-validator first_config.json
 
 You can create a [pre-commit](https://pre-commit.com) hook to validate your configuration automatically.
 Go to the [`renovatebot/pre-commit-hooks` repository](https://github.com/renovatebot/pre-commit-hooks) for more information.
+
+### Validation of Renovate config change PRs
+
+Renovate can validate configuration changes in Pull Requests when you use a special branch name.
+
+Follow these steps to validate your configuration:
+
+1. Create a new Git branch that matches the `{{branchPrefix}}reconfigure` pattern. For example, if you're using the default prefix `renovate/`, your branch name must be `renovate/reconfigure`.
+1. Commit your updated Renovate config file to this branch, and push it to your Git hosting platform.
+
+The next time Renovate runs on that repo it will:
+
+1. Search for a branch that matches the special reconfigure pattern.
+1. Check for a config file in the reconfigure branch. Renovate can even find a renamed configuration file (compared to the config file in the default branch).
+1. Add a passing or failing status to the branch, depending on the outcome of the config validation run.
+1. If there's an _open_ pull request with validation errors from the _reconfigure_ branch then Renovate comments in the PR with details.
+1. Validate each commit the next time Renovate runs on the repository, until the PR is merged.
diff --git a/lib/util/cache/repository/types.ts b/lib/util/cache/repository/types.ts
index d4753ae4929e49dca9804a7f51d018e24de95bac..4560e7aa74023a01c352b3f8507b43855b9ef46d 100644
--- a/lib/util/cache/repository/types.ts
+++ b/lib/util/cache/repository/types.ts
@@ -42,6 +42,11 @@ export interface OnboardingBranchCache {
   configFileParsed?: string;
 }
 
+export interface ReconfigureBranchCache {
+  reconfigureBranchSha: string;
+  isConfigValid: boolean;
+}
+
 export interface PrCache {
   /**
    * Fingerprint of the PR body
@@ -129,6 +134,7 @@ export interface RepoCacheData {
   };
   prComments?: Record<number, Record<string, string>>;
   onboardingBranchCache?: OnboardingBranchCache;
+  reconfigureBranchCache?: ReconfigureBranchCache;
 }
 
 export interface RepoCache {
diff --git a/lib/workers/repository/finalize/index.ts b/lib/workers/repository/finalize/index.ts
index e0afb3893f1c1b82b8f2a16212fa743d8138af44..c4b291b6b1bb2709e31dd204ea1be03c16c2687a 100644
--- a/lib/workers/repository/finalize/index.ts
+++ b/lib/workers/repository/finalize/index.ts
@@ -5,6 +5,7 @@ import * as repositoryCache from '../../../util/cache/repository';
 import { clearRenovateRefs } from '../../../util/git';
 import { configMigration } from '../config-migration';
 import { PackageFiles } from '../package-files';
+import { validateReconfigureBranch } from '../reconfigure';
 import { pruneStaleBranches } from './prune';
 import {
   runBranchSummary,
@@ -16,6 +17,7 @@ export async function finalizeRepo(
   config: RenovateConfig,
   branchList: string[]
 ): Promise<void> {
+  await validateReconfigureBranch(config);
   await configMigration(config, branchList);
   await repositoryCache.saveCache();
   await pruneStaleBranches(config, branchList);
diff --git a/lib/workers/repository/finalize/prune.spec.ts b/lib/workers/repository/finalize/prune.spec.ts
index cca995e99eccfe467345d5f42a564d2289ea8eb6..3818b973aea8e74526f924cbff9155c1f54b8ac6 100644
--- a/lib/workers/repository/finalize/prune.spec.ts
+++ b/lib/workers/repository/finalize/prune.spec.ts
@@ -37,6 +37,12 @@ describe('workers/repository/finalize/prune', () => {
       expect(git.getBranchList).toHaveBeenCalledTimes(0);
     });
 
+    it('ignores reconfigure branch', async () => {
+      delete config.branchList;
+      await cleanup.pruneStaleBranches(config, config.branchList);
+      expect(git.getBranchList).toHaveBeenCalledTimes(0);
+    });
+
     it('returns if no renovate branches', async () => {
       config.branchList = [];
       git.getBranchList.mockReturnValueOnce([]);
diff --git a/lib/workers/repository/finalize/prune.ts b/lib/workers/repository/finalize/prune.ts
index 87cb4fb4d0cf18600e339c5de5cf2fc9a00655c3..15f06d56f216a62058a61055ffde4eeb435af530 100644
--- a/lib/workers/repository/finalize/prune.ts
+++ b/lib/workers/repository/finalize/prune.ts
@@ -6,6 +6,7 @@ import { platform } from '../../../modules/platform';
 import { ensureComment } from '../../../modules/platform/comment';
 import { scm } from '../../../modules/platform/scm';
 import { getBranchList, setUserRepoConfig } from '../../../util/git';
+import { getReconfigureBranchName } from '../reconfigure';
 
 async function cleanUpBranches(
   config: RenovateConfig,
@@ -109,8 +110,10 @@ export async function pruneStaleBranches(
     return;
   }
   // TODO: types (#22198)
-  let renovateBranches = getBranchList().filter((branchName) =>
-    branchName.startsWith(config.branchPrefix!)
+  let renovateBranches = getBranchList().filter(
+    (branchName) =>
+      branchName.startsWith(config.branchPrefix!) &&
+      branchName !== getReconfigureBranchName(config.branchPrefix!)
   );
   if (!renovateBranches?.length) {
     logger.debug('No renovate branches found');
diff --git a/lib/workers/repository/init/merge.ts b/lib/workers/repository/init/merge.ts
index a0233d22e5594afcc0a75db55fec61ee212480eb..7c4b2b0b81ac279ac746a3fb6690383c1e7bdddf 100644
--- a/lib/workers/repository/init/merge.ts
+++ b/lib/workers/repository/init/merge.ts
@@ -32,7 +32,7 @@ import {
 import { OnboardingState } from '../onboarding/common';
 import type { RepoFileConfig } from './types';
 
-async function detectConfigFile(): Promise<string | null> {
+export async function detectConfigFile(): Promise<string | null> {
   const fileList = await scm.getFileList();
   for (const fileName of configFileNames) {
     if (fileName === 'package.json') {
diff --git a/lib/workers/repository/reconfigure/index.spec.ts b/lib/workers/repository/reconfigure/index.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9efbf29065d5e7ba332d67975377852934e18af8
--- /dev/null
+++ b/lib/workers/repository/reconfigure/index.spec.ts
@@ -0,0 +1,196 @@
+import { mock } from 'jest-mock-extended';
+import {
+  RenovateConfig,
+  fs,
+  git,
+  mocked,
+  platform,
+  scm,
+} from '../../../../test/util';
+import { logger } from '../../../logger';
+import type { Pr } from '../../../modules/platform/types';
+import * as _cache from '../../../util/cache/repository';
+import * as _merge from '../init/merge';
+import { validateReconfigureBranch } from '.';
+
+jest.mock('../../../util/cache/repository');
+jest.mock('../../../util/fs');
+jest.mock('../../../util/git');
+jest.mock('../init/merge');
+
+const cache = mocked(_cache);
+const merge = mocked(_merge);
+
+describe('workers/repository/reconfigure/index', () => {
+  const config: RenovateConfig = {
+    branchPrefix: 'prefix/',
+    baseBranch: 'base',
+  };
+
+  beforeEach(() => {
+    config.repository = 'some/repo';
+    merge.detectConfigFile.mockResolvedValue('renovate.json');
+    scm.branchExists.mockResolvedValue(true);
+    cache.getCache.mockReturnValue({});
+    git.getBranchCommit.mockReturnValue('sha');
+    fs.readLocalFile.mockResolvedValue(null);
+    platform.getBranchPr.mockResolvedValue(null);
+    platform.getBranchStatusCheck.mockResolvedValue(null);
+  });
+
+  it('no effect on repo with no reconfigure branch', async () => {
+    scm.branchExists.mockResolvedValueOnce(false);
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith('No reconfigure branch found');
+  });
+
+  it('logs error if config file search fails', async () => {
+    const err = new Error();
+    merge.detectConfigFile.mockRejectedValueOnce(err as never);
+    await validateReconfigureBranch(config);
+    expect(logger.error).toHaveBeenCalledWith(
+      { err },
+      'Error while searching for config file in reconfigure branch'
+    );
+  });
+
+  it('throws error if config file not found in reconfigure branch', async () => {
+    merge.detectConfigFile.mockResolvedValue(null);
+    await validateReconfigureBranch(config);
+    expect(logger.warn).toHaveBeenCalledWith(
+      'No config file found in reconfigure branch'
+    );
+  });
+
+  it('logs error if config file is unreadable', async () => {
+    const err = new Error();
+    fs.readLocalFile.mockRejectedValueOnce(err as never);
+    await validateReconfigureBranch(config);
+    expect(logger.error).toHaveBeenCalledWith(
+      { err },
+      'Error while reading config file'
+    );
+  });
+
+  it('throws error if config file is empty', async () => {
+    await validateReconfigureBranch(config);
+    expect(logger.warn).toHaveBeenCalledWith('Empty or invalid config file');
+  });
+
+  it('throws error if config file content is invalid', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "name":
+        }
+        `);
+    await validateReconfigureBranch(config);
+    expect(logger.error).toHaveBeenCalledWith(
+      { err: expect.any(Object) },
+      'Error while parsing config file'
+    );
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Failed - Unparsable config file',
+      state: 'red',
+    });
+  });
+
+  it('handles failed validation', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "enabledManagers": ["docker"]
+        }
+        `);
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      { errors: expect.any(String) },
+      'Validation Errors'
+    );
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Failed',
+      state: 'red',
+    });
+  });
+
+  it('adds comment if reconfigure PR exists', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "enabledManagers": ["docker"]
+        }
+        `);
+    platform.getBranchPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      { errors: expect.any(String) },
+      'Validation Errors'
+    );
+    expect(platform.setBranchStatus).toHaveBeenCalled();
+    expect(platform.ensureComment).toHaveBeenCalled();
+  });
+
+  it('handles successful validation', async () => {
+    const pJson = `
+    {
+       "renovate": {
+        "enabledManagers": ["npm"]
+       }
+    }
+    `;
+    merge.detectConfigFile.mockResolvedValue('package.json');
+    fs.readLocalFile.mockResolvedValueOnce(pJson).mockResolvedValueOnce(pJson);
+    await validateReconfigureBranch(config);
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Successful',
+      state: 'green',
+    });
+  });
+
+  it('skips validation if cache is valid', async () => {
+    cache.getCache.mockReturnValueOnce({
+      reconfigureBranchCache: {
+        reconfigureBranchSha: 'sha',
+        isConfigValid: false,
+      },
+    });
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      'Skipping validation check as branch sha is unchanged'
+    );
+  });
+
+  it('skips validation if status check present', async () => {
+    cache.getCache.mockReturnValueOnce({
+      reconfigureBranchCache: {
+        reconfigureBranchSha: 'new_sha',
+        isConfigValid: false,
+      },
+    });
+    platform.getBranchStatusCheck.mockResolvedValueOnce('green');
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      'Skipping validation check as status check already exists'
+    );
+  });
+
+  it('handles non-default config file', async () => {
+    merge.detectConfigFile.mockResolvedValue('.renovaterc');
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "enabledManagers": ["npm",]
+        }
+        `);
+    platform.getBranchPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
+    await validateReconfigureBranch(config);
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Successful',
+      state: 'green',
+    });
+  });
+});
diff --git a/lib/workers/repository/reconfigure/index.ts b/lib/workers/repository/reconfigure/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..281b2696bbd5aa62e41c9ff1c20251deb085d1c4
--- /dev/null
+++ b/lib/workers/repository/reconfigure/index.ts
@@ -0,0 +1,174 @@
+import is from '@sindresorhus/is';
+import JSON5 from 'json5';
+import type { RenovateConfig } from '../../../config/types';
+import { validateConfig } from '../../../config/validation';
+import { logger } from '../../../logger';
+import { platform } from '../../../modules/platform';
+import { ensureComment } from '../../../modules/platform/comment';
+import { scm } from '../../../modules/platform/scm';
+import { getCache } from '../../../util/cache/repository';
+import { readLocalFile } from '../../../util/fs';
+import { getBranchCommit } from '../../../util/git';
+import { regEx } from '../../../util/regex';
+import { detectConfigFile } from '../init/merge';
+import {
+  deleteReconfigureBranchCache,
+  setReconfigureBranchCache,
+} from './reconfigure-cache';
+
+export function getReconfigureBranchName(prefix: string): string {
+  return `${prefix}reconfigure`;
+}
+export async function validateReconfigureBranch(
+  config: RenovateConfig
+): Promise<void> {
+  logger.debug('validateReconfigureBranch()');
+  const context = `renovate/config-validation`;
+
+  const branchName = getReconfigureBranchName(config.branchPrefix!);
+  const branchExists = await scm.branchExists(branchName);
+
+  // this is something the user initiates, so skip if no branch exists
+  if (!branchExists) {
+    logger.debug('No reconfigure branch found');
+    deleteReconfigureBranchCache(); // in order to remove cache when the branch has been deleted
+    return;
+  }
+
+  // look for config file
+  // 1. check reconfigure branch cache and use the configFileName if it exists
+  // 2. checkout reconfigure branch and look for the config file, don't assume default configFileName
+  const branchSha = getBranchCommit(branchName)!;
+  const cache = getCache();
+  let configFileName: string | null = null;
+  const reconfigureCache = cache.reconfigureBranchCache;
+  // only use valid cached information
+  if (reconfigureCache?.reconfigureBranchSha === branchSha) {
+    logger.debug('Skipping validation check as branch sha is unchanged');
+    return;
+  }
+
+  const validationStatus = await platform.getBranchStatusCheck(
+    branchName,
+    'renovate/config-validation'
+  );
+  // if old status check is present skip validation
+  if (is.nonEmptyString(validationStatus)) {
+    logger.debug('Skipping validation check as status check already exists');
+    return;
+  }
+
+  try {
+    await scm.checkoutBranch(branchName);
+    configFileName = await detectConfigFile();
+  } catch (err) {
+    logger.error(
+      { err },
+      'Error while searching for config file in reconfigure branch'
+    );
+  }
+
+  if (!is.nonEmptyString(configFileName)) {
+    logger.warn('No config file found in reconfigure branch');
+    await platform.setBranchStatus({
+      branchName,
+      context,
+      description: 'Validation Failed - No config file found',
+      state: 'red',
+    });
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.defaultBranch!);
+    return;
+  }
+
+  let configFileRaw: string | null = null;
+  try {
+    configFileRaw = await readLocalFile(configFileName, 'utf8');
+  } catch (err) {
+    logger.error({ err }, 'Error while reading config file');
+  }
+
+  if (!is.nonEmptyString(configFileRaw)) {
+    logger.warn('Empty or invalid config file');
+    await platform.setBranchStatus({
+      branchName,
+      context,
+      description: 'Validation Failed - Empty/Invalid config file',
+      state: 'red',
+    });
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.baseBranch!);
+    return;
+  }
+
+  let configFileParsed: any;
+  try {
+    configFileParsed = JSON5.parse(configFileRaw);
+    // no need to confirm renovate field in package.json we already do it in `detectConfigFile()`
+    if (configFileName === 'package.json') {
+      configFileParsed = configFileParsed.renovate;
+    }
+  } catch (err) {
+    logger.error({ err }, 'Error while parsing config file');
+    await platform.setBranchStatus({
+      branchName,
+      context,
+      description: 'Validation Failed - Unparsable config file',
+      state: 'red',
+    });
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.baseBranch!);
+    return;
+  }
+
+  // perform validation and provide a passing or failing check run based on result
+  const validationResult = await validateConfig(configFileParsed);
+
+  // failing check
+  if (validationResult.errors.length > 0) {
+    logger.debug(
+      { errors: validationResult.errors.map((err) => err.message).join(', ') },
+      'Validation Errors'
+    );
+
+    // add comment to reconfigure PR if it exists
+    const branchPr = await platform.getBranchPr(
+      branchName,
+      config.defaultBranch
+    );
+    if (branchPr) {
+      let body = `There is an error with this repository's Renovate configuration that needs to be fixed.\n\n`;
+      body += `Location: \`${configFileName}\`\n`;
+      body += `Message: \`${validationResult.errors
+        .map((e) => e.message)
+        .join(', ')
+        .replace(regEx(/`/g), "'")}\`\n`;
+
+      await ensureComment({
+        number: branchPr.number,
+        topic: 'Action Required: Fix Renovate Configuration',
+        content: body,
+      });
+    }
+    await platform.setBranchStatus({
+      branchName,
+      context,
+      description: 'Validation Failed',
+      state: 'red',
+    });
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.baseBranch!);
+    return;
+  }
+
+  // passing check
+  await platform.setBranchStatus({
+    branchName,
+    context,
+    description: 'Validation Successful',
+    state: 'green',
+  });
+
+  setReconfigureBranchCache(branchSha, true);
+  await scm.checkoutBranch(config.baseBranch!);
+}
diff --git a/lib/workers/repository/reconfigure/reconfigure-cache.spec.ts b/lib/workers/repository/reconfigure/reconfigure-cache.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..166f69f1495040932ada959d681e8cf19d3abc50
--- /dev/null
+++ b/lib/workers/repository/reconfigure/reconfigure-cache.spec.ts
@@ -0,0 +1,58 @@
+import { mocked } from '../../../../test/util';
+import * as _cache from '../../../util/cache/repository';
+import type { RepoCacheData } from '../../../util/cache/repository/types';
+import {
+  deleteReconfigureBranchCache,
+  setReconfigureBranchCache,
+} from './reconfigure-cache';
+
+jest.mock('../../../util/cache/repository');
+
+const cache = mocked(_cache);
+
+describe('workers/repository/reconfigure/reconfigure-cache', () => {
+  describe('setReconfigureBranchCache()', () => {
+    it('sets new cache', () => {
+      const dummyCache = {} satisfies RepoCacheData;
+      cache.getCache.mockReturnValue(dummyCache);
+      setReconfigureBranchCache('reconfigure-sha', false);
+      expect(dummyCache).toEqual({
+        reconfigureBranchCache: {
+          reconfigureBranchSha: 'reconfigure-sha',
+          isConfigValid: false,
+        },
+      });
+    });
+
+    it('updates old cache', () => {
+      const dummyCache = {
+        reconfigureBranchCache: {
+          reconfigureBranchSha: 'reconfigure-sha',
+          isConfigValid: false,
+        },
+      } satisfies RepoCacheData;
+      cache.getCache.mockReturnValue(dummyCache);
+      setReconfigureBranchCache('reconfigure-sha-1', false);
+      expect(dummyCache).toEqual({
+        reconfigureBranchCache: {
+          reconfigureBranchSha: 'reconfigure-sha-1',
+          isConfigValid: false,
+        },
+      });
+    });
+  });
+
+  describe('deleteReconfigureBranchCache()', () => {
+    it('deletes cache', () => {
+      const dummyCache = {
+        reconfigureBranchCache: {
+          reconfigureBranchSha: 'reconfigure-sha',
+          isConfigValid: false,
+        },
+      } satisfies RepoCacheData;
+      cache.getCache.mockReturnValue(dummyCache);
+      deleteReconfigureBranchCache();
+      expect(dummyCache.reconfigureBranchCache).toBeUndefined();
+    });
+  });
+});
diff --git a/lib/workers/repository/reconfigure/reconfigure-cache.ts b/lib/workers/repository/reconfigure/reconfigure-cache.ts
new file mode 100644
index 0000000000000000000000000000000000000000..03bd9678375cc2ab0e6e03b8f7afc42246fcf1d8
--- /dev/null
+++ b/lib/workers/repository/reconfigure/reconfigure-cache.ts
@@ -0,0 +1,28 @@
+import { logger } from '../../../logger';
+import { getCache } from '../../../util/cache/repository';
+
+export function setReconfigureBranchCache(
+  reconfigureBranchSha: string,
+  isConfigValid: boolean
+): void {
+  const cache = getCache();
+  const reconfigureBranchCache = {
+    reconfigureBranchSha,
+    isConfigValid,
+  };
+  if (cache.reconfigureBranchCache) {
+    logger.debug({ reconfigureBranchCache }, 'Update reconfigure branch cache');
+  } else {
+    logger.debug({ reconfigureBranchCache }, 'Create reconfigure branch cache');
+  }
+  cache.reconfigureBranchCache = reconfigureBranchCache;
+}
+
+export function deleteReconfigureBranchCache(): void {
+  const cache = getCache();
+
+  if (cache?.reconfigureBranchCache) {
+    logger.debug('Delete reconfigure branch cache');
+    delete cache.reconfigureBranchCache;
+  }
+}