Turns out all I needed to do was run it as single-threaded apartment. This answer gave me the hint I needed:
https://stackoverflow.com/a/19275241/2382032
using System;
using System.Reflection;
using System.Threading;
using mshtml;
using SHDocVw;
namespace ConsoleApp12
{
class Program
{
static void Main(string[] args)
{
string command = string.Empty;
while (command != "exit")
{
Console.Write("Enter command: ");
command = Console.ReadLine();
if (command == "go")
{
Thread thread = new Thread(() =>
{
ShellWindows shellWindows = new SHDocVw.ShellWindows();
foreach (var shellWindow in shellWindows)
{
var ie = shellWindow as SHDocVw.InternetExplorer;
if (ie != null)
{
var doc = ie.Document as HTMLDocument;
if (doc != null)
{
if (doc.title.Contains("Test Page"))
{
object script = doc.Script;
script.GetType().InvokeMember("sayHello", BindingFlags.InvokeMethod, null, script, null);
}
}
}
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
else
{
Console.WriteLine("Unrecognized command");
}
}
}
}
}