diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
index a6fc20be8def590faa9434dc26ba5b3e48d877b0..6dd40c126530145068fe8e719c9e120e708a6d03 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
@@ -25,6 +25,8 @@ import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.parameters.options.counted
 import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.runBlocking
 import org.briarproject.mailbox.core.CoreEagerSingletons
 import org.briarproject.mailbox.core.JavaCliEagerSingletons
 import org.briarproject.mailbox.core.db.TransactionManager
@@ -33,6 +35,7 @@ import org.briarproject.mailbox.core.setup.QrCodeEncoder
 import org.briarproject.mailbox.core.setup.SetupManager
 import org.briarproject.mailbox.core.setup.WipeManager
 import org.briarproject.mailbox.core.system.InvalidIdException
+import org.briarproject.mailbox.core.tor.TorPlugin
 import org.slf4j.LoggerFactory.getLogger
 import java.util.logging.Level.ALL
 import java.util.logging.Level.INFO
@@ -77,6 +80,9 @@ class Main : CliktCommand(
     @Inject
     internal lateinit var wipeManager: WipeManager
 
+    @Inject
+    internal lateinit var torPlugin: TorPlugin
+
     @Inject
     internal lateinit var qrCodeEncoder: QrCodeEncoder
 
@@ -138,11 +144,20 @@ class Main : CliktCommand(
             setupManager.getOwnerToken(txn) != null
         }
         if (!ownerTokenExists) {
-            // TODO remove before release
-            val token = setupToken ?: db.read { setupManager.getSetupToken(it) }
-            println("curl -v -H \"Authorization: Bearer $token\" -X PUT http://localhost:8000/setup")
-            // FIXME: We need to wait for the hidden service address to become available
+            if (debug) {
+                val token = setupToken ?: db.read { setupManager.getSetupToken(it) }
+                println(
+                    "curl -v -H \"Authorization: Bearer $token\" -X PUT " +
+                        "http://localhost:8000/setup"
+                )
+            }
             // If not set up, show QR code for manual setup
+            runBlocking {
+                // wait until Tor becomes active and published the onion service
+                torPlugin.state.takeWhile { state ->
+                    state != TorPlugin.State.ACTIVE
+                }
+            }
             qrCodeEncoder.getQrCodeBitMatrix()?.let {
                 println(QrCodeRenderer.getQrString(it))
             }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
index ab2bbd79673fdc25445e7dbae238bd415e5d5426..73f881b47dfd9e931d5fc0b0db9809e0c6d0a59e 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
@@ -59,10 +59,14 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
 import static java.util.Objects.requireNonNull;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
 import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS;
 import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY;
 import static org.briarproject.mailbox.core.tor.TorConstants.CONTROL_PORT;
@@ -70,7 +74,6 @@ import static org.briarproject.mailbox.core.tor.TorConstants.HS_ADDRESS_V3;
 import static org.briarproject.mailbox.core.tor.TorConstants.HS_PRIVATE_KEY_V3;
 import static org.briarproject.mailbox.core.tor.TorConstants.SETTINGS_NAMESPACE;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.ACTIVE;
-import static org.briarproject.mailbox.core.tor.TorPlugin.State.DISABLED;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.ENABLING;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.INACTIVE;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.STARTING_STOPPING;
@@ -155,6 +158,10 @@ public abstract class TorPlugin
 		return new File(torDirectory, "obfs4proxy");
 	}
 
+	public StateFlow<State> getState() {
+		return state.state;
+	}
+
 	@Override
 	public void startService() throws ServiceException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
@@ -404,6 +411,7 @@ public abstract class TorPlugin
 				logException(LOG, e);
 			}
 		}
+		state.setServicePublished();
 	}
 
 	@Nullable
@@ -534,7 +542,6 @@ public abstract class TorPlugin
 				else LOG.info("Country code: " + country);
 			}
 
-			int reasonsDisabled = 0;
 			boolean enableNetwork = false;
 			boolean enableBridges = false;
 			boolean useMeek = false;
@@ -557,7 +564,6 @@ public abstract class TorPlugin
 					LOG.info("Not using bridges");
 				}
 			}
-			state.setReasonsDisabled(reasonsDisabled);
 			try {
 				if (enableNetwork) {
 					enableBridges(enableBridges, useMeek);
@@ -581,7 +587,10 @@ public abstract class TorPlugin
 	}
 
 	@ThreadSafe
-	protected class PluginState {
+	protected static class PluginState {
+
+		private final MutableStateFlow<State> state =
+				MutableStateFlow(STARTING_STOPPING);
 
 		@GuardedBy("this")
 		private boolean started = false,
@@ -590,10 +599,7 @@ public abstract class TorPlugin
 				networkEnabled = false,
 				bootstrapped = false,
 				circuitBuilt = false,
-				settingsChecked = false;
-
-		@GuardedBy("this")
-		private int reasonsDisabled = 0;
+				servicePublished = false;
 
 		@GuardedBy("this")
 		@Nullable
@@ -601,7 +607,7 @@ public abstract class TorPlugin
 
 		synchronized void setStarted() {
 			started = true;
-//            callback.pluginStateChanged(getState());
+			state.setValue(getCurrentState());
 		}
 
 		synchronized boolean isTorRunning() {
@@ -613,63 +619,52 @@ public abstract class TorPlugin
 			stopped = true;
 			ServerSocket ss = serverSocket;
 			serverSocket = null;
-//            callback.pluginStateChanged(getState());
+			state.setValue(getCurrentState());
 			return ss;
 		}
 
 		synchronized void setBootstrapped() {
 			bootstrapped = true;
-//            callback.pluginStateChanged(getState());
+			state.setValue(getCurrentState());
 		}
 
 		synchronized boolean getAndSetCircuitBuilt() {
 			boolean firstCircuit = !circuitBuilt;
 			circuitBuilt = true;
-//            callback.pluginStateChanged(getState());
+			state.setValue(getCurrentState());
 			return firstCircuit;
 		}
 
+		synchronized void setServicePublished() {
+			servicePublished = true;
+			state.setValue(getCurrentState());
+		}
+
 		synchronized void enableNetwork(boolean enable) {
 			networkInitialised = true;
 			networkEnabled = enable;
 			if (!enable) circuitBuilt = false;
-//            callback.pluginStateChanged(getState());
+			state.setValue(getCurrentState());
 		}
 
-		synchronized void setReasonsDisabled(int reasonsDisabled) {
-			settingsChecked = true;
-			this.reasonsDisabled = reasonsDisabled;
-//            callback.pluginStateChanged(getState());
-		}
-
-		synchronized State getState() {
-			if (!started || stopped || !settingsChecked) {
+		private synchronized State getCurrentState() {
+			if (!started || stopped) {
 				return STARTING_STOPPING;
 			}
-			if (reasonsDisabled != 0) return DISABLED;
 			if (!networkInitialised) return ENABLING;
 			if (!networkEnabled) return INACTIVE;
-			return bootstrapped && circuitBuilt ? ACTIVE : ENABLING;
-		}
-
-		synchronized int getReasonsDisabled() {
-			return getState() == DISABLED ? reasonsDisabled : 0;
+			return bootstrapped && circuitBuilt && servicePublished ?
+					ACTIVE : ENABLING;
 		}
 
 	}
 
-	enum State {
-
+	public enum State {
 		/**
 		 * The plugin has not finished starting or has been stopped.
 		 */
 		STARTING_STOPPING,
 
-		/**
-		 * The plugin is disabled by settings.
-		 */
-		DISABLED,
-
 		/**
 		 * The plugin is being enabled and can't yet make or receive
 		 * connections.