diff --git a/src/ClaudeDo.Worker/Logging/HubLogFilter.cs b/src/ClaudeDo.Worker/Logging/HubLogFilter.cs
new file mode 100644
index 00000000..f1567155
--- /dev/null
+++ b/src/ClaudeDo.Worker/Logging/HubLogFilter.cs
@@ -0,0 +1,15 @@
+using Serilog.Events;
+
+namespace ClaudeDo.Worker.Logging;
+
+/// Logger-level filter keeping hub invocation arguments (PATs) out of every sink.
+public static class HubLogFilter
+{
+ private const string Dispatcher = "Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher";
+
+ /// True for the DefaultHubDispatcher events that print the invocation message including its arguments.
+ public static bool IsInvocationWithArguments(LogEvent e) =>
+ e.Properties.TryGetValue("SourceContext", out var src) && src is ScalarValue { Value: Dispatcher }
+ && e.Properties.TryGetValue("EventId", out var id) && id is StructureValue sv
+ && sv.Properties.FirstOrDefault(p => p.Name == "Name")?.Value is ScalarValue { Value: "ReceivedHubInvocation" or "ReceivedStreamHubInvocation" };
+}
diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs
index 4541a5cc..1ebec23e 100644
--- a/src/ClaudeDo.Worker/Program.cs
+++ b/src/ClaudeDo.Worker/Program.cs
@@ -65,6 +65,8 @@ builder.Host.UseSerilog((ctx, lc) => lc
// see it either, since SignalR rejects the invocation while parsing the message, before any
// hub method (and its filter pipeline) is dispatched.
.MinimumLevel.Override("Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher", Serilog.Events.LogEventLevel.Debug)
+ // The same Debug override logs every invocation's arguments (PATs); drop just those events for all sinks.
+ .Filter.ByExcluding(ClaudeDo.Worker.Logging.HubLogFilter.IsInvocationWithArguments)
.WriteTo.File(
System.IO.Path.Combine(logRoot, "worker-.log"),
rollingInterval: RollingInterval.Day,
diff --git a/tests/ClaudeDo.Worker.Tests/Logging/HubLogFilterTests.cs b/tests/ClaudeDo.Worker.Tests/Logging/HubLogFilterTests.cs
new file mode 100644
index 00000000..fd618f1c
--- /dev/null
+++ b/tests/ClaudeDo.Worker.Tests/Logging/HubLogFilterTests.cs
@@ -0,0 +1,39 @@
+using ClaudeDo.Worker.Logging;
+using Serilog.Events;
+using Serilog.Parsing;
+
+namespace ClaudeDo.Worker.Tests.Logging;
+
+public class HubLogFilterTests
+{
+ private const string Dispatcher = "Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher";
+
+ private static LogEvent Evt(string source, int eventId, string eventName, string message) => new(
+ DateTimeOffset.UtcNow, LogEventLevel.Debug, null, new MessageTemplateParser().Parse(message),
+ [
+ new LogEventProperty("SourceContext", new ScalarValue(source)),
+ new LogEventProperty("EventId", new StructureValue(
+ [new LogEventProperty("Id", new ScalarValue(eventId)), new LogEventProperty("Name", new ScalarValue(eventName))])),
+ ]);
+
+ [Fact]
+ public void Received_invocation_with_arguments_is_excluded()
+ {
+ var e = Evt(Dispatcher, 1, "ReceivedHubInvocation", "Received hub invocation: Target: SetAzureDevOpsToken, Arguments: [ secret ]");
+ Assert.True(HubLogFilter.IsInvocationWithArguments(e));
+ }
+
+ [Fact]
+ public void Binding_failure_from_same_source_passes()
+ {
+ var e = Evt(Dispatcher, 3, "InvalidHubParameters", "Parameters to hub method 'X' are incorrect.");
+ Assert.False(HubLogFilter.IsInvocationWithArguments(e));
+ }
+
+ [Fact]
+ public void Same_event_name_from_other_source_passes()
+ {
+ var e = Evt("Other.Source", 1, "ReceivedHubInvocation", "x");
+ Assert.False(HubLogFilter.IsInvocationWithArguments(e));
+ }
+}