Repository: projectdiscovery/nuclei

Issue:
Nuclei v3: Custom templates directory configuration after -config-directory removal
### Issue Description

I'm trying to upgrade nuclei from v2 to v3 in my system, but encountering configuration issues. I realize there are two separate needs:

1. **Custom config directory** (for .templates-config.json, etc.) - The `-config-directory` flag was removed in v3
2. **Custom templates directory** - I know `-ud /data/nuclei-templates` works for this

### Current Situation

- **Templates directory**: I can use `-ud /data/nuclei-templates` to specify custom templates location ✅
- **Config directory**: No way to customize where nuclei stores its config files (like .templates-config.json) after `-config-directory` removal ❌

### Expected Behavior

It would be helpful to have:

1. **For config directory**: An environment variable like `NUCLEI_CONFIG_DIR` or bring back `-config-directory` flag
2. **For templates directory**: An environment variable like `NUCLEI_TEMPLATES_DIR` as an alternative to `-ud` flag

### Related Issue

This is related to #1612 which implemented `-config-directory` in v2 for customizing config files location, but this functionality appears to have been removed in v3.

### Use Cases

- **Containerized environments**: Need to specify both config and templates directories via environment variables
- **CI/CD pipelines**: Environment variables are easier than command line flags
- **Systems without home directory write access**: Need custom locations for both config and templates

### Question

Could you please clarify:
1. Is there a replacement for the removed `-config-directory` functionality in v3?
2. Would you consider adding environment variable support for both config directory and templates directory?

### Environment
- Nuclei version: v3.x
- Previous version: v2.x (where `-config-directory` worked)
- Use case: Systems without write access to default home directory, containerized deployments

Hints from issue comments:
@dogancanbakir: Thank you for raising this issue. Regarding `NUCLEI_CONFIG_DIR`, there is an environment variable. I will investigate why `config-directory` was removed and determine if we should reinstate it.

Solution patch:
diff --git a/DEBUG.md b/DEBUG.md
index 8d94b51f..dc5c6e8a 100644
--- a/DEBUG.md
+++ b/DEBUG.md
@@ -40,6 +40,8 @@ Nuclei was built with some environment variables in mind to help with debugging.
 | `SHOW_DSL_ERRORS=true`           | Enables Printing DSL Errors (that are hidden by default) |
 | `HIDE_TEMPLATE_SIG_WARNING=true` | Hides Template Signature Verification Warnings           |
 | `NUCLEI_LOG_ALL=true`            | Log All Events that were skipped in verbose mode         |
+| `NUCLEI_CONFIG_DIR`              | Sets custom configuration directory path                 |
+| `NUCLEI_TEMPLATES_DIR`           | Sets custom nuclei templates directory path              |
 
 
 
diff --git a/cmd/nuclei/main.go b/cmd/nuclei/main.go
index 56caa6fe..e2de5ff0 100644
--- a/cmd/nuclei/main.go
+++ b/cmd/nuclei/main.go
@@ -620,8 +620,13 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
 
 		}
 	}
-	if options.NewTemplatesDirectory != "" {
-		config.DefaultConfig.SetTemplatesDir(options.NewTemplatesDirectory)
+
+	templatesDir := options.NewTemplatesDirectory
+	if templatesDir == "" {
+		templatesDir = os.Getenv(config.NucleiTemplatesDirEnv)
+	}
+	if templatesDir != "" {
+		config.DefaultConfig.SetTemplatesDir(templatesDir)
 	}
 
 	defaultProfilesPath := filepath.Join(config.DefaultConfig.GetTemplateDir(), "profiles")
diff --git a/go.mod b/go.mod
index 56b91f6d..3ff00d3c 100644
--- a/go.mod
+++ b/go.mod
@@ -138,6 +138,7 @@ require (
 	github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
+	github.com/BurntSushi/toml v1.3.2 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
 	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
@@ -197,7 +198,7 @@ require (
 	github.com/containerd/log v0.1.0 // indirect
 	github.com/containerd/platforms v0.2.1 // indirect
 	github.com/cpuguy83/dockercfg v0.3.2 // indirect
-	github.com/cyphar/filepath-securejoin v0.4.1 // indirect
+	github.com/cyphar/filepath-securejoin v0.5.1 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/davidmz/go-pageant v1.0.2 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -309,7 +310,7 @@ require (
 	github.com/olekukonko/ll v0.0.9 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.1 // indirect
-	github.com/opencontainers/runc v1.2.3 // indirect
+	github.com/opencontainers/runc v1.2.8 // indirect
 	github.com/openrdap/rdap v0.9.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
diff --git a/go.sum b/go.sum
index fbbb3a7d..38f19344 100644
--- a/go.sum
+++ b/go.sum
@@ -310,8 +310,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
-github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
 github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
@@ -573,8 +573,8 @@ github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
-github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
-github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
+github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
+github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -1213,8 +1213,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
 github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
-github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
-github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
+github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q=
+github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI=
 github.com/openrdap/rdap v0.9.1 h1:Rv6YbanbiVPsKRvOLdUmlU1AL5+2OFuEFLjFN+mQsCM=
 github.com/openrdap/rdap v0.9.1/go.mod h1:vKSiotbsENrjM/vaHXLddXbW8iQkBfa+ldEuYEjyLTQ=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
diff --git a/pkg/catalog/config/constants.go b/pkg/catalog/config/constants.go
index f7fca13b..97c37930 100644
--- a/pkg/catalog/config/constants.go
+++ b/pkg/catalog/config/constants.go
@@ -40,6 +40,7 @@ const (
 	BinaryName                   = "nuclei"
 	FallbackConfigFolderName     = ".nuclei-config"
 	NucleiConfigDirEnv           = "NUCLEI_CONFIG_DIR"
+	NucleiTemplatesDirEnv        = "NUCLEI_TEMPLATES_DIR"
 )
 
 // IsOutdatedVersion compares two versions and returns true



Test patch:
diff --git a/cmd/integration-test/integration-test.go b/cmd/integration-test/integration-test.go
index 10af587c..82ca1bc8 100644
--- a/cmd/integration-test/integration-test.go
+++ b/cmd/integration-test/integration-test.go
@@ -46,6 +46,7 @@ var (
 		"library":         libraryTestcases,
 		"templatesPath":   templatesPathTestCases,
 		"templatesDir":    templatesDirTestCases,
+		"env_vars":        templatesDirEnvTestCases,
 		"file":            fileTestcases,
 		"offlineHttp":     offlineHttpTestcases,
 		"customConfigDir": customConfigDirTestCases,
diff --git a/cmd/integration-test/templates-dir-env.go b/cmd/integration-test/templates-dir-env.go
new file mode 100644
index 00000000..3435c18b
--- /dev/null
+++ b/cmd/integration-test/templates-dir-env.go
@@ -0,0 +1,217 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+
+	osutils "github.com/projectdiscovery/utils/os"
+
+	"github.com/projectdiscovery/nuclei/v3/pkg/testutils"
+	"github.com/projectdiscovery/utils/errkit"
+)
+
+// isNotLinux returns true if not running on Linux (used to skip tests on non-Linux OS)
+var isNotLinux = func() bool { return !osutils.IsLinux() }
+
+var templatesDirEnvTestCases = []TestCaseInfo{
+	{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvBasicTest{}, DisableOn: isNotLinux},
+	{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvAbsolutePathTest{}, DisableOn: isNotLinux},
+	{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvRelativePathTest{}, DisableOn: isNotLinux},
+	{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvPrecedenceTest{}, DisableOn: isNotLinux},
+	{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvCustomTemplatesTest{}, DisableOn: isNotLinux},
+}
+
+// copyTemplateToDir copies a template file to a destination directory, preserving the directory structure
+func copyTemplateToDir(templatePath, destDir string) error {
+	// Read the template file
+	templateData, err := os.ReadFile(templatePath)
+	if err != nil {
+		return errkit.Wrap(err, "failed to read template file")
+	}
+
+	// Create the destination path preserving directory structure
+	destPath := filepath.Join(destDir, templatePath)
+	destDirPath := filepath.Dir(destPath)
+
+	// Create the destination directory if it doesn't exist
+	if err := os.MkdirAll(destDirPath, 0755); err != nil {
+		return errkit.Wrap(err, "failed to create destination directory")
+	}
+
+	// Write the template file
+	if err := os.WriteFile(destPath, templateData, 0644); err != nil {
+		return errkit.Wrap(err, "failed to write template file")
+	}
+
+	return nil
+}
+
+// templatesDirEnvBasicTest tests basic functionality of NUCLEI_TEMPLATES_DIR
+type templatesDirEnvBasicTest struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *templatesDirEnvBasicTest) Execute(filePath string) error {
+	tempdir, err := os.MkdirTemp("", "nuclei-templates-dir-env-*")
+	if err != nil {
+		return errkit.Wrap(err, "failed to create temp dir")
+	}
+	defer func() {
+		_ = os.RemoveAll(tempdir)
+	}()
+
+	// Copy template to temp directory
+	if err := copyTemplateToDir(filePath, tempdir); err != nil {
+		return err
+	}
+
+	// Set NUCLEI_TEMPLATES_DIR and run nuclei
+	envVars := []string{"NUCLEI_TEMPLATES_DIR=" + tempdir}
+	results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
+	if err != nil {
+		return err
+	}
+
+	return expectResultsCount(results, 1)
+}
+
+// templatesDirEnvAbsolutePathTest tests that absolute paths work correctly
+type templatesDirEnvAbsolutePathTest struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *templatesDirEnvAbsolutePathTest) Execute(filePath string) error {
+	tempdir, err := os.MkdirTemp("", "nuclei-templates-dir-env-abs-*")
+	if err != nil {
+		return errkit.Wrap(err, "failed to create temp dir")
+	}
+	defer func() {
+		_ = os.RemoveAll(tempdir)
+	}()
+
+	// Get absolute path
+	absTempDir, err := filepath.Abs(tempdir)
+	if err != nil {
+		return errkit.Wrap(err, "failed to get absolute path")
+	}
+
+	// Copy template to temp directory
+	if err := copyTemplateToDir(filePath, absTempDir); err != nil {
+		return err
+	}
+
+	// Set NUCLEI_TEMPLATES_DIR with absolute path and run nuclei
+	envVars := []string{"NUCLEI_TEMPLATES_DIR=" + absTempDir}
+	results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
+	if err != nil {
+		return err
+	}
+
+	return expectResultsCount(results, 1)
+}
+
+// templatesDirEnvRelativePathTest tests that relative paths are resolved correctly
+type templatesDirEnvRelativePathTest struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *templatesDirEnvRelativePathTest) Execute(filePath string) error {
+	// Create temp directory in current working directory
+	tempdir, err := os.MkdirTemp(".", "nuclei-templates-dir-env-rel-*")
+	if err != nil {
+		return errkit.Wrap(err, "failed to create temp dir")
+	}
+	defer func() {
+		_ = os.RemoveAll(tempdir)
+	}()
+
+	// Get relative path (just the directory name)
+	relPath := filepath.Base(tempdir)
+
+	// Copy template to temp directory
+	if err := copyTemplateToDir(filePath, tempdir); err != nil {
+		return err
+	}
+
+	// Set NUCLEI_TEMPLATES_DIR with relative path and run nuclei
+	// Note: The implementation should convert relative paths to absolute
+	envVars := []string{"NUCLEI_TEMPLATES_DIR=" + relPath}
+	results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
+	if err != nil {
+		return err
+	}
+
+	return expectResultsCount(results, 1)
+}
+
+// templatesDirEnvPrecedenceTest tests that -ud flag takes precedence over NUCLEI_TEMPLATES_DIR
+type templatesDirEnvPrecedenceTest struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *templatesDirEnvPrecedenceTest) Execute(filePath string) error {
+	// Create two temp directories
+	envTempDir, err := os.MkdirTemp("", "nuclei-templates-dir-env-*")
+	if err != nil {
+		return errkit.Wrap(err, "failed to create env temp dir")
+	}
+	defer func() {
+		_ = os.RemoveAll(envTempDir)
+	}()
+
+	flagTempDir, err := os.MkdirTemp("", "nuclei-templates-dir-flag-*")
+	if err != nil {
+		return errkit.Wrap(err, "failed to create flag temp dir")
+	}
+	defer func() {
+		_ = os.RemoveAll(flagTempDir)
+	}()
+
+	// Copy template to flag temp directory (this should be used due to precedence)
+	if err := copyTemplateToDir(filePath, flagTempDir); err != nil {
+		return err
+	}
+
+	// Set NUCLEI_TEMPLATES_DIR to envTempDir (should be ignored due to -ud flag)
+	envVars := []string{"NUCLEI_TEMPLATES_DIR=" + envTempDir}
+	// Use -ud flag which should take precedence
+	results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com", "-ud", flagTempDir)
+	if err != nil {
+		return err
+	}
+
+	return expectResultsCount(results, 1)
+}
+
+// templatesDirEnvCustomTemplatesTest tests that custom template subdirectories are correctly set
+type templatesDirEnvCustomTemplatesTest struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *templatesDirEnvCustomTemplatesTest) Execute(filePath string) error {
+	tempdir, err := os.MkdirTemp("", "nuclei-templates-dir-custom-*")
+	if err != nil {
+		return errkit.Wrap(err, "failed to create temp dir")
+	}
+	defer func() {
+		_ = os.RemoveAll(tempdir)
+	}()
+
+	// Create custom template subdirectories structure
+	customDirs := []string{"github", "s3", "gitlab", "azure"}
+	for _, dir := range customDirs {
+		customDirPath := filepath.Join(tempdir, dir)
+		if err := os.MkdirAll(customDirPath, 0755); err != nil {
+			return errkit.Wrap(err, "failed to create custom template directory")
+		}
+	}
+
+	// Copy template to temp directory
+	if err := copyTemplateToDir(filePath, tempdir); err != nil {
+		return err
+	}
+
+	// Set NUCLEI_TEMPLATES_DIR and run nuclei
+	envVars := []string{"NUCLEI_TEMPLATES_DIR=" + tempdir}
+	results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
+	if err != nil {
+		return err
+	}
+
+	return expectResultsCount(results, 1)
+}