Add check_includes_strict_exceptions configuration to .gn

Allows configuring strict deps include check exemptions via a list of
scopes in .gn:

  check_includes_strict_exceptions = [
    {
      includer = "//some/dir/*"
      included = "//some/target"  # optional, omit for global
    },
  ]

This provides fine-grained, target-level exemptions for check_includes_strict
without requiring expensive regex filtering or custom parser formats.

Bug: 556959580
Change-Id: I2306f74835db8299e9e924637ac4bd686a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/27340
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/build/gen.py b/build/gen.py
index 5c49973..57f148e 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -804,6 +804,7 @@
               'src/gn/c_include_iterator.cc',
               'src/gn/c_substitution_type.cc',
               'src/gn/c_tool.cc',
+              'src/gn/check_includes_strict_exceptions.cc',
               'src/gn/command_analyze.cc',
               'src/gn/command_args.cc',
               'src/gn/command_check.cc',
@@ -980,6 +981,7 @@
         'src/gn/binary_target_generator_unittest.cc',
         'src/gn/bundle_data_unittest.cc',
         'src/gn/c_include_iterator_unittest.cc',
+        'src/gn/check_includes_strict_exceptions_unittest.cc',
         'src/gn/command_format_unittest.cc',
         'src/gn/command_suggest_unittest.cc',
         'src/gn/commands_unittest.cc',
diff --git a/docs/reference.md b/docs/reference.md
index 3201930..3b55a39 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -7731,6 +7731,15 @@
       default. They can be checked explicitly by running
       "gn check --check-system" or "gn gen --check=system"
 
+  check_includes_strict_exceptions [optional]
+      A list of scopes containing exceptions for strict dependencies checking.
+      Each scope in this list specifies an exception rule:
+          includer = <from_pattern>
+          included = <to_label> [optional]
+      which waives check_includes_strict violations from targets matching
+      <from_pattern> trying to include headers from <to_label>
+      (or to headers from any target if included is omitted).
+
   exec_script_allowlist [optional]
       A list of .gn/.gni files (not labels) that have permission to call the
       exec_script function. If this list is defined, calls to exec_script will
diff --git a/src/gn/build_settings.h b/src/gn/build_settings.h
index 8330c7d..7feef27 100644
--- a/src/gn/build_settings.h
+++ b/src/gn/build_settings.h
@@ -13,6 +13,7 @@
 
 #include "base/files/file_path.h"
 #include "gn/args.h"
+#include "gn/check_includes_strict_exceptions.h"
 #include "gn/label.h"
 #include "gn/label_pattern.h"
 #include "gn/scope.h"
@@ -165,6 +166,15 @@
     allow_circular_includes_from_allowlist_ = std::move(list);
   }
 
+  const CheckIncludesStrictExceptions& check_includes_strict_exceptions()
+      const {
+    return check_includes_strict_exceptions_;
+  }
+  void set_check_includes_strict_exceptions(
+      CheckIncludesStrictExceptions exceptions) {
+    check_includes_strict_exceptions_ = std::move(exceptions);
+  }
+
   // If true, all generated_file() metadata walks include metadata from
   // validations targets.
   bool experimental_collect_validations_metadata() const {
@@ -191,6 +201,7 @@
 
   SourceFile build_config_file_;
   SourceFile arg_file_template_path_;
+  CheckIncludesStrictExceptions check_includes_strict_exceptions_;
   SourceDir build_dir_;
   Args build_args_;
 
diff --git a/src/gn/check_includes_strict_exceptions.cc b/src/gn/check_includes_strict_exceptions.cc
new file mode 100644
index 0000000..a680838
--- /dev/null
+++ b/src/gn/check_includes_strict_exceptions.cc
@@ -0,0 +1,73 @@
+// Copyright 2026 The GN Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gn/check_includes_strict_exceptions.h"
+
+#include <string_view>
+
+#include "gn/scope.h"
+#include "gn/value.h"
+
+bool CheckIncludesStrictExceptions::CanInclude(const Label& from,
+                                               const Label& to) const {
+  if (LabelPattern::VectorMatches(global_, from))
+    return true;
+
+  auto it = specific_targets_.find(to.GetWithNoToolchain());
+  if (it != specific_targets_.end())
+    return LabelPattern::VectorMatches(it->second, from);
+
+  return false;
+}
+
+Result<CheckIncludesStrictExceptions> CheckIncludesStrictExceptions::FromValue(
+    const Value& value,
+    const SourceDir& current_dir,
+    std::string_view source_root) {
+  RETURN_IF_ERROR(value.VerifyTypeIs(Value::LIST));
+
+  CheckIncludesStrictExceptions exceptions;
+  for (const auto& item : value.list_value()) {
+    RETURN_IF_ERROR(item.VerifyTypeIs(Value::SCOPE));
+
+    const Scope* scope = item.scope_value();
+    const Value* includer = scope->GetValue("includer");
+    if (!includer)
+      return Err(item, "Exception rule requires \"includer\".");
+
+    for (auto [key, val] : scope->GetCurrentScopeValues()) {
+      if (key != "includer" && key != "included") {
+        return Err(
+            *val,
+            "Unknown key in exception rule: \"" + std::string(key) + "\".",
+            "Only \"includer\" and \"included\" are allowed.");
+      }
+    }
+
+    Err err;
+    LabelPattern from =
+        LabelPattern::GetPattern(current_dir, source_root, *includer, &err);
+    RETURN_IF_ERROR(err);
+
+    if (!includer->string_value().starts_with("//"))
+      return Err(*includer,
+                 "Label pattern must be absolute (starting with '//').");
+
+    const Value* included = scope->GetValue("included");
+    if (included) {
+      Label to =
+          Label::Resolve(current_dir, source_root, Label(), *included, &err);
+      RETURN_IF_ERROR(err);
+
+      if (!included->string_value().starts_with("//"))
+        return Err(*included,
+                   "Target label must be absolute (starting with '//').");
+      exceptions.AddException(from, to);
+    } else {
+      exceptions.AddGlobalException(from);
+    }
+  }
+
+  return exceptions;
+}
diff --git a/src/gn/check_includes_strict_exceptions.h b/src/gn/check_includes_strict_exceptions.h
new file mode 100644
index 0000000..c1e4c53
--- /dev/null
+++ b/src/gn/check_includes_strict_exceptions.h
@@ -0,0 +1,43 @@
+// Copyright 2026 The GN Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_CHECK_INCLUDES_STRICT_EXCEPTIONS_H_
+#define TOOLS_GN_CHECK_INCLUDES_STRICT_EXCEPTIONS_H_
+
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+#include "gn/err.h"
+#include "gn/label.h"
+#include "gn/label_pattern.h"
+#include "gn/source_dir.h"
+
+class Value;
+
+// CheckIncludesStrictExceptions stores allowlist rules for
+// check_includes_strict. An includer LabelPattern can be allowed to include
+// from any target (global) or from a specific target Label.
+class CheckIncludesStrictExceptions {
+ public:
+  static Result<CheckIncludesStrictExceptions> FromValue(
+      const Value& value,
+      const SourceDir& current_dir,
+      std::string_view source_root);
+
+  void AddGlobalException(const LabelPattern& from) { global_.push_back(from); }
+  void AddException(const LabelPattern& from, const Label& to) {
+    specific_targets_[to.GetWithNoToolchain()].push_back(from);
+  }
+
+  // Checks whether `from` is allowed to include headers from `to`.
+  bool CanInclude(const Label& from, const Label& to) const;
+
+ private:
+  std::vector<LabelPattern> global_;
+  std::unordered_map<Label, std::vector<LabelPattern>> specific_targets_;
+};
+
+#endif  // TOOLS_GN_CHECK_INCLUDES_STRICT_EXCEPTIONS_H_
diff --git a/src/gn/check_includes_strict_exceptions_unittest.cc b/src/gn/check_includes_strict_exceptions_unittest.cc
new file mode 100644
index 0000000..7794eb7
--- /dev/null
+++ b/src/gn/check_includes_strict_exceptions_unittest.cc
@@ -0,0 +1,92 @@
+// Copyright 2026 The GN Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gn/check_includes_strict_exceptions.h"
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+#include "gn/label.h"
+#include "gn/scope.h"
+#include "gn/source_dir.h"
+#include "gn/value.h"
+#include "util/test/test.h"
+
+TEST(CheckIncludesStrictExceptionsTest, FromValue) {
+  auto parse = [](const Value& val) {
+    return CheckIncludesStrictExceptions::FromValue(val, SourceDir("//"), "");
+  };
+
+  auto make_rule = [](std::string_view from, std::string_view to) {
+    auto scope = std::make_unique<Scope>(static_cast<const Settings*>(nullptr));
+    scope->SetValue("includer", Value(nullptr, std::string(from)), nullptr);
+    if (!to.empty())
+      scope->SetValue("included", Value(nullptr, std::string(to)), nullptr);
+    Value v(nullptr, Value::SCOPE);
+    v.SetScopeValue(std::move(scope));
+    return v;
+  };
+
+  EXPECT_FAILURE(parse(Value(nullptr, "not_a_list")));
+
+  Value bad_list(nullptr, Value::LIST);
+  bad_list.list_value().push_back(Value(nullptr, "not_a_scope"));
+  EXPECT_FAILURE(parse(bad_list));
+
+  Value non_abs(nullptr, Value::LIST);
+  non_abs.list_value().push_back(make_rule("not_abs:a", "//b:b"));
+  EXPECT_FAILURE(parse(non_abs));
+
+  // Specific target exception.
+  Value specific_list(nullptr, Value::LIST);
+  specific_list.list_value().push_back(make_rule("//:includer", "//:included"));
+  auto specific_exceptions = parse(specific_list);
+  ASSERT_SUCCESS(specific_exceptions);
+  EXPECT_TRUE(specific_exceptions->CanInclude(
+      Label(SourceDir("//"), "includer"), Label(SourceDir("//"), "included")));
+  EXPECT_FALSE(specific_exceptions->CanInclude(
+      Label(SourceDir("//"), "includer"), Label(SourceDir("//"), "other")));
+
+  // Global exception (included omitted).
+  Value global_list(nullptr, Value::LIST);
+  global_list.list_value().push_back(make_rule("//:includer", ""));
+  auto global_exceptions = parse(global_list);
+  ASSERT_SUCCESS(global_exceptions);
+  EXPECT_TRUE(
+      global_exceptions->CanInclude(Label(SourceDir("//"), "includer"),
+                                    Label(SourceDir("//any/"), "target")));
+  EXPECT_FALSE(
+      global_exceptions->CanInclude(Label(SourceDir("//other/"), "target"),
+                                    Label(SourceDir("//any/"), "target")));
+}
+
+TEST(CheckIncludesStrictExceptionsTest, CanInclude) {
+  CheckIncludesStrictExceptions exceptions;
+
+  exceptions.AddException(
+      LabelPattern(LabelPattern::RECURSIVE_DIRECTORY,
+                   SourceDir("//includer_recursive/"), "", Label()),
+      Label(SourceDir("//included/"), "target"));
+
+  EXPECT_TRUE(exceptions.CanInclude(
+      Label(SourceDir("//includer_recursive/sub/"), "target"),
+      Label(SourceDir("//included/"), "target")));
+  EXPECT_FALSE(
+      exceptions.CanInclude(Label(SourceDir("//other_dir/"), "target"),
+                            Label(SourceDir("//included/"), "target")));
+  EXPECT_FALSE(exceptions.CanInclude(
+      Label(SourceDir("//includer_recursive/sub/"), "target"),
+      Label(SourceDir("//included/"), "other_target")));
+
+  exceptions.AddGlobalException(LabelPattern(
+      LabelPattern::DIRECTORY, SourceDir("//includer_global/"), "", Label()));
+
+  EXPECT_TRUE(
+      exceptions.CanInclude(Label(SourceDir("//includer_global/"), "foo"),
+                            Label(SourceDir("//any_dir/"), "any_target")));
+  EXPECT_FALSE(
+      exceptions.CanInclude(Label(SourceDir("//other_dir/"), "foo"),
+                            Label(SourceDir("//any_dir/"), "any_target")));
+}
diff --git a/src/gn/command_suggest.cc b/src/gn/command_suggest.cc
index 9f9c7a9..5442eb3 100644
--- a/src/gn/command_suggest.cc
+++ b/src/gn/command_suggest.cc
@@ -824,7 +824,9 @@
       Label label = target->label();
       HeaderChecker::Chain cycle;
       if (cache.GetReachabilityCache(target).SearchForDependencyTo(
-              includer, /*permitted=*/false, &cycle)) {
+              includer,
+              HeaderChecker::ReachabilityCache::DependencyType::kNonPublic,
+              &cycle)) {
         std::reverse(cycle.begin(), cycle.end());
         StartWarning();
         OutputTarget(target);
diff --git a/src/gn/header_checker.cc b/src/gn/header_checker.cc
index 97ea8b9..7f31064 100644
--- a/src/gn/header_checker.cc
+++ b/src/gn/header_checker.cc
@@ -16,6 +16,7 @@
 #include "gn/build_settings.h"
 #include "gn/builder.h"
 #include "gn/c_include_iterator.h"
+#include "gn/check_includes_strict_exceptions.h"
 #include "gn/config.h"
 #include "gn/config_values_extractors.h"
 #include "gn/err.h"
@@ -83,7 +84,8 @@
 // Given a reverse dependency chain where the target chain[0]'s includes are
 // being used by chain[end] and not all deps are public, returns the string
 // describing the error.
-std::string GetDependencyChainPublicError(const HeaderChecker::Chain& chain) {
+std::string GetDependencyChainPublicError(const HeaderChecker::Chain& chain,
+                                          bool strict_exempt) {
   std::string ret =
       "The target:\n  " +
       chain[chain.size() - 1].target->label().GetUserVisibleName(false) +
@@ -99,10 +101,13 @@
   } else {
     // Indirect dependency chain, print the chain.
     const Target* strict_target = nullptr;
-    for (size_t i = 1; i < chain.size() - 1; i++) {
-      if (chain[i].target->check_includes_strict()) {
-        strict_target = chain[i].target;
-        break;
+    if (!strict_exempt) {
+      for (size_t i = 1; i < chain.size() - 1; i++) {
+        if (chain[i - 1].is_public &&
+            chain[i].target->check_includes_strict()) {
+          strict_target = chain[i].target;
+          break;
+        }
       }
     }
 
@@ -249,8 +254,10 @@
     DoWorkChunked(
         pool, targets_to_precompute, 32,
         [this](std::span<const Target* const> chunk) {
-          for (const Target* target : chunk)
-            GetReachabilityCacheForTarget(target).PerformDependencyWalk(true);
+          for (const Target* target : chunk) {
+            GetReachabilityCacheForTarget(target).PerformDependencyWalk(
+                ReachabilityCache::DependencyType::kPermitted);
+          }
         });
   }
 
@@ -427,16 +434,14 @@
   return SourceFile();
 }
 
-void HeaderChecker::ReachabilityCache::PerformDependencyWalk(bool permitted) {
+void HeaderChecker::ReachabilityCache::PerformDependencyWalk(
+    DependencyType type) {
   // Conduct the actual BFS.
-  if (permitted ? permitted_complete_.load(std::memory_order_relaxed)
-                : any_complete_.load(std::memory_order_relaxed)) {
+  auto& walk = walks_[static_cast<size_t>(type)];
+  if (walk.complete.load(std::memory_order_relaxed)) {
     return;
   }
 
-  BreadcrumbTable& breadcrumbs =
-      permitted ? permitted_breadcrumbs_ : any_breadcrumbs_;
-
   // work_queue maintains a queue of targets which need to be considered as part
   // of dependency chain, in the order they were first traversed. Each time a
   // new transitive dependency of source_target_ is discovered for the first
@@ -444,67 +449,62 @@
   // which target it was reached from when first discovered.
   base::queue<const Target*> work_queue;
   work_queue.push(source_target_);
-  breadcrumbs.Insert(source_target_, nullptr, true);
+  walk.breadcrumbs.Insert(source_target_, nullptr, true);
 
   while (!work_queue.empty()) {
     const Target* target = work_queue.front();
     work_queue.pop();
 
-    if (permitted && target != source_target_ &&
+    if (type == DependencyType::kPermitted && target != source_target_ &&
         target->check_includes_strict()) {
       continue;
     }
 
     for (const auto& dep : target->public_deps()) {
-      if (breadcrumbs.Insert(dep.ptr, target, true))
+      if (walk.breadcrumbs.Insert(dep.ptr, target, true))
         work_queue.push(dep.ptr);
     }
 
-    if (!permitted || target == source_target_) {
+    if (type == DependencyType::kNonPublic || target == source_target_) {
       // Consider all dependencies since all target paths are allowed, so add
       // in private ones. Also do this the first time through the loop, since
       // a target can include headers from its direct deps regardless of
       // public/private-ness.
       for (const auto& dep : target->private_deps()) {
-        if (breadcrumbs.Insert(dep.ptr, target, false))
+        if (walk.breadcrumbs.Insert(dep.ptr, target, false))
           work_queue.push(dep.ptr);
       }
     }
   }
-  if (permitted) {
-    permitted_complete_.store(true, std::memory_order_release);
-  } else {
-    any_complete_.store(true, std::memory_order_release);
-  }
+
+  walk.complete.store(true, std::memory_order_release);
 }
 
 bool HeaderChecker::ReachabilityCache::SearchForDependencyTo(
     const Target* search_for,
-    bool permitted,
+    DependencyType type,
     Chain* chain) {
-  if (permitted ? permitted_complete_.load(std::memory_order_acquire)
-                : any_complete_.load(std::memory_order_acquire)) {
-    return SearchBreadcrumbs(search_for, permitted, chain);
+  auto& walk = walks_[static_cast<size_t>(type)];
+  if (walk.complete.load(std::memory_order_acquire)) {
+    return SearchBreadcrumbs(search_for, type, chain);
   }
 
   {
     std::unique_lock<std::shared_mutex> write_lock(lock_);
-    if (!(permitted ? permitted_complete_.load(std::memory_order_relaxed)
-                    : any_complete_.load(std::memory_order_relaxed))) {
-      PerformDependencyWalk(permitted);
+    if (!walk.complete.load(std::memory_order_relaxed)) {
+      PerformDependencyWalk(type);
     }
   }
 
-  return SearchBreadcrumbs(search_for, permitted, chain);
+  return SearchBreadcrumbs(search_for, type, chain);
 }
 
 bool HeaderChecker::ReachabilityCache::SearchBreadcrumbs(
     const Target* search_for,
-    bool permitted,
+    DependencyType type,
     Chain* chain) const {
-  const BreadcrumbTable& breadcrumbs =
-      permitted ? permitted_breadcrumbs_ : any_breadcrumbs_;
-  ChainLink incoming_link = breadcrumbs.GetLink(search_for);
+  const auto& walk = walks_[static_cast<size_t>(type)];
+  ChainLink incoming_link = walk.breadcrumbs.GetLink(search_for);
   if (!incoming_link.target)
     return false;
 
@@ -512,7 +512,7 @@
   chain->clear();
   const Target* cur = search_for;
   while (cur != source_target_) {
-    ChainLink link = breadcrumbs.GetLink(cur);
+    ChainLink link = walk.breadcrumbs.GetLink(cur);
     chain->push_back(ChainLink(cur, link.is_public));
     cur = link.target;
   }
@@ -676,6 +676,18 @@
   //    includes to detect when A implicitly includes C's header. This will not
   //    have the annoying false positive problem, but is complex to write.
 
+  // Some files are declared in multiple targets.
+  // While it does make exemptions a little overly broad, it does become much
+  // simpler this way, and it's pretty rare anyway.
+  bool strict_exempt = false;
+  for (const auto& target : targets) {
+    if (build_settings_->check_includes_strict_exceptions().CanInclude(
+            from_target->label(), target.target->label())) {
+      strict_exempt = true;
+      break;
+    }
+  }
+
   // Fast path: the include is valid if any target containing this header is
   // the includer itself, allowlists the includer, or is reachable through a
   // permitted dependency chain.
@@ -683,7 +695,7 @@
     const Target* to_target = target.target;
     if (to_target == from_target) {
       if (from_target->check_includes_strict() && is_public_header &&
-          !target.is_public) {
+          !target.is_public && !strict_exempt) {
         break;
       }
       return;
@@ -697,12 +709,17 @@
     if (!target.is_public && !FriendMatches(to_target, from_target))
       continue;
 
-    if (!from_target_cache.SearchForDependencyTo(to_target, true, &chain))
+    auto search_type =
+        strict_exempt
+            ? ReachabilityCache::DependencyType::kPermittedIgnoringStrict
+            : ReachabilityCache::DependencyType::kPermitted;
+    if (!from_target_cache.SearchForDependencyTo(to_target, search_type,
+                                                 &chain))
       continue;
     DCHECK(chain.size() >= 2);
 
     if (from_target->check_includes_strict() && is_public_header &&
-        !chain[chain.size() - 2].is_public) {
+        !chain[chain.size() - 2].is_public && !strict_exempt) {
       continue;
     }
     return;
@@ -759,7 +776,7 @@
     } else {
       last_error = Err(CreatePersistentRange(source_file, range),
                        "Can't include this header from here.",
-                       GetDependencyChainPublicError(chain));
+                       GetDependencyChainPublicError(chain, strict_exempt));
     }
   }
 
@@ -800,13 +817,15 @@
   }
 
   // 1. Try permitted dependency.
-  if (from_target_cache.SearchForDependencyTo(search_for, true, chain)) {
+  if (from_target_cache.SearchForDependencyTo(
+          search_for, ReachabilityCache::DependencyType::kPermitted, chain)) {
     *is_permitted = true;
     return true;
   }
 
   // 2. Try any dependency.
-  if (from_target_cache.SearchForDependencyTo(search_for, false, chain)) {
+  if (from_target_cache.SearchForDependencyTo(
+          search_for, ReachabilityCache::DependencyType::kNonPublic, chain)) {
     *is_permitted = false;
     return true;
   }
diff --git a/src/gn/header_checker.h b/src/gn/header_checker.h
index 14f4eaf..9009128 100644
--- a/src/gn/header_checker.h
+++ b/src/gn/header_checker.h
@@ -72,9 +72,6 @@
   // Store the shortest-dependency-path information for all BFS walks starting
   // from a given `search_from` target.
   //
-  // `permitted_breadcrumbs` corresponds to public dependencies only.
-  // `any_breadcrumbs` corresponds to all dependencies.
-  //
   // Each walk type needs only to be performed once, which is recorded by the
   // corresponding completion flag.
   class ReachabilityCache {
@@ -83,6 +80,17 @@
     ReachabilityCache(const ReachabilityCache&) = delete;
     ReachabilityCache& operator=(const ReachabilityCache&) = delete;
 
+    enum class DependencyType : size_t {
+      // Searches across public_deps, stopping at targets for which
+      // check_includes_strict is true.
+      kPermitted,
+      // Searches across public deps.
+      kPermittedIgnoringStrict,
+      // Searches across deps as well as public_deps.
+      kNonPublic,
+    };
+    static constexpr size_t kNumDependencyTypes = 3;
+
     const Target* source_target() const { return source_target_; }
 
     // Returns true if the given `search_for` target is reachable from
@@ -90,16 +98,13 @@
     //
     // If found, the vector given in `chain` will be filled with the reverse
     // dependency chain from the destination target to the source target.
-    //
-    // If `permitted` is true, only permitted (public) dependency paths are
-    // searched.
     bool SearchForDependencyTo(const Target* search_for,
-                               bool permitted,
+                               DependencyType type,
                                Chain* chain);
 
     // Conducts a breadth-first search through the dependency graph to find a
     // shortest chain from source_target_.
-    void PerformDependencyWalk(bool permitted);
+    void PerformDependencyWalk(DependencyType type);
 
    private:
     // Header checking structures ----------------------------------------------
@@ -189,19 +194,19 @@
     // Reconstructs the shortest dependency chain to the given target if it was
     // found during a previous walk of the given type. Returns true on success.
     bool SearchBreadcrumbs(const Target* search_for,
-                           bool permitted,
+                           DependencyType type,
                            Chain* chain) const;
 
     const Target* source_target_;
 
     mutable std::shared_mutex lock_;
-    // Breadcrumbs for the shortest permitted path.
-    BreadcrumbTable permitted_breadcrumbs_;
-    // Breadcrumbs for the shortest path of any type.
-    BreadcrumbTable any_breadcrumbs_;
 
-    std::atomic<bool> permitted_complete_ = false;
-    std::atomic<bool> any_complete_ = false;
+    struct WalkData {
+      BreadcrumbTable breadcrumbs;
+      std::atomic<bool> complete = false;
+    };
+    // Indexed by DependencyType.
+    std::array<WalkData, kNumDependencyTypes> walks_;
   };
 
   // check_generated, if true, will also check generated
@@ -246,6 +251,7 @@
                            CheckIncludesStrictPrivateInPublicHeader);
   FRIEND_TEST_ALL_PREFIXES(HeaderCheckerTest,
                            CheckIncludesStrictSameTargetPrivateHeader);
+  FRIEND_TEST_ALL_PREFIXES(HeaderCheckerTest, CheckIncludesStrictExceptions);
   FRIEND_TEST_ALL_PREFIXES(HeaderCheckerTest, NoCheckGenerated);
 
   ~HeaderChecker();
diff --git a/src/gn/header_checker_unittest.cc b/src/gn/header_checker_unittest.cc
index e575ec3..2d41462 100644
--- a/src/gn/header_checker_unittest.cc
+++ b/src/gn/header_checker_unittest.cc
@@ -9,6 +9,9 @@
 #include <unordered_set>
 #include <vector>
 
+#include "gn/check_includes_strict_exceptions.h"
+#include "gn/input_file.h"
+#include "gn/label_pattern.h"
 #include "gn/scheduler.h"
 #include "gn/substitution_list.h"
 #include "gn/target.h"
@@ -651,3 +654,87 @@
   // Listing another target does not affect A.
   EXPECT_EQ(files_to_check(true, {&b_}), (Files{handwritten, generated}));
 }
+
+TEST_F(HeaderCheckerTest, CheckIncludesStrictExceptions) {
+  InputFile input_file(SourceFile("//a_public.h"));
+  input_file.SetContents(std::string());
+  LocationRange range;
+
+  // Setup: a_ is strict, has private dep on b_, and has private header.
+  a_.public_deps().clear();
+  a_.private_deps().push_back(LabelTargetPair(&b_));
+  a_.private_deps().push_back(LabelTargetPair(&d_));
+
+  SourceFile a_public("//a_public.h");
+  SourceFile a_private("//a_private.h");
+  a_.sources().push_back(a_public);
+  a_.sources().push_back(a_private);
+  a_.public_headers().push_back(a_public);
+  a_.set_all_headers_public(false);
+  a_.set_check_includes_strict(true);
+
+  SourceFile b_public("//b_public.h");
+  b_.sources().push_back(b_public);
+
+  SourceFile c_public("//c_public.h");
+  c_.sources().push_back(c_public);
+  b_.set_check_includes_strict(true);
+
+  SourceFile d_public("//d_public.h");
+  SourceFile d_private("//d_private.h");
+  d_.sources().push_back(d_public);
+  d_.sources().push_back(d_private);
+  d_.public_headers().push_back(d_public);
+  d_.set_all_headers_public(false);
+  d_.set_check_includes_strict(true);
+
+  Err err;
+  d_.OnResolved(&err);
+  ASSERT_SUCCESS(err);
+  b_.OnResolved(&err);
+  ASSERT_SUCCESS(err);
+  a_.OnResolved(&err);
+  ASSERT_SUCCESS(err);
+
+  auto checker = CreateChecker();
+  auto& a_cache = checker->GetReachabilityCacheForTarget(&a_);
+  auto& d_cache = checker->GetReachabilityCacheForTarget(&d_);
+
+  auto check_ok = [&](const scoped_refptr<HeaderChecker>& c,
+                      HeaderChecker::ReachabilityCache& cache,
+                      bool is_public_header, const SourceFile& file) {
+    std::vector<Err> errors;
+    c->CheckInclude(cache, is_public_header, input_file, file, range, &errors);
+    return errors.empty();
+  };
+
+  // Without exceptions, includes fail:
+  EXPECT_FALSE(check_ok(checker, a_cache, true, b_public));
+  EXPECT_FALSE(check_ok(checker, a_cache, true, a_private));
+  EXPECT_FALSE(check_ok(checker, a_cache, false, c_public));
+  EXPECT_FALSE(check_ok(checker, a_cache, true, d_public));
+  EXPECT_FALSE(check_ok(checker, d_cache, true, d_private));
+
+  // Set exceptions on build_settings:
+  CheckIncludesStrictExceptions exceptions;
+  LabelPattern a_dir(LabelPattern::DIRECTORY, SourceDir("//a/"), "", Label());
+  exceptions.AddException(a_dir, b_.label());
+  exceptions.AddException(a_dir, a_.label());
+  exceptions.AddException(a_dir, c_.label());
+  setup_.build_settings()->set_check_includes_strict_exceptions(
+      std::move(exceptions));
+
+  // Exempt includes with exceptions:
+  EXPECT_TRUE(check_ok(checker, a_cache, true, b_public));
+  EXPECT_TRUE(check_ok(checker, a_cache, true, a_private));
+  EXPECT_TRUE(check_ok(checker, a_cache, false, c_public));
+
+  // Non-exempt includes with exceptions:
+  // Inter-target: a_ includes d_public, but //a:* => //d:* is not in
+  // exceptions.
+  EXPECT_FALSE(check_ok(checker, a_cache, true, d_public));
+
+  // Intra-target: d_ includes d_private, but //d:* => //d:* is not in
+  // exceptions.
+  EXPECT_FALSE(check_ok(checker, d_cache, true, d_private));
+}
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index c91fbf1..6abf641 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -19,6 +19,7 @@
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
+#include "gn/check_includes_strict_exceptions.h"
 #include "gn/command_format.h"
 #include "gn/commands.h"
 #include "gn/exec_process.h"
@@ -137,6 +138,15 @@
       default. They can be checked explicitly by running
       "gn check --check-system" or "gn gen --check=system"
 
+  check_includes_strict_exceptions [optional]
+      A list of scopes containing exceptions for strict dependencies checking.
+      Each scope in this list specifies an exception rule:
+          includer = <from_pattern>
+          included = <to_label> [optional]
+      which waives check_includes_strict violations from targets matching
+      <from_pattern> trying to include headers from <to_label>
+      (or to headers from any target if included is omitted).
+
   exec_script_allowlist [optional]
       A list of .gn/.gni files (not labels) that have permission to call the
       exec_script function. If this list is defined, calls to exec_script will
@@ -1240,6 +1250,20 @@
     check_system_includes_ = check_system_includes_value->boolean_value();
   }
 
+  const Value* check_includes_strict_exceptions_value =
+      dotfile_scope_.GetValue("check_includes_strict_exceptions", true);
+  if (check_includes_strict_exceptions_value) {
+    auto result = CheckIncludesStrictExceptions::FromValue(
+        *check_includes_strict_exceptions_value, current_dir,
+        build_settings_.root_path_utf8());
+    if (!result.has_value()) {
+      *err = result.error();
+      return false;
+    }
+    build_settings_.set_check_includes_strict_exceptions(
+        std::move(result.value()));
+  }
+
   // Fill exec_script_allowlist.
   const Value* exec_script_allowlist_value =
       dotfile_scope_.GetValue("exec_script_allowlist", true);