diff --git a/pubspec.yaml b/pubspec.yaml
index 5ad4f638825..40b23f7ec9f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,3 +15,7 @@ workspace:
 dev_dependencies:
   build_runner: ^2.3.3
   flutter_lints: ^5.0.0
+
+dependency_overrides:
+  analyzer: 6.11.0
+  dart_style: 2.3.3
diff --git a/tool/lib/commands/update_licenses.dart b/tool/lib/commands/update_licenses.dart
new file mode 100644
index 00000000000..a2a39886a8d
--- /dev/null
+++ b/tool/lib/commands/update_licenses.dart
@@ -0,0 +1,78 @@
+// Copyright 2024 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:cli_util/cli_logging.dart';
+import 'package:path/path.dart' as p;
+
+import '../license_utils.dart';
+
+const _argConfig = 'config';
+const _argDirectory = 'directory';
+const _dryRun = 'dry-run';
+
+/// This command updates license headers for the configured files.
+///
+/// The config file is a YAML file as defined in [LicenseConfig].
+///
+/// If directory is not set, it will default to the current directory.
+///
+/// When the '--dry-run' flag is passed in, a list of files to update will
+/// be logged, but no files will be modified.
+///
+/// To run this script
+/// `dt update-licenses [--f <config-file>] [--d <directory>] [--dry-run]`
+class UpdateLicensesCommand extends Command {
+  UpdateLicensesCommand() {
+    argParser
+      ..addOption(
+        _argConfig,
+        abbr: 'c',
+        defaultsTo: p.join(Directory.current.path, 'update_licenses.yaml'),
+        help:
+            'The path to the YAML license config file. Defaults to '
+            'update_licenses.yaml',
+      )
+      ..addOption(
+        _argDirectory,
+        defaultsTo: Directory.current.path,
+        abbr: 'd',
+        help: 'Update license headers for files in the directory.',
+      )
+      ..addFlag(
+        _dryRun,
+        negatable: false,
+        defaultsTo: false,
+        help:
+            'If set, log a list of files that require an update, but do not '
+            'modify any files.',
+      );
+  }
+
+  @override
+  String get description => 'Update license headers as configured.';
+
+  @override
+  String get name => 'update-licenses';
+
+  @override
+  Future run() async {
+    final config = LicenseConfig.fromYamlFile(
+      File(argResults![_argConfig] as String),
+    );
+    final directory = Directory(argResults![_argDirectory] as String);
+    final dryRun = argResults![_dryRun] as bool;
+    final log = Logger.standard();
+    final header = LicenseHeader();
+    final results = await header.bulkUpdate(
+      directory: directory,
+      config: config,
+      dryRun: dryRun,
+    );
+    final updatedPaths = results.updatedPaths;
+    final prefix = dryRun ? 'Requires update: ' : 'Updated: ';
+    log.stdout('$prefix ${updatedPaths.join(", ")}');
+  }
+}
diff --git a/tool/lib/devtools_command_runner.dart b/tool/lib/devtools_command_runner.dart
index 35ab59b8495..be532b04b9a 100644
--- a/tool/lib/devtools_command_runner.dart
+++ b/tool/lib/devtools_command_runner.dart
@@ -13,6 +13,7 @@ import 'package:devtools_tool/commands/serve.dart';
 import 'package:devtools_tool/commands/sync.dart';
 import 'package:devtools_tool/commands/tag_version.dart';
 import 'package:devtools_tool/commands/update_flutter_sdk.dart';
+import 'package:devtools_tool/commands/update_licenses.dart';
 import 'package:devtools_tool/commands/update_perfetto.dart';
 import 'package:devtools_tool/model.dart';
 
@@ -47,6 +48,7 @@ class DevToolsCommandRunner extends CommandRunner {
     addCommand(UpdateDartSdkDepsCommand());
     addCommand(UpdateDevToolsVersionCommand());
     addCommand(UpdateFlutterSdkCommand());
+    addCommand(UpdateLicensesCommand());
     addCommand(UpdatePerfettoCommand());
 
     argParser.addFlag(
diff --git a/tool/lib/license_utils.dart b/tool/lib/license_utils.dart
index c171d13bfa9..e8b8f5e8c5e 100644
--- a/tool/lib/license_utils.dart
+++ b/tool/lib/license_utils.dart
@@ -25,27 +25,27 @@ import 'package:yaml/yaml.dart';
 /// ```yaml
 /// # sequence of license text strings that should be matched against at the top of a file and removed. <value>, which normally represents a date, will be stored.
 /// remove_licenses:
-///   - |
-///     // This is some <value1> multiline license
+///   - |-
+///     // This is some <value> multiline license
 ///     // text that should be removed from the file.
-///   - |
-///     /* This is other <value2> multiline license
+///   - |-
+///     /* This is other <value> multiline license
 ///     text that should be removed from the file. */
-///   - |
-///     # This is more <value3> multiline license
+///   - |-
+///     # This is more <value> multiline license
 ///     # text that should be removed from the file.
-///   - |
+///   - |-
 ///     // This is some multiline license text to
 ///     // remove that does not contain a stored value.
-/// # sequence of license text strings that should be added to the top of a file. {value} will be replaced.
+/// # sequence of license text strings that should be added to the top of a file. <value> will be replaced.
 /// add_licenses:
-///   - |
-///     // This is some <value1> multiline license
+///   - |-
+///     // This is some <value> multiline license
 ///     // text that should be added to the file.
-///   - |
-///     # This is other <value3> multiline license
+///   - |-
+///     # This is other <value> multiline license
 ///     # text that should be added to the file.
-///   - |
+///   - |-
 ///     // This is some multiline license text to
 ///     // add that does not contain a stored value.
 /// # defines which files should have license text added or updated.
@@ -121,17 +121,23 @@ class LicenseConfig {
   final YamlMap fileTypes;
 
   /// Returns the list of indices for the given [ext] of [removeLicenses]
-  /// containing the license text to remove.
+  /// containing the license text to remove if they exist or an empty YamlList.
   YamlList getRemoveIndicesForExtension(String ext) {
     final fileType = fileTypes[_removeDotFromExtension(ext)];
-    return fileType['remove'] as YamlList;
+    if (fileType != null) {
+      return fileType['remove'] as YamlList;
+    }
+    return YamlList();
   }
 
   /// Returns the index for the given [ext] of [addLicenses] containing the
-  /// license text to add.
+  /// license text to add if it exists or -1.
   int getAddIndexForExtension(String ext) {
     final fileType = fileTypes[_removeDotFromExtension(ext)];
-    return fileType['add'];
+    if (fileType != null) {
+      return fileType['add'];
+    }
+    return -1;
   }
 
   /// Returns whether the file should be excluded according to the config.
@@ -202,14 +208,14 @@ class LicenseHeader {
         .handleError(
           (e) =>
               throw StateError(
-                'License header expected, but error reading file - $e',
+                'License header expected, but error reading $file - $e',
               ),
         );
     await for (final content in stream) {
-      // Return just the license headers for the simple case with no stored
-      // value requested (i.e. content matches licenseText verbatim)
+      final storedName = _parseStoredName(replacementLicenseText);
       if (content.contains(existingLicenseText)) {
-        final storedName = _parseStoredName(replacementLicenseText);
+        // Return just the license headers for the simple case with no stored
+        // value requested (i.e. content matches licenseText verbatim)
         replacementLicenseText = replacementLicenseText.replaceAll(
           '<$storedName>',
           defaultStoredValue ?? DateTime.now().year.toString(),
@@ -221,17 +227,14 @@ class LicenseHeader {
       }
       // Return a non-empty map for the case where there is a stored value
       // requested (i.e. when there is a '<value>' defined in the license text)
-      final storedName = _parseStoredName(existingLicenseText);
-      if (storedName.isNotEmpty) {
-        return _processHeaders(
-          storedName: storedName,
-          existingLicenseText: existingLicenseText,
-          replacementLicenseText: replacementLicenseText,
-          content: content,
-        );
-      }
+      return _processHeaders(
+        storedName: storedName,
+        existingLicenseText: existingLicenseText,
+        replacementLicenseText: replacementLicenseText,
+        content: content,
+      );
     }
-    throw StateError('License header expected in ${file.path}, but not found!');
+    throw StateError('License header could not be added to ${file.path}');
   }
 
   /// Returns a copy of the given [file] with the [existingHeader] replaced by
@@ -254,6 +257,24 @@ class LicenseHeader {
     return rewrittenFile;
   }
 
+  /// Returns a copy of the given [file] that is missing a license header
+  /// with the [replacementHeader] added to the top.
+  ///
+  /// Reads and writes the entire file contents all at once, so performance may
+  /// degrade for large files.
+  File addLicenseHeader({
+    required File file,
+    required String replacementHeader,
+  }) {
+    final rewrittenFile = File('${file.path}.tmp');
+    final contents = file.readAsStringSync();
+    rewrittenFile.writeAsStringSync(
+      '$replacementHeader${Platform.lineTerminator}$contents',
+      flush: true,
+    );
+    return rewrittenFile;
+  }
+
   /// Bulk update license headers for files in the [directory] as configured
   /// in the [config] and return a processed paths Record containing:
   /// - list of included paths
@@ -274,20 +295,42 @@ class LicenseHeader {
       if (!config.shouldExclude(file)) {
         includedPathsList.add(file.path);
         final extension = p.extension(file.path);
+        final addIndex = config.getAddIndexForExtension(extension);
+        if (addIndex == -1) {
+          // skip if add index doesn't exist for extension
+          continue;
+        }
+        final fileLength = file.lengthSync();
+        const bufferSize = 20;
+        final replacementLicenseText = config.addLicenses[addIndex];
+        final byteCount = min(
+          bufferSize + replacementLicenseText.length,
+          fileLength,
+        );
+        var replacementInfo = await getReplacementInfo(
+          file: file,
+          existingLicenseText: replacementLicenseText,
+          replacementLicenseText: replacementLicenseText,
+          byteCount: byteCount as int,
+        );
+        if (replacementInfo.existingHeader.isNotEmpty &&
+            replacementInfo.replacementHeader.isNotEmpty &&
+            replacementInfo.existingHeader ==
+                replacementInfo.replacementHeader) {
+          // Do nothing if the replacement header is the same as the
+          // existing header
+          continue;
+        }
         final removeIndices = config.getRemoveIndicesForExtension(extension);
         for (final removeIndex in removeIndices) {
           final existingLicenseText = config.removeLicenses[removeIndex];
-          final addIndex = config.getAddIndexForExtension(extension);
-          final replacementLicenseText = config.addLicenses[addIndex];
-          final fileLength = file.lengthSync();
-          const bufferSize = 20;
           // Assume that the license text will be near the start of the file,
           // but add in some buffer.
           final byteCount = min(
             bufferSize + existingLicenseText.length,
             fileLength,
           );
-          final replacementInfo = await getReplacementInfo(
+          replacementInfo = await getReplacementInfo(
             file: file,
             existingLicenseText: existingLicenseText,
             replacementLicenseText: replacementLicenseText,
@@ -295,6 +338,7 @@ class LicenseHeader {
           );
           if (replacementInfo.existingHeader.isNotEmpty &&
               replacementInfo.replacementHeader.isNotEmpty) {
+            // Case 1: Existing header needs to be replaced
             if (dryRun) {
               updatedPathsList.add(file.path);
             } else {
@@ -303,15 +347,27 @@ class LicenseHeader {
                 existingHeader: replacementInfo.existingHeader,
                 replacementHeader: replacementInfo.replacementHeader,
               );
-              if (rewrittenFile.lengthSync() > 0) {
-                file.writeAsStringSync(
-                  rewrittenFile.readAsStringSync(),
-                  mode: FileMode.writeOnly,
-                  flush: true,
-                );
-                updatedPathsList.add(file.path);
-              }
-              rewrittenFile.deleteSync();
+              _updateLicense(rewrittenFile, file, updatedPathsList);
+            }
+          }
+        }
+        if (!updatedPathsList.contains(file.path)) {
+          final licenseHeaders = _processHeaders(
+            storedName: _parseStoredName(replacementLicenseText),
+            existingLicenseText: '',
+            replacementLicenseText: replacementLicenseText,
+            content: '',
+          );
+          if (licenseHeaders.replacementHeader.isNotEmpty) {
+            // Case 2: Missing header needs to be added
+            if (dryRun) {
+              updatedPathsList.add(file.path);
+            } else {
+              final rewrittenFile = addLicenseHeader(
+                file: file,
+                replacementHeader: licenseHeaders.replacementHeader,
+              );
+              _updateLicense(rewrittenFile, file, updatedPathsList);
             }
           }
         }
@@ -320,6 +376,22 @@ class LicenseHeader {
     return (includedPaths: includedPathsList, updatedPaths: updatedPathsList);
   }
 
+  void _updateLicense(
+    File rewrittenFile,
+    File file,
+    List<String> updatedPathsList,
+  ) {
+    if (rewrittenFile.lengthSync() > 0) {
+      file.writeAsStringSync(
+        rewrittenFile.readAsStringSync(),
+        mode: FileMode.writeOnly,
+        flush: true,
+      );
+      updatedPathsList.add(file.path);
+    }
+    rewrittenFile.deleteSync();
+  }
+
   ({String existingHeader, String replacementHeader}) _processHeaders({
     required String storedName,
     required String existingLicenseText,
@@ -356,7 +428,11 @@ class LicenseHeader {
         );
       }
     }
-    return const (existingHeader: '', replacementHeader: '');
+    final defaultReplacementHeader = replacementLicenseText.replaceAll(
+      '<$storedName>',
+      DateTime.now().year.toString(),
+    );
+    return (existingHeader: '', replacementHeader: defaultReplacementHeader);
   }
 
   // TODO(mossmana) Add support for multiple stored names
diff --git a/tool/test/license_utils_test.dart b/tool/test/license_utils_test.dart
index 5cbd21725c9..98b8fea0ee9 100644
--- a/tool/test/license_utils_test.dart
+++ b/tool/test/license_utils_test.dart
@@ -52,6 +52,8 @@ late File testFile9;
 late File testFile10;
 late File excludeFile1;
 late File excludeFile2;
+late File skipFile;
+late File doNothingFile;
 
 void main() {
   group('config file tests', () {
@@ -73,24 +75,20 @@ void main() {
 
       expect(config.removeLicenses.length, equals(4));
 
-      var expectedVal = '''// This is some <value1> multiline license
-// text that should be removed from the file.
-''';
+      var expectedVal = '''// This is some <value> multiline license
+// text that should be removed from the file.''';
       expect(config.removeLicenses[0], equals(expectedVal));
 
-      expectedVal = '''/* This is other <value2> multiline license
-text that should be removed from the file. */
-''';
+      expectedVal = '''/* This is other <value> multiline license
+text that should be removed from the file. */''';
       expect(config.removeLicenses[1], equals(expectedVal));
 
-      expectedVal = '''# This is more <value3> multiline license
-# text that should be removed from the file.
-''';
+      expectedVal = '''# This is more <value> multiline license
+# text that should be removed from the file.''';
       expect(config.removeLicenses[2], equals(expectedVal));
 
       expectedVal = '''// This is some multiline license text to
-// remove that does not contain a stored value.
-''';
+// remove that does not contain a stored value.''';
       expect(config.removeLicenses[3], equals(expectedVal));
     });
 
@@ -99,19 +97,16 @@ text that should be removed from the file. */
 
       expect(config.addLicenses.length, equals(3));
 
-      var expectedVal = '''// This is some <value1> multiline license
-// text that should be added to the file.
-''';
+      var expectedVal = '''// This is some <value> multiline license
+// text that should be added to the file.''';
       expect(config.addLicenses[0], equals(expectedVal));
 
-      expectedVal = '''# This is other <value3> multiline license
-# text that should be added to the file.
-''';
+      expectedVal = '''# This is other <value> multiline license
+# text that should be added to the file.''';
       expect(config.addLicenses[1], equals(expectedVal));
 
       expectedVal = '''// This is some multiline license text to
-// add that does not contain a stored value.
-''';
+// add that does not contain a stored value.''';
       expect(config.addLicenses[2], equals(expectedVal));
     });
 
@@ -179,10 +174,11 @@ text that should be removed from the file. */
       const existingLicenseText = '''// This is some multiline license text to
 // remove that does not contain a stored value.''';
       const replacementLicenseText =
-          '''// This is some <value4> multiline license
+          '''// This is some <value> multiline license
 // text that should be added to the file.''';
 
-      final replacementInfo = await _getTestReplacementInfo(
+      // Contains existing license
+      var replacementInfo = await _getTestReplacementInfo(
         testFile: testFile10,
         existingLicenseText: existingLicenseText,
         replacementLicenseText: replacementLicenseText,
@@ -205,6 +201,20 @@ text that should be removed from the file. */
         replacementInfo.replacementHeader,
         equals(expectedReplacementHeader),
       );
+
+      // Missing existing license
+      replacementInfo = await _getTestReplacementInfo(
+        testFile: testFile10,
+        existingLicenseText: existingLicenseText,
+        replacementLicenseText: replacementLicenseText,
+      );
+
+      expect(replacementInfo.existingHeader, equals(expectedExistingHeader));
+
+      expect(
+        replacementInfo.replacementHeader,
+        equals(expectedReplacementHeader),
+      );
     });
 
     test('stored value preserved in replacement header', () async {
@@ -263,27 +273,6 @@ text that should be added to the file. */''',
       }
     });
 
-    test('update skipped if license text not found', () async {
-      var errorMessage = '';
-      final header = LicenseHeader();
-      try {
-        await header.getReplacementInfo(
-          file: testFile9,
-          existingLicenseText: 'test',
-          replacementLicenseText: 'test',
-          byteCount: 50,
-        );
-      } on StateError catch (e) {
-        errorMessage = e.toString();
-      }
-      expect(
-        errorMessage,
-        equals(
-          'Bad state: License header expected in ${testFile9.path}, but not found!',
-        ),
-      );
-    });
-
     test("update skipped if file can't be read", () async {
       var errorMessage = '';
       final header = LicenseHeader();
@@ -300,7 +289,7 @@ text that should be added to the file. */''',
       expect(
         errorMessage,
         contains(
-          'Bad state: License header expected, but error reading file - PathNotFoundException',
+          'Bad state: License header expected, but error reading File: \'bad.txt\' - PathNotFoundException',
         ),
       );
     });
@@ -337,42 +326,69 @@ text that should be added to the file. */''',
       );
     });
 
+    test('license header can be added on disk', () async {
+      final header = LicenseHeader();
+      const replacementHeader = '''// This is some 2015 multiline license
+// text that should be added to the file.''';
+      final rewrittenFile = header.addLicenseHeader(
+        file: testFile9,
+        replacementHeader: replacementHeader,
+      );
+
+      expect(rewrittenFile.lengthSync(), greaterThan(0));
+
+      final rewrittenContents = rewrittenFile.readAsStringSync();
+      expect(
+        rewrittenContents.substring(0, replacementHeader.length),
+        equals(replacementHeader),
+      );
+    });
+
     test('license headers can be updated in bulk', () async {
       await _setupTestConfigFile();
       final config = LicenseConfig.fromYamlFile(configFile);
       final header = LicenseHeader();
 
-      final contentsBeforeUpdate = testFile1.readAsStringSync();
+      // missing license
+      final contentsBeforeUpdate = testFile9.readAsStringSync();
       final results = await header.bulkUpdate(
         directory: testDirectory,
         config: config,
       );
-      final contentsAfterUpdate = testFile1.readAsStringSync();
+      final contentsAfterUpdate = testFile9.readAsStringSync();
 
       final includedPaths = results.includedPaths;
       expect(includedPaths, isNotNull);
-      expect(includedPaths.length, equals(7));
+      expect(includedPaths.length, equals(9));
       // Order is not guaranteed
       expect(includedPaths.contains(testFile1.path), true);
-      expect(contentsBeforeUpdate, isNot(equals(contentsAfterUpdate)));
       expect(includedPaths.contains(testFile2.path), true);
       expect(includedPaths.contains(testFile3.path), true);
       expect(includedPaths.contains(testFile7.path), true);
       expect(includedPaths.contains(testFile8.path), true);
       expect(includedPaths.contains(testFile9.path), true);
       expect(includedPaths.contains(testFile10.path), true);
+      expect(includedPaths.contains(skipFile.path), true);
+      expect(includedPaths.contains(doNothingFile.path), true);
 
       final updatedPaths = results.updatedPaths;
       expect(updatedPaths, isNotNull);
-      // testFile9 and testFile10 are intentionally misconfigured and so they
-      // won't be updated even though they are on the include list.
-      expect(updatedPaths.length, equals(5));
+      expect(updatedPaths.length, equals(7));
       // Order is not guaranteed
       expect(updatedPaths.contains(testFile1.path), true);
       expect(updatedPaths.contains(testFile2.path), true);
       expect(updatedPaths.contains(testFile3.path), true);
       expect(updatedPaths.contains(testFile7.path), true);
       expect(updatedPaths.contains(testFile8.path), true);
+      expect(updatedPaths.contains(testFile9.path), true);
+      expect(updatedPaths.contains(testFile10.path), true);
+      expect(updatedPaths.contains(skipFile.path), false);
+      expect(updatedPaths.contains(doNothingFile.path), false);
+
+      expect(contentsBeforeUpdate, isNot(equals(contentsAfterUpdate)));
+      // There is an extremely rare failure case on the year boundary.
+      // TODO(mossmana): Handle running test on Dec 31 - Jan 1.
+      expect(contentsAfterUpdate, contains(DateTime.now().year.toString()));
     });
 
     test('license headers bulk update can be dry run', () async {
@@ -390,7 +406,7 @@ text that should be added to the file. */''',
 
       final updatedPaths = results.updatedPaths;
       expect(updatedPaths, isNotNull);
-      expect(updatedPaths.length, equals(5));
+      expect(updatedPaths.length, equals(7));
       expect(updatedPaths.contains(testFile1.path), true);
       expect(contentsBeforeUpdate, equals(contentsAfterUpdate));
     });
@@ -483,27 +499,34 @@ Future<void> _setupTestConfigFile() async {
   final contents = '''---
 # sequence of license text strings that should be matched against at the top of a file and removed. <value>, which normally represents a date, will be stored.
 remove_licenses:
-  - |
-    // This is some <value1> multiline license
+  #0
+  - |-
+    // This is some <value> multiline license
     // text that should be removed from the file.
-  - |
-    /* This is other <value2> multiline license
+  #1
+  - |-
+    /* This is other <value> multiline license
     text that should be removed from the file. */
-  - |
-    # This is more <value3> multiline license
+  #2
+  - |-
+    # This is more <value> multiline license
     # text that should be removed from the file.
-  - |
+  #3
+  - |-
     // This is some multiline license text to
     // remove that does not contain a stored value.
-# sequence of license text strings that should be added to the top of a file. {value} will be replaced.
-add_licenses: 
-  - |
-    // This is some <value1> multiline license
+# sequence of license text strings that should be added to the top of a file. <value> will be replaced.
+add_licenses:
+  #0
+  - |-
+    // This is some <value> multiline license
     // text that should be added to the file.
-  - |
-    # This is other <value3> multiline license
+  #1
+  - |-
+    # This is other <value> multiline license
     # text that should be added to the file.
-  - |
+  #2
+  - |-
     // This is some multiline license text to
     // add that does not contain a stored value.
 # defines which files should have license text added or updated.
@@ -530,6 +553,10 @@ update_paths:
     ext2:
       remove:
         - 2
+      add: 1
+    ext3:
+      remove:
+        - 3
       add: 1''';
 
   configFile.writeAsStringSync(contents, flush: true);
@@ -539,6 +566,7 @@ update_paths:
 /// repo_root/
 ///    test1.ext1
 ///    test2.ext2
+///    test.skip
 ///    .hidden/
 ///       test3.ext1
 ///    sub_dir1/
@@ -573,6 +601,18 @@ Future<void> _setupTestDirectoryStructure() async {
     ..createSync(recursive: true);
   testFile2.writeAsStringSync(licenseText3 + extraText, flush: true);
 
+  final licenseText = '''
+# This is other 2001 multiline license
+# text that should be added to the file.
+''';
+  doNothingFile = File(p.join(repoRoot.path, 'doNothingFile.ext3'))
+    ..createSync(recursive: true);
+  doNothingFile.writeAsStringSync(licenseText + extraText, flush: true);
+
+  skipFile = File(p.join(repoRoot.path, 'test.skip'))
+    ..createSync(recursive: true);
+  skipFile.writeAsStringSync(extraText, flush: true);
+
   // Setup /repo_root/.hidden directory structure
   Directory(p.join(repoRoot.path, '.hidden')).createSync(recursive: true);
 
@@ -624,6 +664,7 @@ Future<void> _setupTestDirectoryStructure() async {
     p.join(repoRoot.path, 'sub_dir2', 'sub_dir4'),
   ).createSync(recursive: true);
 
+  // Missing license header
   testFile9 = File(p.join(repoRoot.path, 'sub_dir2', 'sub_dir4', 'test9.ext1'))
     ..createSync(recursive: true);
   testFile9.writeAsStringSync(extraText, flush: true);
@@ -633,6 +674,7 @@ Future<void> _setupTestDirectoryStructure() async {
     p.join(repoRoot.path, 'sub_dir2', 'sub_dir4', 'sub_dir5'),
   ).createSync(recursive: true);
 
+  // Will be treated like a missing license header since not configured
   testFile10 = File(
     p.join(repoRoot.path, 'sub_dir2', 'sub_dir4', 'sub_dir5', 'test10.ext2'),
   )..createSync(recursive: true);
diff --git a/update_licenses.yaml b/update_licenses.yaml
new file mode 100644
index 00000000000..e151e6d8be9
--- /dev/null
+++ b/update_licenses.yaml
@@ -0,0 +1,159 @@
+# Copyright 2025 The Flutter Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+---
+remove_licenses:
+  #0
+  - |-
+    // Copyright <value> The Chromium Authors. All rights reserved.
+    // Use of this source code is governed by a BSD-style license that can be
+    // found in the LICENSE file.
+  #1
+  - |-
+    /**
+    Copyright <value> The Chromium Authors. All rights reserved.
+    Use of this source code is governed by a BSD-style license that can be
+    found in the LICENSE file.
+    **/
+  #2
+  - |-
+    #!/bin/bash
+    #
+    # Copyright <value> The Chromium Authors. All rights reserved.
+    # Use of this source code is governed by a BSD-style license that can be
+    # found in the LICENSE file.
+  #3
+  - |-
+    <!--
+    Copyright <value> The Chromium Authors. All rights reserved.
+    Use of this source code is governed by a BSD-style license that can be
+    found in the LICENSE file.
+    -->
+  #4
+  - |-
+    REM Copyright <value> The Chromium Authors. All rights reserved.
+    REM Use of this source code is governed by a BSD-style license that can be
+    REM found in the LICENSE file.
+  #5
+  - |-
+    // Copyright <value> The Chromium Authors. All rights reserved.
+    // Use of this source code is governed by a BSD-style license that can be
+    // found in the LICENSE file
+  #6
+  - |-
+    // Copyright <value> The Chromium Authors. All rights reserved.
+    // Use of this source code is governed by a BSD-style license that can be found
+    // in the LICENSE file.
+add_licenses:
+  #0 
+  - |-
+    // Copyright <value> The Flutter Authors
+    // Use of this source code is governed by a BSD-style license that can be
+    // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+  #1
+  - |-
+    /**
+    Copyright <value> The Flutter Authors
+    Use of this source code is governed by a BSD-style license that can be
+    found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+    **/
+  #2
+  - |-
+    #!/bin/bash
+    #
+    # Copyright <value> The Flutter Authors
+    # Use of this source code is governed by a BSD-style license that can be
+    # found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+  #3
+  - |-
+    <!--
+    Copyright <value> The Flutter Authors
+    Use of this source code is governed by a BSD-style license that can be
+    found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+    -->
+  #4
+  - |-
+    REM Copyright <value> The Flutter Authors
+    REM Use of this source code is governed by a BSD-style license that can be
+    REM found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+update_paths:
+  include:
+    - /Users/mossman/Development/devtools
+  exclude:
+    - /Users/mossman/Development/devtools/third_party
+    - /Users/mossman/Development/devtools/packages/devtools_app/build
+    - /Users/mossman/Development/devtools/tool/flutter-sdk
+    - /Users/mossman/Development/devtools/.github
+    - /Users/mossman/Development/devtools/.vscode
+    - /Users/mossman/Development/devtools/packages/devtools_app/lib/src/shared/http/_http_date.dart
+    - /Users/mossman/Development/devtools/packages/devtools_app/lib/src/shared/http/_http_exception.dart
+  file_types:
+    bat:
+      remove:
+          - 4
+      add: 4
+    cc:
+      remove:
+          - 0
+          - 1
+      add: 0
+    cpp:
+      remove:
+          - 0
+          - 1
+      add: 0
+    css:
+      remove:
+          - 1
+      add: 1
+    dart:
+      remove:
+          - 0
+          - 1
+          - 5
+          - 6
+      add: 0
+    gradle:
+      remove:
+          - 0
+          - 1
+      add: 0
+    h:
+      remove:
+          - 0
+          - 1
+      add: 0
+    html:
+      remove:
+          - 3
+      add: 3
+    java:
+      remove:
+          - 0
+          - 1
+      add: 0
+    md:
+      remove:
+          - 3
+      add: 3
+    sh:
+      remove:
+          - 2
+      add: 2
+    swift:
+      remove:
+          - 0
+          - 1
+      add: 0
+    xml:
+      remove:
+          - 3
+      add: 3
+    yaml:
+      remove:
+          - 2
+      add: 2
+    yml:
+      remove:
+          - 2
+      add: 2