diff --git a/.custom-gcl.reference.yml b/.custom-gcl.reference.yml
new file mode 100644
index 00000000..a068eaf6
--- /dev/null
+++ b/.custom-gcl.reference.yml
@@ -0,0 +1,37 @@
+# The golangci-lint version used to build the custom binary.
+# Require.
+version: v1.56.2
+
+# the name of the custom binary.
+# Optional.
+# Default: custom-gcl
+name: custom-golangci-lint
+
+# The directory path used to store the custom binary.
+# Optional.
+# Default: .
+destination: ./my/path/
+
+# The list of the plugins to integrate inside the custom binary.
+plugins:
+  # a plugin from a Go proxy
+  - module: 'github.com/example/plugin3'
+    version: v1.2.3
+
+  # a plugin from a Go proxy (with a specific import path)
+  - module: 'github.com/example/plugin4'
+    import: 'github.com/example/plugin4/foo'
+    version: v1.0.0
+
+  # a plugin from local source (with absolute path)
+  - module: 'github.com/example/plugin2'
+    path: /my/local/path/plugin2
+
+  # a plugin from local source (with relative path)
+  - module: 'github.com/example/plugin1'
+    path: ./my/local/path/plugin1
+
+  # a plugin from local source (with absolute path and a specific import path)
+  - module: 'github.com/example/plugin2'
+    import: 'github.com/example/plugin4/foo'
+    path: /my/local/path/plugin2
diff --git a/.gitignore b/.gitignore
index bb265328..827702fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,7 @@
 /vendor/
 coverage.out
 coverage.xml
+/custom-golangci-lint
+/custom-gcl
+.custom-gcl.yml
+.custom-gcl.yaml
diff --git a/.golangci.reference.yml b/.golangci.reference.yml
index ab0a47e5..6a623977 100644
--- a/.golangci.reference.yml
+++ b/.golangci.reference.yml
@@ -2481,6 +2481,10 @@ linters-settings:
   custom:
     # Each custom linter should have a unique name.
     example:
+      # The plugin type.
+      # It can be `goplugin` or `module`.
+      # Default: goplugin
+      type: module
       # The path to the plugin *.so. Can be absolute or local.
       # Required for each custom linter.
       path: /path/to/example.so
diff --git a/.golangci.yml b/.golangci.yml
index 48c3d05e..c9700f77 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -46,6 +46,9 @@ linters-settings:
       - whyNoLint
   gocyclo:
     min-complexity: 15
+  godox:
+    keywords:
+      - FIXME
   gofmt:
     rewrite-rules:
       - pattern: 'interface{}'
@@ -109,6 +112,7 @@ linters:
     - goconst
     - gocritic
     - gocyclo
+    - godox
     - gofmt
     - goimports
     - gomnd
diff --git a/cmd/golangci-lint/plugins.go b/cmd/golangci-lint/plugins.go
new file mode 100644
index 00000000..541ff762
--- /dev/null
+++ b/cmd/golangci-lint/plugins.go
@@ -0,0 +1,3 @@
+package main
+
+// This file is used to declare module plugins.
diff --git a/docs/src/config/sidebar.yml b/docs/src/config/sidebar.yml
index b034cdb4..34ca440d 100644
--- a/docs/src/config/sidebar.yml
+++ b/docs/src/config/sidebar.yml
@@ -46,3 +46,10 @@
       link: /contributing/faq/
     - label: This Website
       link: /contributing/website/
+- label: Plugins
+  items:
+    - label: Module Plugin System
+      link: /contributing/new-linters/
+    - label: Go Plugin System
+      link: /contributing/private-linters/
+
diff --git a/docs/src/docs/contributing/faq.mdx b/docs/src/docs/contributing/faq.mdx
index 9ed95b77..1da0c75a 100644
--- a/docs/src/docs/contributing/faq.mdx
+++ b/docs/src/docs/contributing/faq.mdx
@@ -2,10 +2,6 @@
 title: Contributing FAQ
 ---
 
-## How to write a custom linter
-
-See [there](/contributing/new-linters#how-to-write-a-custom-linter).
-
 ## How to add a new open-source linter to `golangci-lint`
 
 See [there](/contributing/new-linters#how-to-add-a-public-linter-to-golangci-lint).
@@ -16,7 +12,7 @@ See [there](/contributing/new-linters#how-to-add-a-private-linter-to-golangci-li
 
 ## How to update existing linter
 
-Just update it's version in `go.mod`.
+Just update its version in `go.mod`.
 
 ## How to add configuration option to existing linter
 
diff --git a/docs/src/docs/contributing/new-linters.mdx b/docs/src/docs/contributing/new-linters.mdx
index 8e2551cb..55da80e2 100644
--- a/docs/src/docs/contributing/new-linters.mdx
+++ b/docs/src/docs/contributing/new-linters.mdx
@@ -46,75 +46,7 @@ After that:
 Some people and organizations may choose to have custom-made linters run as a part of `golangci-lint`.
 Typically, these linters can't be open-sourced or too specific.
 
-Such linters can be added through Go's plugin library.
+Such linters can be added through 2 plugin systems:
 
-For a private linter (which acts as a plugin) to work properly,
-the plugin as well as the golangci-lint binary **needs to be built for the same environment**.
-
-`CGO_ENABLED` is another requirement.
-
-This means that `golangci-lint` needs to be built for whatever machine you intend to run it on
-(cloning the golangci-lint repository and running a `CGO_ENABLED=1 make build` should do the trick for your machine).
-
-### Configure a Plugin
-
-If you already have a linter plugin available, you can follow these steps to define its usage in a projects `.golangci.yml` file.
-
-An example linter can be found at [here](https://github.com/golangci/example-plugin-linter).
-
-If you're looking for instructions on how to configure your own custom linter, they can be found further down.
-
-1. If the project you want to lint does not have one already, copy the [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) to the root directory.
-2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
-  ```yaml
-  linters-settings:
-    custom:
-      example:
-        path: /example.so
-        description: The description of the linter
-        original-url: github.com/golangci/example-linter
-        settings: # Settings are optional.
-          one: Foo
-          two:
-            - name: Bar
-          three:
-            name: Bar
-  ```
-
-That is all the configuration that is required to run a custom linter in your project.
-
-Custom linters are enabled by default, but abide by the same rules as other linters.
-
-If the disable all option is specified either on command line or in `.golang.yml` files `linters.disable-all: true`, custom linters will be disabled;
-they can be re-enabled by adding them to the `linters:enable` list,
-or providing the enabled option on the command line, `golangci-lint run -Eexample`.
-
-The configuration inside the `settings` field of linter have some limitations (there are NOT related to the plugin system itself):
-we use Viper to handle the configuration but Viper put all the keys in lowercase, and `.` cannot be used inside a key.
-
-### Create a Plugin
-
-Your linter must provide one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
-
-Your project should also use `go.mod`.
-
-All versions of libraries that overlap `golangci-lint` (including replaced libraries) MUST be set to the same version as `golangci-lint`.
-You can see the versions by running `go version -m golangci-lint`.
-
-You'll also need to create a Go file like `plugin/example.go`.
-
-This file MUST be in the package `main`, and MUST define an exposed function called `New` with the following signature:
-```go
-func New(conf any) ([]*analysis.Analyzer, error) {
-	// ...
-}
-```
-
-See [plugin/example.go](https://github.com/golangci/example-plugin-linter/blob/master/plugin/example.go) for more info.
-
-To build the plugin, from the root project directory, run:
-```bash
-go build -buildmode=plugin plugin/example.go
-```
-
-This will create a plugin `*.so` file that can be copied into your project or another well known location for usage in `golangci-lint`.
+- [Go Plugin System](/plugins/module-plugins)
+- [Module Plugin System](/plugins/go-plugins)
diff --git a/docs/src/docs/plugins/go-plugins.mdx b/docs/src/docs/plugins/go-plugins.mdx
new file mode 100644
index 00000000..ea54811a
--- /dev/null
+++ b/docs/src/docs/plugins/go-plugins.mdx
@@ -0,0 +1,76 @@
+---
+title: Go Plugin System
+---
+
+Private linters can be added through [Go's plugin system](https://pkg.go.dev/plugin).
+
+For a private linter (which acts as a plugin) to work properly,
+the plugin as well as the golangci-lint binary **needs to be built for the same environment**.
+
+`CGO_ENABLED` is another requirement.
+
+This means that `golangci-lint` needs to be built for whatever machine you intend to run it on
+(cloning the golangci-lint repository and running a `CGO_ENABLED=1 make build` should do the trick for your machine).
+
+## Create a Plugin
+
+Your linter must provide one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
+
+Your project should also use `go.mod`.
+
+All versions of libraries that overlap `golangci-lint` (including replaced libraries) MUST be set to the same version as `golangci-lint`.
+You can see the versions by running `go version -m golangci-lint`.
+
+You'll also need to create a Go file like `plugin/example.go`.
+
+This file MUST be in the package `main`, and MUST define an exposed function called `New` with the following signature:
+```go
+func New(conf any) ([]*analysis.Analyzer, error) {
+	// ...
+}
+```
+
+See [plugin/example.go](https://github.com/golangci/example-plugin-linter/blob/master/plugin/example.go) for more info.
+
+To build the plugin, from the root project directory, run:
+```bash
+go build -buildmode=plugin plugin/example.go
+```
+
+This will create a plugin `*.so` file that can be copied into your project or another well known location for usage in `golangci-lint`.
+
+## Configure a Plugin
+
+If you already have a linter plugin available, you can follow these steps to define its usage in a projects `.golangci.yml` file.
+
+An example linter can be found at [here](https://github.com/golangci/example-plugin-linter).
+
+If you're looking for instructions on how to configure your own custom linter, they can be found further down.
+
+1. If the project you want to lint does not have one already, copy the [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) to the root directory.
+2. Adjust the yaml to appropriate `linters-settings.custom` entries as so:
+  ```yaml title=.golangci.yml
+  linters-settings:
+    custom:
+      example:
+        path: /example.so
+        description: The description of the linter
+        original-url: github.com/golangci/example-linter
+        settings: # Settings are optional.
+          one: Foo
+          two:
+            - name: Bar
+          three:
+            name: Bar
+  ```
+
+That is all the configuration that is required to run a custom linter in your project.
+
+Custom linters are enabled by default, but abide by the same rules as other linters.
+
+If the disable all option is specified either on command line or in `.golang.yml` files `linters.disable-all: true`, custom linters will be disabled;
+they can be re-enabled by adding them to the `linters.enable` list,
+or providing the enabled option on the command line, `golangci-lint run -Eexample`.
+
+The configuration inside the `settings` field of linter have some limitations (there are NOT related to the plugin system itself):
+we use Viper to handle the configuration but Viper put all the keys in lowercase, and `.` cannot be used inside a key.
diff --git a/docs/src/docs/plugins/module-plugins.mdx b/docs/src/docs/plugins/module-plugins.mdx
new file mode 100644
index 00000000..a0f9d79b
--- /dev/null
+++ b/docs/src/docs/plugins/module-plugins.mdx
@@ -0,0 +1,71 @@
+---
+title: Module Plugin System
+---
+
+An example linter can be found at [here](https://github.com/golangci/example-plugin-module-linter/settings).
+
+## The Automatic Way
+
+- define your building configuration into `.custom-gcl.yml`
+- run the command `golangci-lint custom` ( or `golangci-lint custom -v` to have logs)
+- define the plugin inside the  `linters-settings.custom` section with the type `module`.
+- run your custom version of golangci-lint
+
+Requirements:
+- Go
+- git
+
+### Configuration Example
+
+```yaml title=.custom-gcl.yml
+version: v1.57.0
+plugins:
+  # a plugin from a Go proxy
+  - module: 'github.com/golangci/plugin1'
+    import: 'github.com/golangci/plugin1/foo'
+    version: v1.0.0
+
+  # a plugin from local source
+  - module: 'github.com/golangci/plugin2'
+    path: /my/local/path/plugin2
+```
+
+```yaml title=.golangci.yml
+linters-settings:
+  custom:
+    foo:
+      type: "module"
+      description: This is an example usage of a plugin linter.
+      settings:
+        message: hello
+
+linters:
+  disable-all: true
+  enable:
+    - foo
+```
+
+## The Manual Way
+
+- add a blank-import of your module inside `cmd/golangci-lint/plugins.go`
+- run `go mod tidy`. (the module containing the plugin will be imported)
+- run `make build`
+- define the plugin inside the configuration `linters-settings.custom` section with the type `module`.
+- run your custom version of golangci-lint
+
+### Configuration Example
+
+```yaml title=.golangci.yml
+linters-settings:
+  custom:
+    foo:
+      type: "module"
+      description: This is an example usage of a plugin linter.
+      settings:
+        message: hello
+
+linters:
+  disable-all: true
+  enable:
+    - foo
+```
diff --git a/go.mod b/go.mod
index 9435701c..cb496115 100644
--- a/go.mod
+++ b/go.mod
@@ -44,6 +44,7 @@ require (
 	github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a
 	github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e
 	github.com/golangci/misspell v0.4.1
+	github.com/golangci/plugin-module-register v0.0.0-20240305222101-f76272ec86ee
 	github.com/golangci/revgrep v0.5.2
 	github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed
 	github.com/gordonklaus/ineffassign v0.1.0
diff --git a/go.sum b/go.sum
index 6ef617f6..7965bda8 100644
--- a/go.sum
+++ b/go.sum
@@ -231,6 +231,8 @@ github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZ
 github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM=
 github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g=
 github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI=
+github.com/golangci/plugin-module-register v0.0.0-20240305222101-f76272ec86ee h1:nl5nPZ5b2O2dj5+LizmFQ8gNq0r65OfALkp0M8EWJ8E=
+github.com/golangci/plugin-module-register v0.0.0-20240305222101-f76272ec86ee/go.mod h1:mGTAkB/NoZMvAGMkiv4+pmmwVO+Gp+zeV77nggByWCc=
 github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU=
 github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA=
 github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs=
diff --git a/pkg/commands/custom.go b/pkg/commands/custom.go
new file mode 100644
index 00000000..3ecb724b
--- /dev/null
+++ b/pkg/commands/custom.go
@@ -0,0 +1,81 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/golangci/golangci-lint/pkg/commands/internal"
+	"github.com/golangci/golangci-lint/pkg/logutils"
+)
+
+const envKeepTempFiles = "CUSTOM_GCL_KEEP_TEMP_FILES"
+
+type customCommand struct {
+	cmd *cobra.Command
+
+	cfg *internal.Configuration
+
+	log logutils.Log
+}
+
+func newCustomCommand(logger logutils.Log) *customCommand {
+	c := &customCommand{log: logger}
+
+	customCmd := &cobra.Command{
+		Use:     "custom",
+		Short:   "Build a version of golangci-lint with custom linters.",
+		Args:    cobra.NoArgs,
+		PreRunE: c.preRunE,
+		RunE:    c.runE,
+	}
+
+	c.cmd = customCmd
+
+	return c
+}
+
+func (c *customCommand) preRunE(_ *cobra.Command, _ []string) error {
+	cfg, err := internal.LoadConfiguration()
+	if err != nil {
+		return err
+	}
+
+	err = cfg.Validate()
+	if err != nil {
+		return err
+	}
+
+	c.cfg = cfg
+
+	return nil
+}
+
+func (c *customCommand) runE(_ *cobra.Command, _ []string) error {
+	ctx := context.Background()
+
+	tmp, err := os.MkdirTemp(os.TempDir(), "custom-gcl")
+	if err != nil {
+		return fmt.Errorf("create temporary directory: %w", err)
+	}
+
+	defer func() {
+		if os.Getenv(envKeepTempFiles) != "" {
+			log.Printf("WARN: The env var %s has been dectected: the temporary directory is preserved: %s", envKeepTempFiles, tmp)
+
+			return
+		}
+
+		_ = os.RemoveAll(tmp)
+	}()
+
+	err = internal.NewBuilder(c.log, c.cfg, tmp).Build(ctx)
+	if err != nil {
+		return fmt.Errorf("build process: %w", err)
+	}
+
+	return nil
+}
diff --git a/pkg/commands/help.go b/pkg/commands/help.go
index 655d1e67..42da4a3d 100644
--- a/pkg/commands/help.go
+++ b/pkg/commands/help.go
@@ -53,8 +53,7 @@ func newHelpCommand(logger logutils.Log) *helpCommand {
 func (c *helpCommand) preRunE(_ *cobra.Command, _ []string) error {
 	// The command doesn't depend on the real configuration.
 	// It just needs the list of all plugins and all presets.
-	dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(),
-		lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
+	dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(), lintersdb.NewLinterBuilder())
 	if err != nil {
 		return err
 	}
diff --git a/pkg/commands/internal/builder.go b/pkg/commands/internal/builder.go
new file mode 100644
index 00000000..dd783925
--- /dev/null
+++ b/pkg/commands/internal/builder.go
@@ -0,0 +1,219 @@
+package internal
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+	"unicode"
+
+	"github.com/golangci/golangci-lint/pkg/logutils"
+)
+
+// Builder runs all the required commands to build a binary.
+type Builder struct {
+	cfg *Configuration
+
+	log logutils.Log
+
+	root string
+	repo string
+}
+
+// NewBuilder creates a new Builder.
+func NewBuilder(logger logutils.Log, cfg *Configuration, root string) *Builder {
+	return &Builder{
+		cfg:  cfg,
+		log:  logger,
+		root: root,
+		repo: filepath.Join(root, "golangci-lint"),
+	}
+}
+
+// Build builds the custom binary.
+func (b Builder) Build(ctx context.Context) error {
+	b.log.Infof("Cloning golangci-lint repository.")
+
+	err := b.clone(ctx)
+	if err != nil {
+		return fmt.Errorf("clone golangci-lint: %w", err)
+	}
+
+	b.log.Infof("Adding plugin imports.")
+
+	err = b.updatePluginsFile()
+	if err != nil {
+		return fmt.Errorf("update plugin file: %w", err)
+	}
+
+	b.log.Infof("Adding replace directives.")
+
+	err = b.addReplaceDirectives(ctx)
+	if err != nil {
+		return fmt.Errorf("add replace directives: %w", err)
+	}
+
+	b.log.Infof("Running go mod tidy.")
+
+	err = b.goModTidy(ctx)
+	if err != nil {
+		return fmt.Errorf("go mod tidy: %w", err)
+	}
+
+	b.log.Infof("Building golangci-lint binary.")
+
+	binaryName := b.getBinaryName()
+
+	err = b.goBuild(ctx, binaryName)
+	if err != nil {
+		return fmt.Errorf("build golangci-lint binary: %w", err)
+	}
+
+	b.log.Infof("Moving golangci-lint binary.")
+
+	err = b.copyBinary(binaryName)
+	if err != nil {
+		return fmt.Errorf("move golangci-lint binary: %w", err)
+	}
+
+	return nil
+}
+
+func (b Builder) clone(ctx context.Context) error {
+	//nolint:gosec // the variable is sanitized.
+	cmd := exec.CommandContext(ctx,
+		"git", "clone", "--branch", sanitizeVersion(b.cfg.Version),
+		"--single-branch", "--depth", "1", "-c advice.detachedHead=false", "-q",
+		"https://github.com/golangci/golangci-lint.git",
+	)
+	cmd.Dir = b.root
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		b.log.Infof(string(output))
+
+		return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
+	}
+
+	return nil
+}
+
+func (b Builder) addReplaceDirectives(ctx context.Context) error {
+	for _, plugin := range b.cfg.Plugins {
+		if plugin.Path == "" {
+			continue
+		}
+
+		replace := fmt.Sprintf("%s=%s", plugin.Module, plugin.Path)
+
+		cmd := exec.CommandContext(ctx, "go", "mod", "edit", "-replace", replace)
+		cmd.Dir = b.repo
+
+		b.log.Infof("run: %s", strings.Join(cmd.Args, " "))
+
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			b.log.Warnf(string(output))
+
+			return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
+		}
+	}
+
+	return nil
+}
+
+func (b Builder) goModTidy(ctx context.Context) error {
+	cmd := exec.CommandContext(ctx, "go", "mod", "tidy")
+	cmd.Dir = b.repo
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		b.log.Warnf(string(output))
+
+		return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
+	}
+
+	return nil
+}
+
+func (b Builder) goBuild(ctx context.Context, binaryName string) error {
+	//nolint:gosec // the variable is sanitized.
+	cmd := exec.CommandContext(ctx, "go", "build",
+		"-ldflags",
+		fmt.Sprintf(
+			"-s -w -X 'main.version=%s-custom-gcl' -X 'main.date=%s'",
+			sanitizeVersion(b.cfg.Version), time.Now().UTC().String(),
+		),
+		"-o", binaryName,
+		"./cmd/golangci-lint",
+	)
+	cmd.Dir = b.repo
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		b.log.Warnf(string(output))
+
+		return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
+	}
+
+	return nil
+}
+
+func (b Builder) copyBinary(binaryName string) error {
+	src := filepath.Join(b.repo, binaryName)
+
+	source, err := os.Open(filepath.Clean(src))
+	if err != nil {
+		return fmt.Errorf("open source file: %w", err)
+	}
+
+	defer func() { _ = source.Close() }()
+
+	info, err := source.Stat()
+	if err != nil {
+		return fmt.Errorf("stat source file: %w", err)
+	}
+
+	if b.cfg.Destination != "" {
+		err = os.MkdirAll(b.cfg.Destination, os.ModePerm)
+		if err != nil {
+			return fmt.Errorf("create destination directory: %w", err)
+		}
+	}
+
+	dst, err := os.OpenFile(filepath.Join(b.cfg.Destination, binaryName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
+	if err != nil {
+		return fmt.Errorf("create destination file: %w", err)
+	}
+
+	defer func() { _ = dst.Close() }()
+
+	_, err = io.Copy(dst, source)
+	if err != nil {
+		return fmt.Errorf("copy source to destination: %w", err)
+	}
+
+	return nil
+}
+
+func (b Builder) getBinaryName() string {
+	name := b.cfg.Name
+	if runtime.GOOS == "windows" {
+		name += ".exe"
+	}
+
+	return name
+}
+
+func sanitizeVersion(v string) string {
+	fn := func(c rune) bool {
+		return !(unicode.IsLetter(c) || unicode.IsNumber(c) || c == '.' || c == '/')
+	}
+
+	return strings.Join(strings.FieldsFunc(v, fn), "")
+}
diff --git a/pkg/commands/internal/builder_test.go b/pkg/commands/internal/builder_test.go
new file mode 100644
index 00000000..508755cf
--- /dev/null
+++ b/pkg/commands/internal/builder_test.go
@@ -0,0 +1,57 @@
+package internal
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_sanitizeVersion(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		input    string
+		expected string
+	}{
+		{
+			desc:     "ampersand",
+			input:    " te&st",
+			expected: "test",
+		},
+		{
+			desc:     "pipe",
+			input:    " te|st",
+			expected: "test",
+		},
+		{
+			desc:     "version",
+			input:    "v1.2.3",
+			expected: "v1.2.3",
+		},
+		{
+			desc:     "branch",
+			input:    "feat/test",
+			expected: "feat/test",
+		},
+		{
+			desc:     "branch",
+			input:    "value --key",
+			expected: "valuekey",
+		},
+		{
+			desc:     "hash",
+			input:    "cd8b1177",
+			expected: "cd8b1177",
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			v := sanitizeVersion(test.input)
+
+			assert.Equal(t, test.expected, v)
+		})
+	}
+}
diff --git a/pkg/commands/internal/configuration.go b/pkg/commands/internal/configuration.go
new file mode 100644
index 00000000..53270259
--- /dev/null
+++ b/pkg/commands/internal/configuration.go
@@ -0,0 +1,138 @@
+package internal
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"gopkg.in/yaml.v3"
+)
+
+const base = ".custom-gcl"
+
+const defaultBinaryName = "custom-gcl"
+
+// Configuration represents the configuration file.
+type Configuration struct {
+	// golangci-lint version.
+	Version string `yaml:"version"`
+
+	// Name of the binary.
+	Name string `yaml:"name,omitempty"`
+
+	// Destination is the path to a directory to store the binary.
+	Destination string `yaml:"destination,omitempty"`
+
+	// Plugins information.
+	Plugins []*Plugin `yaml:"plugins,omitempty"`
+}
+
+// Validate checks and clean the configuration.
+func (c *Configuration) Validate() error {
+	if strings.TrimSpace(c.Version) == "" {
+		return errors.New("root field 'version' is required")
+	}
+
+	if strings.TrimSpace(c.Name) == "" {
+		c.Name = defaultBinaryName
+	}
+
+	if len(c.Plugins) == 0 {
+		return errors.New("no plugins defined")
+	}
+
+	for _, plugin := range c.Plugins {
+		if strings.TrimSpace(plugin.Module) == "" {
+			return errors.New("field 'module' is required")
+		}
+
+		if strings.TrimSpace(plugin.Import) == "" {
+			plugin.Import = plugin.Module
+		}
+
+		if strings.TrimSpace(plugin.Path) == "" && strings.TrimSpace(plugin.Version) == "" {
+			return errors.New("missing information: 'version' or 'path' should be provided")
+		}
+
+		if strings.TrimSpace(plugin.Path) != "" && strings.TrimSpace(plugin.Version) != "" {
+			return errors.New("invalid configuration: 'version' and 'path' should not be provided at the same time")
+		}
+
+		if strings.TrimSpace(plugin.Path) == "" {
+			continue
+		}
+
+		abs, err := filepath.Abs(plugin.Path)
+		if err != nil {
+			return err
+		}
+
+		plugin.Path = abs
+	}
+
+	return nil
+}
+
+// Plugin represents information about a plugin.
+type Plugin struct {
+	// Module name.
+	Module string `yaml:"module"`
+
+	// Import to use.
+	Import string `yaml:"import,omitempty"`
+
+	// Version of the module.
+	// Only for module available through a Go proxy.
+	Version string `yaml:"version,omitempty"`
+
+	// Path to the local module.
+	// Only for local module.
+	Path string `yaml:"path,omitempty"`
+}
+
+func LoadConfiguration() (*Configuration, error) {
+	configFilePath, err := findConfigurationFile()
+	if err != nil {
+		return nil, fmt.Errorf("file %s not found: %w", configFilePath, err)
+	}
+
+	file, err := os.Open(configFilePath)
+	if err != nil {
+		return nil, fmt.Errorf("file %s open: %w", configFilePath, err)
+	}
+
+	var cfg Configuration
+
+	err = yaml.NewDecoder(file).Decode(&cfg)
+	if err != nil {
+		return nil, fmt.Errorf("YAML decoding: %w", err)
+	}
+
+	return &cfg, nil
+}
+
+func findConfigurationFile() (string, error) {
+	entries, err := os.ReadDir(".")
+	if err != nil {
+		return "", fmt.Errorf("read directory: %w", err)
+	}
+
+	for _, entry := range entries {
+		ext := filepath.Ext(entry.Name())
+
+		switch strings.ToLower(strings.TrimPrefix(ext, ".")) {
+		case "yml", "yaml", "json":
+			if isConf(ext, entry.Name()) {
+				return entry.Name(), nil
+			}
+		}
+	}
+
+	return "", errors.New("configuration file not found")
+}
+
+func isConf(ext, name string) bool {
+	return base+ext == name
+}
diff --git a/pkg/commands/internal/configuration_test.go b/pkg/commands/internal/configuration_test.go
new file mode 100644
index 00000000..3c85a907
--- /dev/null
+++ b/pkg/commands/internal/configuration_test.go
@@ -0,0 +1,127 @@
+package internal
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestConfiguration_Validate(t *testing.T) {
+	testCases := []struct {
+		desc string
+		cfg  *Configuration
+	}{
+		{
+			desc: "version",
+			cfg: &Configuration{
+				Version: "v1.57.0",
+				Plugins: []*Plugin{
+					{
+						Module:  "example.org/foo/bar",
+						Import:  "example.org/foo/bar/test",
+						Version: "v1.2.3",
+					},
+				},
+			},
+		},
+		{
+			desc: "path",
+			cfg: &Configuration{
+				Version: "v1.57.0",
+				Plugins: []*Plugin{
+					{
+						Module: "example.org/foo/bar",
+						Import: "example.org/foo/bar/test",
+						Path:   "/my/path",
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.cfg.Validate()
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestConfiguration_Validate_error(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		cfg      *Configuration
+		expected string
+	}{
+		{
+			desc:     "missing version",
+			cfg:      &Configuration{},
+			expected: "root field 'version' is required",
+		},
+		{
+			desc: "no plugins",
+			cfg: &Configuration{
+				Version: "v1.57.0",
+			},
+			expected: "no plugins defined",
+		},
+		{
+			desc: "missing module",
+			cfg: &Configuration{
+				Version: "v1.57.0",
+				Plugins: []*Plugin{
+					{
+						Module:  "",
+						Import:  "example.org/foo/bar/test",
+						Version: "v1.2.3",
+						Path:    "/my/path",
+					},
+				},
+			},
+			expected: "field 'module' is required",
+		},
+		{
+			desc: "module version and path",
+			cfg: &Configuration{
+				Version: "v1.57.0",
+				Plugins: []*Plugin{
+					{
+						Module:  "example.org/foo/bar",
+						Import:  "example.org/foo/bar/test",
+						Version: "v1.2.3",
+						Path:    "/my/path",
+					},
+				},
+			},
+			expected: "invalid configuration: 'version' and 'path' should not be provided at the same time",
+		},
+		{
+			desc: "no module version and path",
+			cfg: &Configuration{
+				Version: "v1.57.0",
+				Plugins: []*Plugin{
+					{
+						Module: "example.org/foo/bar",
+						Import: "example.org/foo/bar/test",
+					},
+				},
+			},
+			expected: "missing information: 'version' or 'path' should be provided",
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.cfg.Validate()
+
+			assert.EqualError(t, err, test.expected)
+		})
+	}
+}
diff --git a/pkg/commands/internal/imports.go b/pkg/commands/internal/imports.go
new file mode 100644
index 00000000..3bebf596
--- /dev/null
+++ b/pkg/commands/internal/imports.go
@@ -0,0 +1,69 @@
+package internal
+
+import (
+	"bytes"
+	"fmt"
+	"go/format"
+	"os"
+	"path/filepath"
+	"text/template"
+)
+
+const importsTemplate = `
+package main
+
+import (
+{{range .Imports -}}
+	_ "{{.}}"
+{{end -}}
+)
+`
+
+func (b Builder) updatePluginsFile() error {
+	importsDest := filepath.Join(b.repo, "cmd", "golangci-lint", "plugins.go")
+
+	info, err := os.Stat(importsDest)
+	if err != nil {
+		return fmt.Errorf("file %s not found: %w", importsDest, err)
+	}
+
+	source, err := generateImports(b.cfg)
+	if err != nil {
+		return fmt.Errorf("generate imports: %w", err)
+	}
+
+	b.log.Infof("generated imports info %s:\n%s\n", importsDest, source)
+
+	err = os.WriteFile(filepath.Clean(importsDest), source, info.Mode())
+	if err != nil {
+		return fmt.Errorf("write file %s: %w", importsDest, err)
+	}
+
+	return nil
+}
+
+func generateImports(cfg *Configuration) ([]byte, error) {
+	impTmpl, err := template.New("plugins.go").Parse(importsTemplate)
+	if err != nil {
+		return nil, fmt.Errorf("parse template: %w", err)
+	}
+
+	var imps []string
+	for _, plugin := range cfg.Plugins {
+		imps = append(imps, plugin.Import)
+	}
+
+	buf := &bytes.Buffer{}
+
+	err = impTmpl.Execute(buf, map[string]any{"Imports": imps})
+	if err != nil {
+		return nil, fmt.Errorf("execute template: %w", err)
+	}
+
+	source, err := format.Source(buf.Bytes())
+	if err != nil {
+		return nil, fmt.Errorf("format source: %w", err)
+	}
+
+	return source, nil
+}
diff --git a/pkg/commands/internal/imports_test.go b/pkg/commands/internal/imports_test.go
new file mode 100644
index 00000000..049e55e7
--- /dev/null
+++ b/pkg/commands/internal/imports_test.go
@@ -0,0 +1,36 @@
+package internal
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_generateImports(t *testing.T) {
+	cfg := &Configuration{
+		Version: "v1.57.0",
+		Plugins: []*Plugin{
+			{
+				Module:  "example.org/foo/bar",
+				Import:  "example.org/foo/bar/test",
+				Version: "v1.2.3",
+			},
+			{
+				Module: "example.com/foo/bar",
+				Import: "example.com/foo/bar/test",
+				Path:   "/my/path",
+			},
+		},
+	}
+
+	data, err := generateImports(cfg)
+	require.NoError(t, err)
+
+	expected, err := os.ReadFile(filepath.Join("testdata", "imports.go"))
+	require.NoError(t, err)
+
+	assert.Equal(t, expected, data)
+}
diff --git a/pkg/commands/internal/testdata/imports.go b/pkg/commands/internal/testdata/imports.go
new file mode 100644
index 00000000..c3c4f131
--- /dev/null
+++ b/pkg/commands/internal/testdata/imports.go
@@ -0,0 +1,6 @@
+package main
+
+import (
+	_ "example.com/foo/bar/test"
+	_ "example.org/foo/bar/test"
+)
diff --git a/pkg/commands/linters.go b/pkg/commands/linters.go
index bd1057b5..08394770 100644
--- a/pkg/commands/linters.go
+++ b/pkg/commands/linters.go
@@ -65,7 +65,7 @@ func (c *lintersCommand) preRunE(cmd *cobra.Command, _ []string) error {
 	}
 
 	dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
-		lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
+		lintersdb.NewLinterBuilder(), lintersdb.NewPluginModuleBuilder(c.log), lintersdb.NewPluginGoBuilder(c.log))
 	if err != nil {
 		return err
 	}
diff --git a/pkg/commands/root.go b/pkg/commands/root.go
index de2710ca..a3e57be6 100644
--- a/pkg/commands/root.go
+++ b/pkg/commands/root.go
@@ -63,6 +63,7 @@ func newRootCommand(info BuildInfo) *rootCommand {
 		newCacheCommand().cmd,
 		newConfigCommand(log).cmd,
 		newVersionCommand(info).cmd,
+		newCustomCommand(log).cmd,
 	)
 
 	rootCmd.SetHelpCommand(newHelpCommand(log).cmd)
diff --git a/pkg/commands/run.go b/pkg/commands/run.go
index 0e1be529..610a9e9e 100644
--- a/pkg/commands/run.go
+++ b/pkg/commands/run.go
@@ -180,7 +180,7 @@ func (c *runCommand) persistentPostRunE(_ *cobra.Command, _ []string) error {
 
 func (c *runCommand) preRunE(_ *cobra.Command, _ []string) error {
 	dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
-		lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
+		lintersdb.NewLinterBuilder(), lintersdb.NewPluginModuleBuilder(c.log), lintersdb.NewPluginGoBuilder(c.log))
 	if err != nil {
 		return err
 	}
diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go
index 588aa643..484ce0cc 100644
--- a/pkg/config/linters_settings.go
+++ b/pkg/config/linters_settings.go
@@ -3,6 +3,7 @@ package config
 import (
 	"encoding"
 	"errors"
+	"fmt"
 	"runtime"
 
 	"gopkg.in/yaml.v3"
@@ -281,7 +282,17 @@ type LintersSettings struct {
 }
 
 func (s *LintersSettings) Validate() error {
-	return s.Govet.Validate()
+	if err := s.Govet.Validate(); err != nil {
+		return err
+	}
+
+	for name, settings := range s.Custom {
+		if err := settings.Validate(); err != nil {
+			return fmt.Errorf("custom linter %q: %w", name, err)
+		}
+	}
+
+	return nil
 }
 
 type AsasalintSettings struct {
@@ -946,17 +957,15 @@ type WSLSettings struct {
 }
 
 // CustomLinterSettings encapsulates the meta-data of a private linter.
-// For example, a private linter may be added to the golangci config file as shown below.
-//
-//	linters-settings:
-//	 custom:
-//	   example:
-//	     path: /example.so
-//	     description: The description of the linter
-//	     original-url: github.com/golangci/example-linter
 type CustomLinterSettings struct {
+	// Type plugin type.
+	// It can be `goplugin` or `module`.
+	Type string `mapstructure:"type"`
+
 	// Path to a plugin *.so file that implements the private linter.
+	// Only for Go plugin system.
 	Path string
+
 	// Description describes the purpose of the private linter.
 	Description string
 	// OriginalURL The URL containing the source code for the private linter.
@@ -965,3 +974,19 @@ type CustomLinterSettings struct {
 	// Settings plugin settings only work with linterdb.PluginConstructor symbol.
 	Settings any
 }
+
+func (s *CustomLinterSettings) Validate() error {
+	if s.Type == "module" {
+		if s.Path != "" {
+			return errors.New("path not supported with module type")
+		}
+
+		return nil
+	}
+
+	if s.Path == "" {
+		return errors.New("path is required")
+	}
+
+	return nil
+}
diff --git a/pkg/config/linters_settings_test.go b/pkg/config/linters_settings_test.go
new file mode 100644
index 00000000..1fd77d2e
--- /dev/null
+++ b/pkg/config/linters_settings_test.go
@@ -0,0 +1,236 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLintersSettings_Validate(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		settings *LintersSettings
+	}{
+		{
+			desc: "custom linter",
+			settings: &LintersSettings{
+				Custom: map[string]CustomLinterSettings{
+					"example": {
+						Type: "module",
+					},
+				},
+			},
+		},
+		{
+			desc: "govet",
+			settings: &LintersSettings{
+				Govet: GovetSettings{
+					Enable:     []string{"a"},
+					DisableAll: true,
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.settings.Validate()
+			assert.NoError(t, err)
+		})
+	}
+}
+
+func TestLintersSettings_Validate_error(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		settings *LintersSettings
+		expected string
+	}{
+		{
+			desc: "custom linter error",
+			settings: &LintersSettings{
+				Custom: map[string]CustomLinterSettings{
+					"example": {
+						Type: "module",
+						Path: "example",
+					},
+				},
+			},
+			expected: `custom linter "example": path not supported with module type`,
+		},
+		{
+			desc: "govet error",
+			settings: &LintersSettings{
+				Govet: GovetSettings{
+					EnableAll:  true,
+					DisableAll: true,
+				},
+			},
+			expected: "govet: enable-all and disable-all can't be combined",
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.settings.Validate()
+
+			assert.EqualError(t, err, test.expected)
+		})
+	}
+}
+
+func TestCustomLinterSettings_Validate(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		settings *CustomLinterSettings
+	}{
+		{
+			desc: "only path",
+			settings: &CustomLinterSettings{
+				Path: "example",
+			},
+		},
+		{
+			desc: "path and type goplugin",
+			settings: &CustomLinterSettings{
+				Type: "goplugin",
+				Path: "example",
+			},
+		},
+		{
+			desc: "type module",
+			settings: &CustomLinterSettings{
+				Type: "module",
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.settings.Validate()
+			assert.NoError(t, err)
+		})
+	}
+}
+
+func TestCustomLinterSettings_Validate_error(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		settings *CustomLinterSettings
+		expected string
+	}{
+		{
+			desc:     "missing path",
+			settings: &CustomLinterSettings{},
+			expected: "path is required",
+		},
+		{
+			desc: "module and path",
+			settings: &CustomLinterSettings{
+				Type: "module",
+				Path: "example",
+			},
+			expected: "path not supported with module type",
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.settings.Validate()
+
+			assert.EqualError(t, err, test.expected)
+		})
+	}
+}
+
+func TestGovetSettings_Validate(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		settings *GovetSettings
+	}{
+		{
+			desc:     "empty",
+			settings: &GovetSettings{},
+		},
+		{
+			desc: "disable-all and enable",
+			settings: &GovetSettings{
+				Enable:     []string{"a"},
+				DisableAll: true,
+			},
+		},
+		{
+			desc: "enable-all and disable",
+			settings: &GovetSettings{
+				Disable:   []string{"a"},
+				EnableAll: true,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.settings.Validate()
+			assert.NoError(t, err)
+		})
+	}
+}
+
+func TestGovetSettings_Validate_error(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		settings *GovetSettings
+		expected string
+	}{
+		{
+			desc: "enable-all and disable-all",
+			settings: &GovetSettings{
+				EnableAll:  true,
+				DisableAll: true,
+			},
+			expected: "govet: enable-all and disable-all can't be combined",
+		},
+		{
+			desc: "enable-all and enable",
+			settings: &GovetSettings{
+				EnableAll: true,
+				Enable:    []string{"a"},
+			},
+			expected: "govet: enable-all and enable can't be combined",
+		},
+		{
+			desc: "disable-all and disable",
+			settings: &GovetSettings{
+				DisableAll: true,
+				Disable:    []string{"a"},
+			},
+			expected: "govet: disable-all and disable can't be combined",
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			err := test.settings.Validate()
+
+			assert.EqualError(t, err, test.expected)
+		})
+	}
+}
diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go
index 60ec2497..954315d3 100644
--- a/pkg/lint/lintersdb/builder_linter.go
+++ b/pkg/lint/lintersdb/builder_linter.go
@@ -16,9 +16,9 @@ func NewLinterBuilder() *LinterBuilder {
 
 // Build loads all the "internal" linters.
 // The configuration is use for the linter settings.
-func (b LinterBuilder) Build(cfg *config.Config) []*linter.Config {
+func (b LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
 	if cfg == nil {
-		return nil
+		return nil, nil
 	}
 
 	const megacheckName = "megacheck"
@@ -707,5 +707,5 @@ func (b LinterBuilder) Build(cfg *config.Config) []*linter.Config {
 			WithSince("v1.26.0").
 			WithPresets(linter.PresetStyle).
 			WithURL("https://github.com/golangci/golangci-lint/blob/master/pkg/golinters/nolintlint/README.md"),
-	}
+	}, nil
 }
diff --git a/pkg/lint/lintersdb/builder_plugin.go b/pkg/lint/lintersdb/builder_plugin_go.go
similarity index 72%
rename from pkg/lint/lintersdb/builder_plugin.go
rename to pkg/lint/lintersdb/builder_plugin_go.go
index 401a645c..4c6b4b59 100644
--- a/pkg/lint/lintersdb/builder_plugin.go
+++ b/pkg/lint/lintersdb/builder_plugin_go.go
@@ -14,43 +14,51 @@ import (
 	"github.com/golangci/golangci-lint/pkg/logutils"
 )
 
+const goPluginType = "goplugin"
+
 type AnalyzerPlugin interface {
 	GetAnalyzers() []*analysis.Analyzer
 }
 
-// PluginBuilder builds the custom linters (plugins) based on the configuration.
-type PluginBuilder struct {
+// PluginGoBuilder builds the custom linters (Go plugin) based on the configuration.
+type PluginGoBuilder struct {
 	log logutils.Log
 }
 
-// NewPluginBuilder creates new PluginBuilder.
-func NewPluginBuilder(log logutils.Log) *PluginBuilder {
-	return &PluginBuilder{log: log}
+// NewPluginGoBuilder creates new PluginGoBuilder.
+func NewPluginGoBuilder(log logutils.Log) *PluginGoBuilder {
+	return &PluginGoBuilder{log: log}
 }
 
 // Build loads custom linters that are specified in the golangci-lint config file.
-func (b *PluginBuilder) Build(cfg *config.Config) []*linter.Config {
+func (b *PluginGoBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
 	if cfg == nil || b.log == nil {
-		return nil
+		return nil, nil
 	}
 
 	var linters []*linter.Config
 
 	for name, settings := range cfg.LintersSettings.Custom {
-		lc, err := b.loadConfig(cfg, name, settings)
+		if settings.Type != goPluginType && settings.Type != "" {
+			continue
+		}
+
+		settings := settings
+
+		lc, err := b.loadConfig(cfg, name, &settings)
 		if err != nil {
-			b.log.Errorf("Unable to load custom analyzer %s:%s, %v", name, settings.Path, err)
+			return nil, fmt.Errorf("unable to load custom analyzer %q: %s, %w", name, settings.Path, err)
 		} else {
 			linters = append(linters, lc)
 		}
 	}
 
-	return linters
+	return linters, nil
 }
 
 // loadConfig loads the configuration of private linters.
 // Private linters are dynamically loaded from .so plugin files.
-func (b *PluginBuilder) loadConfig(cfg *config.Config, name string, settings config.CustomLinterSettings) (*linter.Config, error) {
+func (b *PluginGoBuilder) loadConfig(cfg *config.Config, name string, settings *config.CustomLinterSettings) (*linter.Config, error) {
 	analyzers, err := b.getAnalyzerPlugin(cfg, settings.Path, settings.Settings)
 	if err != nil {
 		return nil, err
@@ -74,7 +82,7 @@ func (b *PluginBuilder) loadConfig(cfg *config.Config, name string, settings con
 // and returns the 'AnalyzerPlugin' interface implemented by the private plugin.
 // An error is returned if the private linter cannot be loaded
 // or the linter does not implement the AnalyzerPlugin interface.
-func (b *PluginBuilder) getAnalyzerPlugin(cfg *config.Config, path string, settings any) ([]*analysis.Analyzer, error) {
+func (b *PluginGoBuilder) getAnalyzerPlugin(cfg *config.Config, path string, settings any) ([]*analysis.Analyzer, error) {
 	if !filepath.IsAbs(path) {
 		// resolve non-absolute paths relative to config file's directory
 		path = filepath.Join(cfg.GetConfigDir(), path)
@@ -93,7 +101,7 @@ func (b *PluginBuilder) getAnalyzerPlugin(cfg *config.Config, path string, setti
 	return analyzers, nil
 }
 
-func (b *PluginBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*analysis.Analyzer, error) {
+func (b *PluginGoBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*analysis.Analyzer, error) {
 	symbol, err := plug.Lookup("New")
 	if err != nil {
 		analyzers, errP := b.lookupAnalyzerPlugin(plug)
@@ -113,7 +121,7 @@ func (b *PluginBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*anal
 	return constructor(settings)
 }
 
-func (b *PluginBuilder) lookupAnalyzerPlugin(plug *plugin.Plugin) ([]*analysis.Analyzer, error) {
+func (b *PluginGoBuilder) lookupAnalyzerPlugin(plug *plugin.Plugin) ([]*analysis.Analyzer, error) {
 	symbol, err := plug.Lookup("AnalyzerPlugin")
 	if err != nil {
 		return nil, err
diff --git a/pkg/lint/lintersdb/builder_plugin_module.go b/pkg/lint/lintersdb/builder_plugin_module.go
new file mode 100644
index 00000000..904b18bd
--- /dev/null
+++ b/pkg/lint/lintersdb/builder_plugin_module.go
@@ -0,0 +1,85 @@
+package lintersdb
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/golangci/plugin-module-register/register"
+
+	"github.com/golangci/golangci-lint/pkg/config"
+	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
+	"github.com/golangci/golangci-lint/pkg/lint/linter"
+	"github.com/golangci/golangci-lint/pkg/logutils"
+)
+
+const modulePluginType = "module"
+
+// PluginModuleBuilder builds the custom linters (module plugin) based on the configuration.
+type PluginModuleBuilder struct {
+	log logutils.Log
+}
+
+// NewPluginModuleBuilder creates new PluginModuleBuilder.
+func NewPluginModuleBuilder(log logutils.Log) *PluginModuleBuilder {
+	return &PluginModuleBuilder{log: log}
+}
+
+// Build loads custom linters that are specified in the golangci-lint config file.
+func (b *PluginModuleBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
+	if cfg == nil || b.log == nil {
+		return nil, nil
+	}
+
+	var linters []*linter.Config
+
+	for name, settings := range cfg.LintersSettings.Custom {
+		if settings.Type != modulePluginType {
+			continue
+		}
+
+		b.log.Infof("Loaded %s: %s", settings.Path, name)
+
+		newPlugin, err := register.GetPlugin(name)
+		if err != nil {
+			return nil, fmt.Errorf("plugin(%s): %w", name, err)
+		}
+
+		p, err := newPlugin(settings.Settings)
+		if err != nil {
+			return nil, fmt.Errorf("plugin(%s): newPlugin %w", name, err)
+		}
+
+		analyzers, err := p.BuildAnalyzers()
+		if err != nil {
+			return nil, fmt.Errorf("plugin(%s): BuildAnalyzers %w", name, err)
+		}
+
+		customLinter := goanalysis.NewLinter(name, settings.Description, analyzers, nil)
+
+		switch strings.ToLower(p.GetLoadMode()) {
+		case register.LoadModeSyntax:
+			customLinter = customLinter.WithLoadMode(goanalysis.LoadModeSyntax)
+		case register.LoadModeTypesInfo:
+			customLinter = customLinter.WithLoadMode(goanalysis.LoadModeTypesInfo)
+		default:
+			customLinter = customLinter.WithLoadMode(goanalysis.LoadModeTypesInfo)
+		}
+
+		lc := linter.NewConfig(customLinter).
+			WithEnabledByDefault().
+			WithURL(settings.OriginalURL)
+
+		switch strings.ToLower(p.GetLoadMode()) {
+		case register.LoadModeSyntax:
+			// noop
+		case register.LoadModeTypesInfo:
+			lc = lc.WithLoadForGoAnalysis()
+		default:
+			lc = lc.WithLoadForGoAnalysis()
+		}
+
+		linters = append(linters, lc)
+	}
+
+	return linters, nil
+}
diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go
index a55ef9a3..57d45dd4 100644
--- a/pkg/lint/lintersdb/manager.go
+++ b/pkg/lint/lintersdb/manager.go
@@ -1,6 +1,7 @@
 package lintersdb
 
 import (
+	"fmt"
 	"os"
 	"slices"
 	"sort"
@@ -17,7 +18,7 @@ import (
 const EnvTestRun = "GL_TEST_RUN"
 
 type Builder interface {
-	Build(cfg *config.Config) []*linter.Config
+	Build(cfg *config.Config) ([]*linter.Config, error)
 }
 
 // Manager is a type of database for all linters (internals or plugins).
@@ -48,7 +49,12 @@ func NewManager(log logutils.Log, cfg *config.Config, builders ...Builder) (*Man
 	}
 
 	for _, builder := range builders {
-		m.linters = append(m.linters, builder.Build(m.cfg)...)
+		linters, err := builder.Build(m.cfg)
+		if err != nil {
+			return nil, fmt.Errorf("build linters: %w", err)
+		}
+
+		m.linters = append(m.linters, linters...)
 	}
 
 	for _, lc := range m.linters {
diff --git a/pkg/result/processors/nolint_test.go b/pkg/result/processors/nolint_test.go
index 1588a2e1..d6fb56a8 100644
--- a/pkg/result/processors/nolint_test.go
+++ b/pkg/result/processors/nolint_test.go
@@ -34,8 +34,7 @@ func newNolint2FileIssue(line int) result.Issue {
 }
 
 func newTestNolintProcessor(log logutils.Log) *Nolint {
-	dbManager, _ := lintersdb.NewManager(log, config.NewDefault(),
-		lintersdb.NewPluginBuilder(log), lintersdb.NewLinterBuilder())
+	dbManager, _ := lintersdb.NewManager(log, config.NewDefault(), lintersdb.NewLinterBuilder())
 
 	return NewNolint(log, dbManager, nil)
 }