Redact option headers from BEP command lines.
Ensures that options like --bes_header, --remote_header, and
--remote_{cache,exec,downloader}_header
are redacted from the unstructured command line reported
to the Build Event Protocol (BEP) for metadata handling.
Contributes to https://github.com/bazelbuild/bazel/discussions/20126.
PiperOrigin-RevId: 954562702
Change-Id: Iac335b38e7a75f63013ca743f8f59c42c71edf8d
diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java b/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java
index 7343ffa..2ed3605 100644
--- a/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java
@@ -72,6 +72,7 @@
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"Specifies the file to get authentication credentials from. See "
+ "https://cloud.google.com/docs/authentication for details.")
@@ -107,6 +108,7 @@
converter = EmptyToNullStringConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"Specify the TLS client key to use; you also need to provide a client certificate to "
+ "enable client authentication. An empty value resets the flag to its default.")
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventservice/BuildEventServiceOptions.java b/src/main/java/com/google/devtools/build/lib/buildeventservice/BuildEventServiceOptions.java
index e4ac866..fc6ec33 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventservice/BuildEventServiceOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventservice/BuildEventServiceOptions.java
@@ -20,6 +20,7 @@
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsClass;
import java.time.Duration;
@@ -68,6 +69,7 @@
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"""
Specify a header in `NAME=VALUE` form that will be included in BES requests.
diff --git a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
index dd0eb6d..02af142 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
@@ -200,6 +200,7 @@
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.REMOTE,
effectTags = {OptionEffectTag.UNKNOWN},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"Specify a header that will be included in requests: --remote_header=Name=Value. "
+ "Multiple headers can be passed by specifying the flag multiple times. Multiple "
@@ -215,6 +216,7 @@
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.REMOTE,
effectTags = {OptionEffectTag.UNKNOWN},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"Specify a header that will be included in cache requests: "
+ "--remote_cache_header=Name=Value. "
@@ -231,6 +233,7 @@
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.REMOTE,
effectTags = {OptionEffectTag.UNKNOWN},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"Specify a header that will be included in execution requests: "
+ "--remote_exec_header=Name=Value. "
@@ -247,6 +250,7 @@
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.REMOTE,
effectTags = {OptionEffectTag.UNKNOWN},
+ metadataTags = {OptionMetadataTag.FULLY_REDACTED_IN_LOGS},
help =
"Specify a header that will be included in remote downloader requests: "
+ "--remote_downloader_header=Name=Value. "
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BUILD b/src/main/java/com/google/devtools/build/lib/runtime/BUILD
index 1744d44..cc651e3 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BUILD
@@ -26,6 +26,7 @@
java_library(
name = "safe_request_logging",
srcs = ["SafeRequestLogging.java"],
+ deps = ["//third_party:guava"],
)
java_library(
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
index d0156aa..b2d06f6 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
@@ -786,7 +786,8 @@
unstructuredServerCommandLineEvent =
OriginalUnstructuredCommandLineEvent.REDACTED_UNSTRUCTURED_COMMAND_LINE_EVENT;
} else {
- unstructuredServerCommandLineEvent = new OriginalUnstructuredCommandLineEvent(args);
+ unstructuredServerCommandLineEvent =
+ new OriginalUnstructuredCommandLineEvent(SafeRequestLogging.redactArguments(args));
}
env.getEventBus().post(unstructuredServerCommandLineEvent);
env.getEventBus().post(originalCommandLineEvent);
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandLineEvent.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandLineEvent.java
index d692a21..92234a9 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/CommandLineEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandLineEvent.java
@@ -136,12 +136,25 @@
List<ParsedOptionDescription> parsedOptionDescriptions) {
List<Option> options = new ArrayList<>();
for (ParsedOptionDescription parsedOption : parsedOptionDescriptions) {
- options.add(
- createOption(
- parsedOption.getOptionDefinition(),
- parsedOption.getSource(),
- parsedOption.getCommandLineForm(),
- parsedOption.getUnconvertedValue()));
+ if (parsedOption.isHidden()) {
+ continue;
+ }
+ if (parsedOption.isFullyRedactedInLogs()) {
+ String optionName = parsedOption.getOptionDefinition().getOptionName();
+ options.add(
+ createOption(
+ parsedOption.getOptionDefinition(),
+ parsedOption.getSource(),
+ "--" + optionName + "=<REDACTED>",
+ "<REDACTED>"));
+ } else {
+ options.add(
+ createOption(
+ parsedOption.getOptionDefinition(),
+ parsedOption.getSource(),
+ parsedOption.getCommandLineForm(),
+ parsedOption.getUnconvertedValue()));
+ }
}
return options;
}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/SafeRequestLogging.java b/src/main/java/com/google/devtools/build/lib/runtime/SafeRequestLogging.java
index 8819038..ad596ea 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/SafeRequestLogging.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/SafeRequestLogging.java
@@ -14,6 +14,10 @@
package com.google.devtools.build.lib.runtime;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -24,6 +28,46 @@
Pattern.compile(
"--client_env=([^=]*(?:auth|pass|cookie|token|api_key)[^=]*)=", Pattern.CASE_INSENSITIVE);
+ private static final ImmutableSet<String> CREDENTIAL_OPTION_NAMES =
+ ImmutableSet.of(
+ "--bes_header",
+ "--remote_header",
+ "--remote_cache_header",
+ "--remote_exec_header",
+ "--remote_downloader_header",
+ "--tls_client_key",
+ "--google_credentials");
+
+ private static final Pattern CREDENTIAL_OPTIONS =
+ Pattern.compile(
+ "--("
+ + CREDENTIAL_OPTION_NAMES.stream().map(s -> s.substring(2)).collect(joining("|"))
+ + ")=",
+ Pattern.CASE_INSENSITIVE);
+
+ public static ImmutableList<String> redactArguments(List<String> args) {
+ ImmutableList.Builder<String> redactedArgs = ImmutableList.builder();
+ Matcher credMatcher = CREDENTIAL_OPTIONS.matcher("");
+ boolean redactNext = false;
+ for (String arg : args) {
+ if (redactNext) {
+ redactedArgs.add("<REDACTED>");
+ redactNext = false;
+ continue;
+ }
+ credMatcher.reset(arg);
+ if (credMatcher.lookingAt()) {
+ redactedArgs.add(credMatcher.group() + "<REDACTED>");
+ } else if (CREDENTIAL_OPTION_NAMES.contains(arg)) {
+ redactedArgs.add(arg);
+ redactNext = true;
+ } else {
+ redactedArgs.add(arg);
+ }
+ }
+ return redactedArgs.build();
+ }
+
private SafeRequestLogging() {}
/**
diff --git a/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java b/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java
index e17fabb..100ae96 100644
--- a/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java
@@ -42,7 +42,14 @@
if (result.length() != 0) {
result.append(' ');
}
- result.append(option.getCanonicalFormWithValueEscaper(ShellEscaper::escapeString));
+ if (option.isFullyRedactedInLogs()) {
+ result
+ .append("--")
+ .append(option.getOptionDefinition().getOptionName())
+ .append("=<REDACTED>");
+ } else {
+ result.append(option.getCanonicalFormWithValueEscaper(ShellEscaper::escapeString));
+ }
}
return result.toString();
}
@@ -65,7 +72,11 @@
if (option.isHidden()) {
continue;
}
- builder.add(option.getCanonicalForm());
+ if (option.isFullyRedactedInLogs()) {
+ builder.add("--" + option.getOptionDefinition().getOptionName() + "=<REDACTED>");
+ } else {
+ builder.add(option.getCanonicalForm());
+ }
}
return builder.build();
}
diff --git a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
index 626f71c..d5f1dea 100644
--- a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
+++ b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
@@ -189,7 +189,10 @@
"This option isn't even a option, and should not be logged.")
.put(
OptionMetadataTag.NON_CONFIGURABLE,
- "This option cannot be changed in a transition or be used in a select() statement.");
+ "This option cannot be changed in a transition or be used in a select() statement.")
+ .put(
+ OptionMetadataTag.FULLY_REDACTED_IN_LOGS,
+ "This option may contain sensitive credentials and should be redacted in logs.");
return effectTagDescriptionBuilder.build();
}
}
diff --git a/src/main/java/com/google/devtools/common/options/OptionMetadataTag.java b/src/main/java/com/google/devtools/common/options/OptionMetadataTag.java
index 45f7dac..8f10395 100644
--- a/src/main/java/com/google/devtools/common/options/OptionMetadataTag.java
+++ b/src/main/java/com/google/devtools/common/options/OptionMetadataTag.java
@@ -68,7 +68,13 @@
* Options which are NON_CONFIGURABLE cannot be changed in (non-exec) Starlark transitions and
* cannot be used in select().
*/
- NON_CONFIGURABLE(8);
+ NON_CONFIGURABLE(8),
+
+ /**
+ * Options which are FULLY_REDACTED_IN_LOGS contain sensitive credentials and should be redacted
+ * in logs.
+ */
+ FULLY_REDACTED_IN_LOGS(9);
private final int value;
diff --git a/src/main/java/com/google/devtools/common/options/ParsedOptionDescription.java b/src/main/java/com/google/devtools/common/options/ParsedOptionDescription.java
index 5af7ac4..765f698 100644
--- a/src/main/java/com/google/devtools/common/options/ParsedOptionDescription.java
+++ b/src/main/java/com/google/devtools/common/options/ParsedOptionDescription.java
@@ -162,6 +162,10 @@
return tags.contains(OptionMetadataTag.HIDDEN) || tags.contains(OptionMetadataTag.INTERNAL);
}
+ public boolean isFullyRedactedInLogs() {
+ return metadataTags().contains(OptionMetadataTag.FULLY_REDACTED_IN_LOGS);
+ }
+
@Nullable
public String getUnconvertedValue() {
return unconvertedValue;
diff --git a/src/main/protobuf/option_filters.proto b/src/main/protobuf/option_filters.proto
index bfbe840..ba00dfa 100644
--- a/src/main/protobuf/option_filters.proto
+++ b/src/main/protobuf/option_filters.proto
@@ -60,4 +60,5 @@
reserved "IMMUTABLE";
reserved 7;
NON_CONFIGURABLE = 8;
+ FULLY_REDACTED_IN_LOGS = 9;
}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BUILD b/src/test/java/com/google/devtools/build/lib/runtime/BUILD
index cf9a211..da89842 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BUILD
@@ -89,6 +89,7 @@
"//src/main/java/com/google/devtools/build/lib/query2/engine",
"//src/main/java/com/google/devtools/build/lib/query2/query/output",
"//src/main/java/com/google/devtools/build/lib/remote:store",
+ "//src/main/java/com/google/devtools/build/lib/remote/options",
"//src/main/java/com/google/devtools/build/lib/runtime:after_command_event",
"//src/main/java/com/google/devtools/build/lib/runtime:blaze_command_cluster",
"//src/main/java/com/google/devtools/build/lib/runtime:blaze_command_result",
@@ -114,6 +115,7 @@
"//src/main/java/com/google/devtools/build/lib/runtime:location_printer",
"//src/main/java/com/google/devtools/build/lib/runtime:memory_pressure",
"//src/main/java/com/google/devtools/build/lib/runtime:memory_pressure_listener",
+ "//src/main/java/com/google/devtools/build/lib/runtime:original_unstructured_command_line_event",
"//src/main/java/com/google/devtools/build/lib/runtime:process_wrapper",
"//src/main/java/com/google/devtools/build/lib/runtime:quiescing_executors_impl",
"//src/main/java/com/google/devtools/build/lib/runtime:rc_chunk_of_args",
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BepCommandLineLeakTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BepCommandLineLeakTest.java
new file mode 100644
index 0000000..7a85691
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BepCommandLineLeakTest.java
@@ -0,0 +1,114 @@
+// Copyright 2025 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.devtools.build.lib.buildeventservice.BuildEventServiceOptions;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent;
+import com.google.devtools.build.lib.remote.options.RemoteOptions;
+import com.google.devtools.build.lib.runtime.CommandLineEvent.CanonicalCommandLineEvent;
+import com.google.devtools.build.lib.runtime.CommandLineEvent.OriginalCommandLineEvent;
+import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.CommandLine;
+import com.google.devtools.common.options.OptionPriority.PriorityCategory;
+import com.google.devtools.common.options.OptionsParser;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test that options with FULLY_REDACTED_IN_LOGS are redacted in BEP command line events. */
+@RunWith(JUnit4.class)
+public final class BepCommandLineLeakTest {
+
+ private static final String BES_BEARER = "ya29.BES_BEARER_TOKEN_xxxxxxxxxxxxxxxx";
+ private static final String BES_HEADER_ARG = "--bes_header=Authorization=Bearer " + BES_BEARER;
+
+ private static final String REMOTE_BEARER = "REMOTE_EXEC_TOKEN_zzzzzzzzzzzzzzzz";
+ private static final String REMOTE_HEADER_ARG =
+ "--remote_header=Authorization=Bearer " + REMOTE_BEARER;
+
+ private static OptionsParser newCommandParser() throws Exception {
+ OptionsParser parser =
+ OptionsParser.builder()
+ .optionsClasses(BuildEventServiceOptions.class, RemoteOptions.class)
+ .build();
+ parser.parse(
+ PriorityCategory.COMMAND_LINE,
+ /* source= */ "command line options",
+ ImmutableList.of(BES_HEADER_ARG, REMOTE_HEADER_ARG));
+ return parser;
+ }
+
+ @Test
+ public void structuredCommandLine_redactsHeaders() throws Exception {
+ OptionsParser cmd = newCommandParser();
+ OptionsParser startup =
+ OptionsParser.builder().optionsClasses(BlazeServerStartupOptions.class).build();
+
+ CommandLine original =
+ new OriginalCommandLineEvent(
+ "testblaze",
+ startup,
+ "build",
+ ImmutableList.of("//some:target"),
+ false,
+ cmd.asListOfExplicitOptions(),
+ ImmutableSortedMap.of(),
+ ImmutableSet.of(),
+ Optional.of(ImmutableList.of()))
+ .asStreamProto(null)
+ .getStructuredCommandLine();
+
+ CommandLine canonical =
+ new CanonicalCommandLineEvent(
+ "testblaze",
+ startup,
+ "build",
+ ImmutableList.of("//some:target"),
+ false,
+ ImmutableSortedMap.of(),
+ ImmutableSortedMap.of(),
+ ImmutableSet.of(),
+ cmd.asListOfCanonicalOptions(),
+ false)
+ .asStreamProto(null)
+ .getStructuredCommandLine();
+
+ for (CommandLine line : ImmutableList.of(original, canonical)) {
+ assertThat(line.toString()).doesNotContain(BES_BEARER);
+ assertThat(line.toString()).doesNotContain(REMOTE_BEARER);
+ assertThat(line.toString()).contains("--bes_header=<REDACTED>");
+ assertThat(line.toString()).contains("--remote_header=<REDACTED>");
+ }
+ }
+
+ @Test
+ public void unstructuredCommandLine_redactsHeaders() {
+ ImmutableList<String> rawArgs =
+ ImmutableList.of("build", BES_HEADER_ARG, REMOTE_HEADER_ARG, "//some:target");
+ BuildEvent proto =
+ new OriginalUnstructuredCommandLineEvent(SafeRequestLogging.redactArguments(rawArgs))
+ .asStreamProto(null);
+
+ String unstructured = proto.getUnstructuredCommandLine().toString();
+ assertThat(unstructured).doesNotContain(BES_BEARER);
+ assertThat(unstructured).doesNotContain(REMOTE_BEARER);
+ assertThat(unstructured).contains("--bes_header=<REDACTED>");
+ assertThat(unstructured).contains("--remote_header=<REDACTED>");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/SafeRequestLoggingTest.java b/src/test/java/com/google/devtools/build/lib/runtime/SafeRequestLoggingTest.java
index 8417b92..6dfd321 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/SafeRequestLoggingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/SafeRequestLoggingTest.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
-import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -97,17 +96,39 @@
@Test
public void testGetRequestLogForStandardCommandLine() {
- List<String> complexCommandLine = ImmutableList.of(
- "blaze",
- "build",
- "--client_env=FOO=BAR",
- "--client_env=FOOPASS=mypassword",
- "--package_path=./MY_PASSWORD/foo",
- "--client_env=SOMEAuThCode=something");
+ ImmutableList<String> complexCommandLine =
+ ImmutableList.of(
+ "blaze",
+ "build",
+ "--client_env=FOO=BAR",
+ "--client_env=FOOPASS=mypassword",
+ "--package_path=./MY_PASSWORD/foo",
+ "--client_env=SOMEAuThCode=something");
assertThat(SafeRequestLogging.getRequestLogString(complexCommandLine))
.isEqualTo(
"[blaze, build, --client_env=FOO=BAR, --client_env=FOOPASS=__private_value_removed__, "
+ "--package_path=./MY_PASSWORD/foo, "
+ "--client_env=SOMEAuThCode=__private_value_removed__]");
}
+
+ @Test
+ public void testRedactArgumentsRedactsCredentials() {
+ ImmutableList<String> args =
+ ImmutableList.of(
+ "blaze",
+ "build",
+ "--bes_header=Auth=123",
+ "--remote_header",
+ "Auth=456",
+ "--some_other_flag");
+ assertThat(SafeRequestLogging.redactArguments(args))
+ .containsExactly(
+ "blaze",
+ "build",
+ "--bes_header=<REDACTED>",
+ "--remote_header",
+ "<REDACTED>",
+ "--some_other_flag")
+ .inOrder();
+ }
}
diff --git a/src/test/shell/integration/build_event_stream_test.sh b/src/test/shell/integration/build_event_stream_test.sh
index b496f56..3093f17 100755
--- a/src/test/shell/integration/build_event_stream_test.sh
+++ b/src/test/shell/integration/build_event_stream_test.sh
@@ -1355,11 +1355,11 @@
expect_log 'command_line_label: "canonical"'
expect_log 'command_line_label: "tool"'
- # Expect the actual tool command line flag to appear twice, because of the two
- # bazel command lines that are reported
- expect_log_n 'combined_form: "--experimental_tool_command_line=' 2
- expect_log_n 'option_name: "experimental_tool_command_line"' 2
- expect_log_n 'option_value: "foo bar"' 2
+ # Hidden options should not appear in the option lists of the original or
+ # canonical command line events
+ expect_not_log 'combined_form: "--experimental_tool_command_line='
+ expect_not_log 'option_name: "experimental_tool_command_line"'
+ expect_not_log 'option_value: "foo bar"'
# Check the contents of the tool command line
expect_log_once 'chunk: "foo bar"'