[hooks] Document user-defines in API (#3407)
diff --git a/pkgs/hooks/CHANGELOG.md b/pkgs/hooks/CHANGELOG.md
index f3cf4fe..ebd0ee3 100644
--- a/pkgs/hooks/CHANGELOG.md
+++ b/pkgs/hooks/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.2
+
+- Update documentation for user-defines.
+
 ## 2.0.1
 
 - Updated documentation for hook caching behavior and semi-hermetic environment variables.
diff --git a/pkgs/hooks/README.md b/pkgs/hooks/README.md
index 47bb64d..4305ded 100644
--- a/pkgs/hooks/README.md
+++ b/pkgs/hooks/README.md
@@ -83,6 +83,57 @@
 
 For more information see [dart.dev/tools/hooks](https://dart.dev/tools/hooks).
 
+## User-defines
+
+Because build hooks execute in a semi-hermetic environment where most environment variables are stripped for reproducibility and caching purposes, you should use **user-defines** to pass custom configurations, flags, or paths to your hooks from `pubspec.yaml`.
+
+### 1. Define in `pubspec.yaml`
+In your package (or workspace root `pubspec.yaml` if using workspaces), add the `hooks.user_defines` section:
+
+```yaml
+hooks:
+  user_defines:
+    my_package_name:
+      enable_debug_logging: true
+      custom_asset: assets/data.json
+```
+
+> [!IMPORTANT]  
+> **Workspace Scope:** If a project is set up as a **pub workspace**, the `hooks.user_defines` configuration block must be placed in the **workspace root** `pubspec.yaml` file. Defines inside member package `pubspec.yaml` files are ignored when workspace resolution is active.
+
+### 2. Read in `hook/build.dart` or `hook/link.dart`
+Access the values using `input.userDefines`:
+
+<!-- file://./example/api/config_snippet_6.dart -->
+```dart
+import 'dart:io';
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+  await build(args, (input, output) async {
+    // Access raw user-defines value
+    final debugLogging = input.userDefines['enable_debug_logging'];
+    if (debugLogging is! bool?) {
+      throw const FormatException(
+        'hooks.user_defines.my_package.enable_debug_logging must be a '
+        'boolean (or omitted)',
+      );
+    }
+    if (debugLogging == true) {
+      print('Debug logging is enabled.');
+    }
+
+    // Resolve relative path against pubspec.yaml base path
+    final customAssetUri = input.userDefines.path('custom_asset');
+    if (customAssetUri != null) {
+      final file = File.fromUri(customAssetUri);
+      output.dependencies.add(file.uri); // Declare cache dependency
+      // Use the file...
+    }
+  });
+}
+```
+
 ## Documentation
 
 For detailed documentation on debugging and the configuration schema, see the
diff --git a/pkgs/hooks/example/api/config_snippet_6.dart b/pkgs/hooks/example/api/config_snippet_6.dart
new file mode 100644
index 0000000..93510ea
--- /dev/null
+++ b/pkgs/hooks/example/api/config_snippet_6.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2026, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// dart format width=74
+
+// snippet-start
+import 'dart:io';
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+  await build(args, (input, output) async {
+    // Access raw user-defines value
+    final debugLogging = input.userDefines['enable_debug_logging'];
+    if (debugLogging is! bool?) {
+      throw const FormatException(
+        'hooks.user_defines.my_package.enable_debug_logging must be a '
+        'boolean (or omitted)',
+      );
+    }
+    if (debugLogging == true) {
+      print('Debug logging is enabled.');
+    }
+
+    // Resolve relative path against pubspec.yaml base path
+    final customAssetUri = input.userDefines.path('custom_asset');
+    if (customAssetUri != null) {
+      final file = File.fromUri(customAssetUri);
+      output.dependencies.add(file.uri); // Declare cache dependency
+      // Use the file...
+    }
+  });
+}
+// snippet-end
diff --git a/pkgs/hooks/example/api/config_snippet_7.dart b/pkgs/hooks/example/api/config_snippet_7.dart
new file mode 100644
index 0000000..92b4b27
--- /dev/null
+++ b/pkgs/hooks/example/api/config_snippet_7.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2026, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// dart format width=74
+
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+  await build(args, (input, output) async {
+    // snippet-start
+    final assetsUri = input.userDefines.path('prebuilt_assets_dir');
+    if (assetsUri != null) {
+      output.dependencies.add(assetsUri);
+      // Read assets from the directory...
+    }
+    // snippet-end
+  });
+}
diff --git a/pkgs/hooks/lib/src/api/build_and_link.dart b/pkgs/hooks/lib/src/api/build_and_link.dart
index 5b46e03..3669612 100644
--- a/pkgs/hooks/lib/src/api/build_and_link.dart
+++ b/pkgs/hooks/lib/src/api/build_and_link.dart
@@ -78,6 +78,16 @@
 /// }
 /// ```
 ///
+/// ## User-defines
+///
+/// Build hooks can read custom, package-specific configuration settings passed
+/// by the end-user from the root package `pubspec.yaml` (or the root package
+/// pub workspace `pubspec.yaml` if using a workspace) via the
+/// `input.userDefines` property.
+///
+/// See [HookInput.userDefines] for detailed documentation, configuration
+/// schema, and code snippets.
+///
 /// ## Environment
 ///
 /// Build hooks are executed in a semi-hermetic environment. This means that
@@ -131,7 +141,8 @@
 /// and only if:
 ///
 /// * The input to the hook didn't change (including the configuration fields
-///   in [BuildConfig] and the `user-defines` in the workspace `pubspec.yaml`).
+///   accessed via [BuildInput.config] and the `user-defines` in the workspace
+///   `pubspec.yaml`).
 /// * No environment variables (that are not filtered out) changed.
 /// * None of the files or directories declared in
 ///   [HookOutputBuilder.dependencies] changed.
@@ -148,13 +159,18 @@
 /// [HookOutputBuilder.dependencies] (e.g., via
 /// `output.dependencies.add(uri)`).
 ///
+/// If your hook resolves and reads local files referenced in user-defines (e.g.
+/// using `input.userDefines.path('key')`), you **must** manually register those
+/// files in [HookOutputBuilder.dependencies] to ensure the hook is re-run
+/// when the referenced files' contents change.
+///
 /// ### Cache Isolation
 ///
 /// Outputs are cached in a configuration-specific subdirectory inside
-/// `.dart_tool/hooks_runner/`. This directory is unique per hook and is derived
-/// from the [HookConfig]/[BuildConfig] structure. Therefore, different
-/// configurations (e.g., building for a different target OS or architecture)
-/// do not collide.
+/// `.dart_tool/hooks_runner/`. This directory is unique per hook and is
+/// derived from the configuration fields in [BuildInput.config]. Therefore,
+/// different configurations (e.g., building for a different target OS or
+/// architecture) do not collide.
 ///
 /// The cache is reused for identical configurations across different builds,
 /// even when inputs outside the configuration or environment variables change.
@@ -289,6 +305,16 @@
 /// non-zero exit code on failure. Throwing will lead to an uncaught exception,
 /// causing a non-zero exit code.
 ///
+/// ## Custom Configurations (User-Defines)
+///
+/// Link hooks can read custom, package-specific configuration settings passed
+/// by the end-user from the root package `pubspec.yaml` (or the root package
+/// pub workspace `pubspec.yaml` if using a workspace) via the
+/// `input.userDefines` property.
+///
+/// See [HookInput.userDefines] for detailed documentation, configuration
+/// schema, and code snippets.
+///
 /// ## Environment
 ///
 /// Link hooks are executed in a semi-hermetic environment. This means that
@@ -342,7 +368,8 @@
 /// and only if:
 ///
 /// * The input to the hook didn't change (including the configuration fields
-///   in [LinkConfig] and the `user-defines` in the workspace `pubspec.yaml`).
+///   accessed via [LinkInput.config] and the `user-defines` in the workspace
+///   `pubspec.yaml`).
 /// * No environment variables (that are not filtered out) changed.
 /// * None of the files or directories declared in
 ///   [HookOutputBuilder.dependencies] changed.
@@ -359,13 +386,18 @@
 /// [HookOutputBuilder.dependencies] (e.g., via
 /// `output.dependencies.add(uri)`).
 ///
+/// If your hook resolves and reads local files referenced in user-defines (e.g.
+/// using `input.userDefines.path('key')`), you **must** manually register those
+/// files in [HookOutputBuilder.dependencies] to ensure the hook is re-run
+/// when the referenced files' contents change.
+///
 /// ### Cache Isolation
 ///
 /// Outputs are cached in a configuration-specific subdirectory inside
-/// `.dart_tool/hooks_runner/`. This directory is unique per hook and is derived
-/// from the [HookConfig]/[LinkConfig] structure. Therefore, different
-/// configurations (e.g., building for a different target OS or architecture)
-/// do not collide.
+/// `.dart_tool/hooks_runner/`. This directory is unique per hook and is
+/// derived from the configuration fields in [LinkInput.config]. Therefore,
+/// different configurations (e.g., building for a different target OS or
+/// architecture) do not collide.
 ///
 /// The cache is reused for identical configurations across different builds,
 /// even when inputs outside the configuration or environment variables change.
diff --git a/pkgs/hooks/lib/src/config.dart b/pkgs/hooks/lib/src/config.dart
index e1a5c4a..a11ee3f 100644
--- a/pkgs/hooks/lib/src/config.dart
+++ b/pkgs/hooks/lib/src/config.dart
@@ -106,7 +106,101 @@
   /// The configuration for this hook input.
   HookConfig get config => HookConfig._(this);
 
-  /// The user-defines for this hook input.
+  /// Custom configurations specified in the root package `pubspec.yaml`
+  /// (or root package pub workspace `pubspec.yaml` if using a workspace) under
+  /// the `hooks.user_defines` block.
+  ///
+  /// These are used to pass custom parameters or local file paths to build and
+  /// link hooks from the project's build environment.
+  ///
+  /// ### Workspace Scope
+  /// The SDK hook runner reads user-defines from the root package's
+  /// `pubspec.yaml` (or the root package pub workspace's `pubspec.yaml` if
+  /// using a workspace). As a result, only end-users (the authors of the root
+  /// app/package consuming the dependencies) can configure user-defines.
+  /// Dependencies cannot supply their own default user-defines.
+  ///
+  /// ### Caching
+  /// Hook user-defines are workspace-wide. If any user-defines inside the
+  /// root package `pubspec.yaml` (or the root package pub workspace
+  /// `pubspec.yaml` if using a workspace) change, the hook is re-run.
+  /// However, if the user-defines for the package are identical, the hook is
+  /// not re-run.
+  ///
+  /// ### Supported Types
+  /// Configured values inside `pubspec.yaml` can be any JSON-compatible type,
+  /// such as booleans, strings, numbers, nested maps, or lists:
+  /// ```yaml
+  /// hooks:
+  ///   user_defines:
+  ///     my_package:
+  ///       supported_archs:
+  ///         - arm64
+  ///         - x64
+  /// ```
+  ///
+  /// ### Package Filtering
+  /// User-defines are filtered per package. A hook inside `my_package` can only
+  /// access keys configured under `hooks.user_defines.my_package`. It cannot
+  /// access defines of other packages.
+  ///
+  /// ### Common Use Cases
+  /// End-users can configure user-defines to:
+  /// - Select whether to download a prebuilt binary or build from source:
+  ///   ```yaml
+  ///   hooks:
+  ///     user_defines:
+  ///       my_package:
+  ///         local_build: false
+  ///   ```
+  /// - Enable/disable debug options or configure a custom compiler flag:
+  ///   ```yaml
+  ///   hooks:
+  ///     user_defines:
+  ///       my_package:
+  ///         debug_mode: true
+  ///   ```
+  ///
+  /// ### Example Hook Usage
+  /// In `pubspec.yaml`:
+  /// ```yaml
+  /// hooks:
+  ///   user_defines:
+  ///     my_package:
+  ///       enable_experimental: true
+  ///       custom_lib: assets/libnative.so
+  /// ```
+  ///
+  /// In `hook/build.dart`:
+  /// <!-- file://./../../example/api/config_snippet_6.dart -->
+  /// ```dart
+  /// import 'dart:io';
+  /// import 'package:hooks/hooks.dart';
+  ///
+  /// void main(List<String> args) async {
+  ///   await build(args, (input, output) async {
+  ///     // Access raw user-defines value
+  ///     final debugLogging = input.userDefines['enable_debug_logging'];
+  ///     if (debugLogging is! bool?) {
+  ///       throw const FormatException(
+  ///         'hooks.user_defines.my_package.enable_debug_logging must be a '
+  ///         'boolean (or omitted)',
+  ///       );
+  ///     }
+  ///     if (debugLogging == true) {
+  ///       print('Debug logging is enabled.');
+  ///     }
+  ///
+  ///     // Resolve relative path against pubspec.yaml base path
+  ///     final customAssetUri = input.userDefines.path('custom_asset');
+  ///     if (customAssetUri != null) {
+  ///       final file = File.fromUri(customAssetUri);
+  ///       output.dependencies.add(file.uri); // Declare cache dependency
+  ///       // Use the file...
+  ///     }
+  ///   });
+  /// }
+  /// ```
   HookInputUserDefines get userDefines => HookInputUserDefines._(this);
 }
 
@@ -118,8 +212,20 @@
 
   /// The value for the user-define for [key] for this package.
   ///
-  /// This can be arbitrary JSON/YAML if provided from the SDK from such source.
+  /// This can be arbitrary JSON if provided from the SDK from such source.
   /// If it's provided from command-line arguments, it's likely a string.
+  ///
+  /// For example, if a project's `pubspec.yaml` contains:
+  /// ```yaml
+  /// hooks:
+  ///   user_defines:
+  ///     my_package:
+  ///       enable_experimental_features: true
+  ///       optimization_level: "O3"
+  /// ```
+  /// Then:
+  /// - `input.userDefines['enable_experimental_features']` returns `true`.
+  /// - `input.userDefines['optimization_level']` returns `"O3"`.
   Object? operator [](String key) {
     final syntaxNode = _input._syntax.userDefines;
     if (syntaxNode == null) {
@@ -132,14 +238,37 @@
     return pubspecSource?.defines[key];
   }
 
-  /// The absolute path for user-defines for [key] for this package.key
+  /// Resolves the relative path provided in the user-define for [key] to an
+  /// absolute [Uri] pointing to the file or directory on the host filesystem.
   ///
-  /// The relative path passed as user-define is resolved against the base path.
-  /// For user-defines originating from a JSON/YAML, the base path is this
-  /// JSON/YAML. For user-defines originating from command-line arguments, the
-  /// base path is the working directory of the command-line invocation.
+  /// The relative path is resolved against the directory containing the
+  /// `pubspec.yaml` where the user-define was declared (or the command-line
+  /// working directory if provided via command-line arguments).
   ///
   /// If the user-define is `null` or not a [String], returns `null`.
+  ///
+  /// > If the hook reads the resolved file or directory, the hook author
+  /// > **must** register it as a dependency in
+  /// > [HookOutputBuilder.dependencies] (e.g. using
+  /// > `output.dependencies.add(resolvedUri)`) to ensure the build cache is
+  /// > invalidated and the hook is re-run when the file changes.
+  ///
+  /// For example, if a project's `pubspec.yaml` contains:
+  /// ```yaml
+  /// hooks:
+  ///   user_defines:
+  ///     my_package:
+  ///       prebuilt_assets_dir: assets/prebuilt/
+  /// ```
+  /// The resolved path can be accessed and registered as a dependency:
+  /// <!-- file://./../../example/api/config_snippet_7.dart -->
+  /// ```dart
+  /// final assetsUri = input.userDefines.path('prebuilt_assets_dir');
+  /// if (assetsUri != null) {
+  ///   output.dependencies.add(assetsUri);
+  ///   // Read assets from the directory...
+  /// }
+  /// ```
   Uri? path(String key) {
     final syntaxNode = _input._syntax.userDefines;
     if (syntaxNode == null) {
diff --git a/pkgs/hooks/pubspec.yaml b/pkgs/hooks/pubspec.yaml
index 01e5965..431bb3c 100644
--- a/pkgs/hooks/pubspec.yaml
+++ b/pkgs/hooks/pubspec.yaml
@@ -3,7 +3,7 @@
   A library that contains a Dart API for the JSON-based protocol for
   `hook/build.dart` and `hook/link.dart`.
 
-version: 2.0.1
+version: 2.0.2
 
 repository: https://github.com/dart-lang/native/tree/main/pkgs/hooks