diff --git a/web/components/gsd/app-shell.tsx b/web/components/gsd/app-shell.tsx
index 3b0da7b49..88442c53b 100644
--- a/web/components/gsd/app-shell.tsx
+++ b/web/components/gsd/app-shell.tsx
@@ -235,6 +235,41 @@ function WorkspaceChrome() {
detection.kind !== "active-gsd" &&
detection.kind !== "empty-gsd"
+ // --- Unauthenticated gate ---
+ // Render a clear recovery screen before any workspace chrome is mounted so
+ // users who open a manually-typed URL (no #token= fragment) get actionable
+ // guidance instead of a cascade of 401 errors.
+ if (workspace.bootStatus === "unauthenticated") {
+ return (
+
+
+
+
+
Authentication Required
+
+ This workspace requires an auth token. Copy the full URL from your terminal
+ (including the{" "}
+ #token=…{" "}
+ part) or restart with{" "}
+ gsd --web.
+
+
+
+ )
+ }
+
return (
diff --git a/web/lib/auth.ts b/web/lib/auth.ts
index 47ac0515f..780df8be1 100644
--- a/web/lib/auth.ts
+++ b/web/lib/auth.ts
@@ -81,10 +81,20 @@ export function authHeaders(extra?: Record): Record {
const token = getAuthToken()
- if (!token) return fetch(input, init)
+ if (!token) {
+ return new Response(JSON.stringify({ error: "No auth token available" }), {
+ status: 401,
+ headers: { "Content-Type": "application/json" },
+ })
+ }
const headers = new Headers(init?.headers)
if (!headers.has("Authorization")) {
diff --git a/web/lib/gsd-workspace-store.tsx b/web/lib/gsd-workspace-store.tsx
index 335085c47..567910ed9 100644
--- a/web/lib/gsd-workspace-store.tsx
+++ b/web/lib/gsd-workspace-store.tsx
@@ -66,7 +66,7 @@ import type {
} from "./session-browser-contract"
import { authFetch, appendAuthParam } from "./auth"
-export type WorkspaceStatus = "idle" | "loading" | "ready" | "error"
+export type WorkspaceStatus = "idle" | "loading" | "ready" | "error" | "unauthenticated"
export type WorkspaceConnectionState =
| "idle"
| "connecting"
@@ -4135,6 +4135,13 @@ export class GSDWorkspaceStore {
})
if (!response.ok) {
+ if (response.status === 401) {
+ this.patchState({
+ bootStatus: "unauthenticated",
+ connectionState: "error",
+ })
+ return
+ }
throw new Error(`Boot request failed with ${response.status}`)
}