diff --git a/fastblocks/cli.py b/fastblocks/cli.py
index 9da0056..3c58be3 100644
--- a/fastblocks/cli.py
+++ b/fastblocks/cli.py
@@ -151,37 +151,66 @@ def _run_async(coro: t.Coroutine[t.Any, t.Any, t.Any]) -> t.Any:
 
     thread = threading.Thread(target=_runner, daemon=True)
     thread.start()
     thread.join()
     if exc:
         raise exc[0]
     return result[0] if result else None
 
 
 @cli.command()
-def run(docker: bool = False, granian: bool = False, host: str = "127.0.0.1") -> None:
+def run(
+    host: Annotated[str, typer.Option(help="Bind host")] = "127.0.0.1",
+    port: Annotated[int, typer.Option(help="Bind port")] = 8000,
+    app_module: Annotated[
+        str, typer.Option(help="ASGI app import string (e.g. main:app)")
+    ] = "main:app",
+    reload: Annotated[bool, typer.Option(help="Enable auto-reload")] = False,
+    docker: bool = False,
+    granian: bool = False,
+) -> None:
+    """Run the FastBlocks app via uvicorn or granian.
+
+    Starts the ASGI server bound to ``host:port`` (defaults 127.0.0.1:8000)
+    and imports ``app_module`` (defaults ``main:app``) via uvicorn's
+    import-string machinery. Pass ``--granian`` to use Granian instead of
+    uvicorn, or ``--docker`` to launch a one-shot container.
+    """
     if docker:
         # Fire-and-forget: the interactive ``docker run`` handles its
         # own exit; we deliberately do not raise on non-zero so a
         # transient error here does not brick the CLI.
         execute(
             f"docker run -it -ePORT=8080 -p8080:8080 {Path.cwd().stem}".split(),
             check=False,
         )
-    else:
-        setup_signal_handlers()
-        if granian:
-            from granian.constants import Interfaces
+        return
+    setup_signal_handlers()
+    if granian:
+        from granian.constants import Interfaces
 
-            Granian("main:app", address=host, interface=Interfaces.ASGI).serve()
-        else:
-            uvicorn.run(app=run_args["app"], host=host, lifespan="on", log_config=None)
+        Granian(
+            app_module,
+            address=host,
+            port=port,
+            reload=reload,
+            interface=Interfaces.ASGI,
+        ).serve()
+    else:
+        uvicorn.run(
+            app=app_module,
+            host=host,
+            port=port,
+            reload=reload,
+            lifespan="on",
+            log_config=None,
+        )
 
 
 @cli.command()
 def dev(granian: bool = False) -> None:
     setup_signal_handlers()
     if granian:
         from granian.constants import Interfaces
 
         Granian(
             "main:app",
diff --git a/tests/cli/test_run_subcommand.py b/tests/cli/test_run_subcommand.py
new file mode 100644
index 0000000..2b15775
--- /dev/null
+++ b/tests/cli/test_run_subcommand.py
@@ -0,0 +1,58 @@
+"""Tests for the ``fastblocks run`` subcommand.
+
+Verifies the ``run`` CLI subcommand advertises the expected options
+(REQ-P2-B1-T8). The subcommand itself launches uvicorn/granian and is
+not exercised here — only the CLI surface (help text + defaults) is
+contract-tested via ``typer.testing.CliRunner``.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+from typer.testing import CliRunner
+
+if TYPE_CHECKING:
+    from fastblocks.cli import FastblocksCLI
+
+
+@pytest.fixture
+def cli_app(tmp_path, monkeypatch: pytest.MonkeyPatch) -> "FastblocksCLI":
+    """Import ``fastblocks.cli`` from a non-repo CWD.
+
+    ``fastblocks/cli.py`` raises ``SystemExit`` at module load if
+    ``Path.cwd() == the fastblocks repo directory``. chdir to
+    ``tmp_path`` before importing so the module loads cleanly.
+    """
+    monkeypatch.chdir(tmp_path)
+    from fastblocks.cli import cli
+
+    return cli
+
+
+@pytest.fixture
+def runner() -> CliRunner:
+    """CliRunner for invoking the typer app."""
+    return CliRunner()
+
+
+def test_run_help(cli_app: "FastblocksCLI", runner: CliRunner) -> None:
+    """``fastblocks run --help`` exits 0 and surfaces the run docstring."""
+    result = runner.invoke(cli_app, ["run", "--help"])
+    assert result.exit_code == 0, result.stdout + result.stderr
+    assert "Run the FastBlocks app" in result.output
+
+
+def test_run_default_args(cli_app: "FastblocksCLI", runner: CliRunner) -> None:
+    """``fastblocks run --help`` advertises the documented defaults.
+
+    Pinned defaults: host ``127.0.0.1``, port ``8000``,
+    app_module ``main:app``. The starter README points users at
+    ``uv run fastblocks run`` and depends on these defaults.
+    """
+    result = runner.invoke(cli_app, ["run", "--help"])
+    assert result.exit_code == 0, result.stdout + result.stderr
+    assert "127.0.0.1" in result.output
+    assert "8000" in result.output
+    assert "main:app" in result.output
\ No newline at end of file
