diff --git a/README.md b/README.md
index db2e090a9..1d24b4321 100644
--- a/README.md
+++ b/README.md
@@ -200,19 +200,24 @@ configuration, by redirecting it to a file and editing it.
 
 ### Configuring the Command Line Tool
 
-For any source file being checked or formatted, `swift-format` looks for a
-JSON-formatted file named `.swift-format` in the same directory. If one is
-found, then that file is loaded to determine the tool's configuration. If the
-file is not found, then it looks in the parent directory, and so on.
+For any source file being checked or formatted, `swift-format` looks for 
+configuration files in the same directory, and parent directories. 
 
-If no configuration file is found, a default configuration is used. The
-settings in the default configuration can be viewed by running
-`swift-format dump-configuration`, which will dump it to standard
-output.
+If it finds a file named `.swift-format-ignore`, its contents will determine
+which files in that directory will be ignored by `swift-format`. Currently
+the only supported option is `*`, which ignores all files.
+
+If it finds a JSON-formatted file called `.swift-format`, then that
+file is loaded to determine the tool's configuration. 
+
+If no configuration file is found at any level, a default configuration 
+is used. The settings in the default configuration can be viewed by
+running `swift-format dump-configuration`, which will dump it to 
+standard output.
 
 If the `--configuration <file>` option is passed to `swift-format`, then that
 configuration will be used unconditionally and the file system will not be
-searched.
+searched for `.swift-format` files.
 
 See [Documentation/Configuration.md](Documentation/Configuration.md) for a
 description of the configuration file format and the settings that are
diff --git a/Sources/SwiftFormat/Core/IgnoreFile.swift b/Sources/SwiftFormat/Core/IgnoreFile.swift
new file mode 100644
index 000000000..9bd009c02
--- /dev/null
+++ b/Sources/SwiftFormat/Core/IgnoreFile.swift
@@ -0,0 +1,109 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+
+/// A file that describes which files and directories should be ignored by the formatter.
+/// In the future, this file may contain complex rules for ignoring files, based
+/// on pattern matching file paths.
+///
+/// Currently, the only valid content for an ignore file is a single asterisk "*",
+/// optionally surrounded by whitespace.
+public class IgnoreFile {
+  /// Name of the ignore file to look for.
+  /// The presence of this file in a directory will cause the formatter
+  /// to skip formatting files in that directory and its subdirectories.
+  public static let standardFileName = ".swift-format-ignore"
+
+  /// Errors that can be thrown by the IgnoreFile initializer.
+  public enum Error: Swift.Error {
+    /// Error thrown when initialising with invalid content.
+    case invalidContent
+
+    /// Error thrown when we fail to initialise with the given URL.
+    case invalidFile(URL, Swift.Error)
+  }
+
+  /// Create an instance from a string.
+  /// Returns nil if the content is not valid.
+  public init(_ content: String) throws {
+    guard content.trimmingCharacters(in: .whitespacesAndNewlines) == "*" else {
+      throw Error.invalidContent
+    }
+  }
+
+  /// Create an instance from the contents of the file at the given URL.
+  /// Throws an error if the file content can't be read, or is not valid.
+  public convenience init(contentsOf url: URL) throws {
+    do {
+      try self.init(try String(contentsOf: url, encoding: .utf8))
+    } catch {
+      throw Error.invalidFile(url, error)
+    }
+  }
+
+  /// Create an instance for the given directory, if a valid
+  /// ignore file with the standard name is found in that directory.
+  /// Returns nil if no ignore file is found.
+  /// Throws an error if an invalid ignore file is found.
+  ///
+  /// Note that this initializer does not search parent directories for ignore files.
+  public convenience init?(forDirectory directory: URL) throws {
+    let url = directory.appendingPathComponent(IgnoreFile.standardFileName)
+
+    do {
+      try self.init(contentsOf: url)
+    } catch {
+      if case let Error.invalidFile(_, underlying) = error, (underlying as NSError).domain == NSCocoaErrorDomain,
+        (underlying as NSError).code == NSFileReadNoSuchFileError
+      {
+        return nil
+      }
+      throw error
+    }
+  }
+
+  /// Create an instance to use for the given URL.
+  /// We search for an ignore file starting from the given URL's container,
+  /// and moving up the directory tree, until we reach the root directory.
+  /// Returns nil if no ignore file is found.
+  /// Throws an error if an invalid ignore file is found somewhere
+  /// in the directory tree.
+  ///
+  /// Note that we start the search from the given URL's **container**,
+  /// not the URL itself; the URL passed in is expected to be for a file.
+  /// If you pass a directory URL, the search will not include the contents
+  /// of that directory.
+  public convenience init?(for url: URL) throws {
+    guard !url.isRoot else {
+      return nil
+    }
+
+    var containingDirectory = url.absoluteURL.standardized
+    repeat {
+      containingDirectory.deleteLastPathComponent()
+      let url = containingDirectory.appendingPathComponent(IgnoreFile.standardFileName)
+      if FileManager.default.isReadableFile(atPath: url.path) {
+        try self.init(contentsOf: url)
+        return
+      }
+    } while !containingDirectory.isRoot
+    return nil
+  }
+
+  /// Should the given URL be processed?
+  /// Currently the only valid ignore file content is "*",
+  /// which means that all files should be ignored.
+  func shouldProcess(_ url: URL) -> Bool {
+    return false
+  }
+}
diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift
index b0a8d2f06..4a573dd3c 100644
--- a/Sources/SwiftFormat/Utilities/FileIterator.swift
+++ b/Sources/SwiftFormat/Utilities/FileIterator.swift
@@ -57,7 +57,7 @@ public struct FileIterator: Sequence, IteratorProtocol {
   ///   - workingDirectory: `URL` that indicates the current working directory. Used for testing.
   public init(urls: [URL], followSymlinks: Bool, workingDirectory: URL = URL(fileURLWithPath: ".")) {
     self.workingDirectory = workingDirectory
-    self.urls = urls
+    self.urls = urls.filter(inputShouldBeProcessed(at:))
     self.urlIterator = self.urls.makeIterator()
     self.followSymlinks = followSymlinks
   }
@@ -92,6 +92,20 @@ public struct FileIterator: Sequence, IteratorProtocol {
           fallthrough
 
         case .typeDirectory:
+          do {
+            if let ignoreFile = try IgnoreFile(forDirectory: next), !ignoreFile.shouldProcess(next) {
+              // skip this directory and its subdirectories if it should be ignored
+              continue
+            }
+          } catch IgnoreFile.Error.invalidFile(let url, _) {
+            // we hit an invalid ignore file
+            // we return the path of the ignore file so that we can report an error
+            // and process the directory as normal
+            output = url
+          } catch {
+            // we hit another unexpected error; process the directory as normal
+          }
+
           dirIterator = FileManager.default.enumerator(
             at: next,
             includingPropertiesForKeys: nil,
@@ -179,3 +193,34 @@ private func fileType(at url: URL) -> FileAttributeType? {
   // Linux.
   return try? FileManager.default.attributesOfItem(atPath: url.path)[.type] as? FileAttributeType
 }
+
+/// Returns true if the file should be processed.
+/// Directories are always processed.
+/// For other files, we look for an ignore file in the containing
+/// directory or any of its parents.
+/// If there is no ignore file, we process the file.
+/// If an ignore file is found, we consult it to see if the file should be processed.
+/// An invalid ignore file is treated here as if it does not exist, but
+/// will be reported as an error when we try to process the directory.
+private func inputShouldBeProcessed(at url: URL) -> Bool {
+  guard fileType(at: url) != .typeDirectory else {
+    return true
+  }
+
+  let ignoreFile = try? IgnoreFile(for: url)
+  return ignoreFile?.shouldProcess(url) ?? true
+}
+
+fileprivate extension URL {
+  var isRoot: Bool {
+    #if os(Windows)
+    // FIXME: We should call into Windows' native check to check if this path is a root once https://github.com/swiftlang/swift-foundation/issues/976 is fixed.
+    // https://github.com/swiftlang/swift-format/issues/844
+    return self.pathComponents.count <= 1
+    #else
+    // On Linux, we may end up with an string for the path due to https://github.com/swiftlang/swift-foundation/issues/980
+    // TODO: Remove the check for "" once https://github.com/swiftlang/swift-foundation/issues/980 is fixed.
+    return self.path == "/" || self.path == ""
+    #endif
+  }
+}
\ No newline at end of file
diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift
index a3ea18a4f..4d3c4002a 100644
--- a/Sources/swift-format/Frontend/Frontend.swift
+++ b/Sources/swift-format/Frontend/Frontend.swift
@@ -165,6 +165,13 @@ class Frontend {
   /// Read and prepare the file at the given path for processing, optionally synchronizing
   /// diagnostic output.
   private func openAndPrepareFile(at url: URL) -> FileToProcess? {
+    guard url.lastPathComponent != IgnoreFile.standardFileName else {
+      diagnosticsEngine.emitError(
+        "Invalid ignore file \(url.relativePath): currently the only supported content for ignore files is a single asterisk `*`, which matches all files."
+      )
+      return nil
+    }
+
     guard let sourceFile = try? FileHandle(forReadingFrom: url) else {
       diagnosticsEngine.emitError(
         "Unable to open \(url.relativePath): file is not readable or does not exist"
diff --git a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift
new file mode 100644
index 000000000..2bff90f36
--- /dev/null
+++ b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift
@@ -0,0 +1,126 @@
+@_spi(Internal) import SwiftFormat
+import XCTest
+
+final class IgnoreFileTests: XCTestCase {
+  var testTreeURL: URL?
+
+  /// Description of a file or directory tree to create for testing.
+  enum TestTree {
+    case file(String, String)
+    case directory(String, [TestTree])
+  }
+
+  override func tearDown() {
+    // Clean up any test tree after each test.
+    if let testTreeURL {
+      // try? FileManager.default.removeItem(at: testTreeURL)
+    }
+  }
+
+  /// Make a temporary directory tree for testing.
+  /// Returns the URL of the root directory.
+  /// The tree will be cleaned up after the test.
+  /// If a tree is already set up, it will be cleaned up first.
+  func makeTempTree(_ tree: TestTree) throws -> URL {
+    if let testTreeURL {
+      try? FileManager.default.removeItem(at: testTreeURL)
+    }
+    let tempDir = FileManager.default.temporaryDirectory
+    let tempURL = tempDir.appendingPathComponent(UUID().uuidString)
+    try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true)
+    try writeTree(tree, to: tempURL)
+    testTreeURL = tempURL
+    return tempURL
+  }
+
+  /// Write a file or directory tree to the given root URL.
+  func writeTree(_ tree: TestTree, to root: URL) throws {
+    switch tree {
+    case let .file(name, contents):
+      print("Writing file \(name) to \(root)")
+      try contents.write(to: root.appendingPathComponent(name), atomically: true, encoding: .utf8)
+    case let .directory(name, children):
+      let directory = root.appendingPathComponent(name)
+      try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+      for child in children {
+        try writeTree(child, to: directory)
+      }
+    }
+  }
+
+  func testMissingIgnoreFile() throws {
+    let url = URL(filePath: "/")
+    XCTAssertNil(try IgnoreFile(forDirectory: url))
+    XCTAssertNil(try IgnoreFile(for: url.appending(path: "file.swift")))
+  }
+
+  func testValidIgnoreFile() throws {
+    let url = try makeTempTree(.file(IgnoreFile.standardFileName, "*"))
+    XCTAssertNotNil(try IgnoreFile(forDirectory: url))
+    XCTAssertNotNil(try IgnoreFile(for: url.appending(path: "file.swift")))
+  }
+
+  func testInvalidIgnoreFile() throws {
+    let url = try makeTempTree(.file(IgnoreFile.standardFileName, "this is an invalid pattern"))
+    XCTAssertThrowsError(try IgnoreFile(forDirectory: url))
+    XCTAssertThrowsError(try IgnoreFile(for: url.appending(path: "file.swift")))
+  }
+
+  func testEmptyIgnoreFile() throws {
+    XCTAssertThrowsError(try IgnoreFile(""))
+  }
+
+  func testNestedIgnoreFile() throws {
+    let url = try makeTempTree(.file(IgnoreFile.standardFileName, "*"))
+    let fileInSubdirectory = url.appendingPathComponent("subdirectory").appending(path: "file.swift")
+    XCTAssertNotNil(try IgnoreFile(for: fileInSubdirectory))
+  }
+
+  func testIterateWithIgnoreFile() throws {
+    let url = try makeTempTree(.file(IgnoreFile.standardFileName, "*"))
+    let iterator = FileIterator(urls: [url], followSymlinks: false)
+    let files = Array(iterator)
+    XCTAssertEqual(files.count, 0)
+  }
+
+  func testIterateWithInvalidIgnoreFile() throws {
+    let url = try makeTempTree(.file(IgnoreFile.standardFileName, "this file is invalid"))
+    let iterator = FileIterator(urls: [url], followSymlinks: false)
+    let files = Array(iterator)
+    XCTAssertEqual(files.count, 1)
+    XCTAssertTrue(files.first?.lastPathComponent == IgnoreFile.standardFileName)
+  }
+
+  func testIterateWithNestedIgnoreFile() throws {
+    let url = try makeTempTree(
+      .directory(
+        "Source",
+        [
+          .directory(
+            "Ignored",
+            [
+              .file(IgnoreFile.standardFileName, "*"),
+              .file("file.swift", "contents"),
+            ]
+          ),
+          .directory(
+            "Not Ignored",
+            [
+              .file("file.swift", "contents")
+            ]
+          ),
+        ]
+      )
+    )
+
+    XCTAssertNil(try IgnoreFile(forDirectory: url))
+    XCTAssertNil(try IgnoreFile(for: url.appending(path: "Source/file.swift")))
+    XCTAssertNotNil(try IgnoreFile(for: url.appending(path: "Source/Ignored/file.swift")))
+    let iterator = FileIterator(urls: [url], followSymlinks: false)
+    let files = Array(iterator)
+
+    XCTAssertEqual(files.count, 1)
+    XCTAssertEqual(files.first?.lastPathComponent, "file.swift")
+  }
+
+}
diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore
new file mode 100644
index 000000000..f59ec20aa
--- /dev/null
+++ b/Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore	
@@ -0,0 +1 @@
+*
\ No newline at end of file