diff --git a/.lane/memory/apps/app/src/team-setup.tsx/01M5JRQPYQBFM1R674ESDP4DR9-hello-s-github-login-can-arr.md b/.lane/memory/apps/app/src/team-setup.tsx/01M5JRQPYQBFM1R674ESDP4DR9-hello-s-github-login-can-arr.md
new file mode 100644
index 00000000..5a94d9f0
--- /dev/null
+++ b/.lane/memory/apps/app/src/team-setup.tsx/01M5JRQPYQBFM1R674ESDP4DR9-hello-s-github-login-can-arr.md
@@ -0,0 +1,12 @@
+---
+id: 01M5JRQPYQBFM1R674ESDP4DR9
+anchor: function Checks
+created: 2026-10-22T21:37:06Z
+norm: '1'
+sig: afce47395c279e36
+body_hash: e0c2b3947f605c0a
+raw_hash: 9875d4d01a08d2a7
+lines: 611-641
+---
+
+hello's GitHub login can arrive before gh answers and isn't resent, so the setup check reads gh's sign-in from diagnostics
diff --git a/apps/app/src/host-settings.tsx b/apps/app/src/host-settings.tsx
index 951bbba4..b639547f 100644
--- a/apps/app/src/host-settings.tsx
+++ b/apps/app/src/host-settings.tsx
@@ -242,7 +242,11 @@ export function HostStatus({ active }: { active: boolean }) {
description={tool.path}
footer={tool.error && {tool.error}}
>
- {(tool.version || !tool.path) && {tool.version || "Not found"}}
+ {(tool.version || tool.account || !tool.path) && (
+
+ {tool.version || (tool.account ? `Signed in as ${tool.account}` : "Not found")}
+
+ )}
))}
>
diff --git a/apps/app/src/team-setup.tsx b/apps/app/src/team-setup.tsx
index 8cc00b6e..77fbd8fd 100644
--- a/apps/app/src/team-setup.tsx
+++ b/apps/app/src/team-setup.tsx
@@ -611,6 +611,9 @@ function Join({ initial, deployed, onJoined, back }: {
export function Checks({ github }: { github?: string }) {
const { value } = useDiagnostics(true);
const tailscale = value?.tailscale;
+ const cli = value?.tools.find((tool) => tool.name === "GitHub CLI");
+ // hello's login can arrive before gh answers and isn't resent, so a fresh host's check reads gh here.
+ const login = cli?.account || github;
return (
@@ -620,12 +623,18 @@ export function Checks({ github }: { github?: string }) {
? "isn't connected"
: "…"}
-
- {github ? `GitHub · ${github}` : (
- <>
- GitHub: run gh auth login
- >
- )}
+
+ {login ? `GitHub · ${login}` : cli && !cli.path
+ ? (
+ <>
+ GitHub: install GitHub CLI, then run gh auth login
+ >
+ )
+ : (
+ <>
+ GitHub: run gh auth login
+ >
+ )}
);
diff --git a/apps/host/src/diagnostics.ts b/apps/host/src/diagnostics.ts
index ac997826..ed5f7d00 100644
--- a/apps/host/src/diagnostics.ts
+++ b/apps/host/src/diagnostics.ts
@@ -6,6 +6,7 @@ import { config } from "./config";
import * as directory from "./directory";
import * as served from "./directory-server";
import { shell as environment } from "./environment";
+import * as github from "./github";
import { latest } from "./keys";
import type { Diagnostics } from "./protocol";
import * as tailnet from "./tailnet";
@@ -41,6 +42,32 @@ async function shell(): Promise {
}
}
+async function gh(): Promise {
+ const name = "GitHub CLI";
+ const path = Bun.which("gh", { PATH: process.env.PATH }) || undefined;
+ if (!path) {
+ return { name, error: "Install GitHub CLI, then sign in with gh auth login, to use GitHub." };
+ }
+ const child = Bun.spawn([path, "auth", "status", "--hostname", "github.com", "--json", "hosts"], {
+ env: github.environment(),
+ stdin: "ignore",
+ stdout: "pipe",
+ stderr: "ignore",
+ timeout: 5000,
+ });
+ const [out, code] = await Promise.all([new Response(child.stdout).text(), child.exited]);
+ if (code !== 0) return { name, path, error: "GitHub CLI could not start." };
+ const { hosts } = JSON.parse(out) as {
+ hosts: Record;
+ };
+ const account = hosts["github.com"]?.find((account) => account.active);
+ if (!account) return { name, path, error: "Sign in with gh auth login to use GitHub." };
+ if (account.state !== "success" || !account.login) {
+ return { name, path, error: "GitHub didn't accept this sign-in. Run gh auth login again." };
+ }
+ return { name, path, account: account.login };
+}
+
export async function diagnostics(sync?: boolean): Promise {
const secret = latest("ACE_SECRET", sync);
if (sync) {
@@ -49,7 +76,7 @@ export async function diagnostics(sync?: boolean): Promise {
if (!(await secret).waiting) await Promise.race([synced, Bun.sleep(25_000)]);
}
const [tools, tailscale] = await Promise.all([
- Promise.all([git(), shell()]),
+ Promise.all([git(), gh(), shell()]),
tailnet.diagnostics(started),
]);
const { hosted, app, compute, hosts } = directory.read();
diff --git a/apps/host/src/github.ts b/apps/host/src/github.ts
index f1818666..cbd0de3c 100644
--- a/apps/host/src/github.ts
+++ b/apps/host/src/github.ts
@@ -147,9 +147,8 @@ function sentence(error: string): string {
return error.replace(/^GraphQL: /, "GitHub refused the request: ");
}
-async function gh(args: string[]): Promise {
- const path = Bun.which("gh", { PATH: process.env.PATH });
- if (!path) throw new Error("Install GitHub CLI on this host, then sign in with gh auth login.");
+/** The environment gh runs in: this host's GitHub token, github.com, and no prompts. */
+export function environment(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
...githubEnv(),
@@ -158,8 +157,14 @@ async function gh(args: string[]): Promise {
GH_PAGER: "cat",
};
delete env.GH_REPO;
+ return env;
+}
+
+async function gh(args: string[]): Promise {
+ const path = Bun.which("gh", { PATH: process.env.PATH });
+ if (!path) throw new Error("Install GitHub CLI on this host, then sign in with gh auth login.");
const child = Bun.spawn([path, ...args], {
- env,
+ env: environment(),
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
diff --git a/apps/host/src/protocol.ts b/apps/host/src/protocol.ts
index 5d72279b..9739d4b3 100644
--- a/apps/host/src/protocol.ts
+++ b/apps/host/src/protocol.ts
@@ -39,7 +39,7 @@ export type WorkerInfo = { id: string; pid: number };
export type WorkerRequest = { op: "worker" } | { op: "freeze" } | { op: "export" };
export type Diagnostics = {
- tools: { name: string; path?: string; version?: string; error?: string }[];
+ tools: { name: string; path?: string; version?: string; account?: string; error?: string }[];
tailscale: {
state: "missing" | "offline" | "running" | "error";
name?: string;