diff --git a/internal/project/project.go b/internal/project/project.go
index 2d88a52d..b5728533 100644
--- a/internal/project/project.go
+++ b/internal/project/project.go
@@ -18,6 +18,7 @@ package project
 
 import (
 	"fmt"
+	"os"
 
 	"github.com/arduino/arduino-lint/internal/configuration"
 	"github.com/arduino/arduino-lint/internal/project/library"
@@ -87,7 +88,7 @@ func findProjects(targetPath *paths.Path) ([]Type, error) {
 	} else {
 		if configuration.SuperprojectTypeFilter() == projecttype.All || configuration.Recursive() {
 			// Project discovery and/or type detection is required.
-			foundParentProjects = findProjectsUnderPath(targetPath, configuration.SuperprojectTypeFilter(), configuration.Recursive())
+			foundParentProjects = findProjectsUnderPath(targetPath, configuration.SuperprojectTypeFilter(), configuration.Recursive(), 0)
 		} else {
 			// Project was explicitly defined by user.
 			foundParentProjects = append(foundParentProjects,
@@ -115,7 +116,7 @@ func findProjects(targetPath *paths.Path) ([]Type, error) {
 }
 
 // findProjectsUnderPath finds projects of the given type under the given path. It returns a slice containing the definitions of all found projects.
-func findProjectsUnderPath(targetPath *paths.Path, projectTypeFilter projecttype.Type, recursive bool) []Type {
+func findProjectsUnderPath(targetPath *paths.Path, projectTypeFilter projecttype.Type, recursive bool, symlinkDepth int) []Type {
 	var foundProjects []Type
 
 	isProject, foundProjectType := isProject(targetPath, projectTypeFilter)
@@ -134,11 +135,26 @@ func findProjectsUnderPath(targetPath *paths.Path, projectTypeFilter projecttype
 	}
 
 	if recursive {
+		if symlinkDepth > 10 {
+			panic(fmt.Sprintf("symlink depth exceeded maximum while finding projects under %s", targetPath))
+		}
 		// targetPath was not a project, so search the subfolders.
 		directoryListing, _ := targetPath.ReadDir()
 		directoryListing.FilterDirs()
 		for _, potentialProjectDirectory := range directoryListing {
-			foundProjects = append(foundProjects, findProjectsUnderPath(potentialProjectDirectory, projectTypeFilter, recursive)...)
+			// It is possible for a combination of symlinks to parent paths to cause project discovery to get stuck in
+			// an endless loop of recursion. This is avoided by keeping count of the depth of symlinks and discontinuing
+			// recursion when it exceeds reason.
+			pathStat, err := os.Lstat(potentialProjectDirectory.String())
+			if err != nil {
+				panic(err)
+			}
+			depthDelta := 0
+			if pathStat.Mode()&os.ModeSymlink != 0 {
+				depthDelta = 1
+			}
+
+			foundProjects = append(foundProjects, findProjectsUnderPath(potentialProjectDirectory, projectTypeFilter, recursive, symlinkDepth+depthDelta)...)
 		}
 	}
 
@@ -184,7 +200,7 @@ func findSubprojects(superproject Type, apexSuperprojectType projecttype.Type) [
 			directoryListing.FilterDirs()
 
 			for _, subprojectPath := range directoryListing {
-				immediateSubprojects = append(immediateSubprojects, findProjectsUnderPath(subprojectPath, subProjectType, searchPathsRecursively)...)
+				immediateSubprojects = append(immediateSubprojects, findProjectsUnderPath(subprojectPath, subProjectType, searchPathsRecursively, 0)...)
 			}
 		}
 	}
diff --git a/internal/project/project_test.go b/internal/project/project_test.go
index 825e7ade..25a1a31e 100644
--- a/internal/project/project_test.go
+++ b/internal/project/project_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/arduino/arduino-lint/internal/util/test"
 	"github.com/arduino/go-paths-helper"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var testDataPath *paths.Path
@@ -38,6 +39,29 @@ func init() {
 	testDataPath = paths.New(workingDirectory, "testdata")
 }
 
+func TestSymlinkLoop(t *testing.T) {
+	// Set up directory structure of test library.
+	libraryPath, err := paths.TempDir().MkTempDir("TestSymlinkLoop")
+	defer libraryPath.RemoveAll() // Clean up after the test.
+	require.Nil(t, err)
+	err = libraryPath.Join("TestSymlinkLoop.h").WriteFile([]byte{})
+	require.Nil(t, err)
+	examplesPath := libraryPath.Join("examples")
+	err = examplesPath.Mkdir()
+	require.Nil(t, err)
+
+	// It's probably most friendly for contributors using Windows to create the symlinks needed for the test on demand.
+	err = os.Symlink(examplesPath.Join("..").String(), examplesPath.Join("UpGoer1").String())
+	require.Nil(t, err, "This test must be run as administrator on Windows to have symlink creation privilege.")
+	// It's necessary to have multiple symlinks to a parent directory to create the loop.
+	err = os.Symlink(examplesPath.Join("..").String(), examplesPath.Join("UpGoer2").String())
+	require.Nil(t, err)
+
+	configuration.Initialize(test.ConfigurationFlags(), []string{libraryPath.String()})
+
+	assert.Panics(t, func() { FindProjects() }, "Infinite symlink loop encountered during project discovery")
+}
+
 func TestFindProjects(t *testing.T) {
 	sketchPath := testDataPath.Join("Sketch")
 	libraryPath := testDataPath.Join("Library")