From 72b5cddbe16b98e3749026b65f3e21e2bc611421 Mon Sep 17 00:00:00 2001 From: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Date: Wed, 6 May 2026 00:04:59 +0200 Subject: [PATCH] fix(doctor): surface GH_CONFIG_DIR hint when gh auth lives at a different HOME When OpenClaw spawns an agent shell with a different HOME than the user that ran `gh auth login` (per-agent codex homes, systemd User= services, sudo'd shells), `gh` looks at $XDG_CONFIG_HOME/gh or $HOME/.config/gh and reports "not logged into any GitHub hosts" even though the operator HOME has a valid hosts.yml. Add `detectGhConfigDirMismatch` in src/agents/skills/gh-config-discovery.ts: a pure helper that takes process env plus a fileExists probe and returns either "auth-discoverable", "no-known-auth", "explicit-gh-config-dir-set", or a "mismatch" with the alternate config dir, the host file path, and a suggested GH_CONFIG_DIR value to set on the gateway service environment. The helper checks `/root`, `$SUDO_USER`'s home, and `$USER`'s home as candidate operator homes on Linux/macOS, and uses platform-specific path joins so the same logic works on Windows test runners. Wire the helper into the doctor skills health flow: when the github skill is reported and the gh binary is present, call the discovery helper and, on a mismatch, print a "GitHub CLI" note with the operator-actionable fix instructions before any unavailable-skill repair prompt. Update skills/github/SKILL.md with a troubleshooting subsection that documents GH_CONFIG_DIR for service/agent environments where HOME differs from the user that ran `gh auth login`. Fixes #78063. --- skills/github/SKILL.md | 19 ++ src/agents/skills/gh-config-discovery.test.ts | 223 ++++++++++++++++++ src/agents/skills/gh-config-discovery.ts | 187 +++++++++++++++ src/commands/doctor-skills.test.ts | 63 +++++ src/commands/doctor-skills.ts | 43 ++++ 5 files changed, 535 insertions(+) create mode 100644 src/agents/skills/gh-config-discovery.test.ts create mode 100644 src/agents/skills/gh-config-discovery.ts diff --git a/skills/github/SKILL.md b/skills/github/SKILL.md index b6bef0798225..4739bd25775b 100644 --- a/skills/github/SKILL.md +++ b/skills/github/SKILL.md @@ -63,6 +63,25 @@ gh auth login gh auth status ``` +### When the gateway HOME differs from the operator HOME + +OpenClaw agent shells often run with a different `HOME` than the user that ran +`gh auth login` (per-agent codex homes, systemd `User=` services, sudo). `gh` +looks up its config under `$GH_CONFIG_DIR`, then `$XDG_CONFIG_HOME/gh`, then +`$HOME/.config/gh`, so the agent shell can report `not logged into any GitHub +hosts` even when the operator login is intact. + +To point the gateway at the canonical `gh` config, set `GH_CONFIG_DIR` on the +service environment, e.g. + +```bash +# Gateway service env file (example: ~/.openclaw/gateway.systemd.env) +GH_CONFIG_DIR=/path/to/operator/.config/gh +``` + +then restart the gateway. `openclaw doctor` warns when it detects an authenticated +`hosts.yml` outside the agent process's effective `gh` config dir. + ## Common Commands ### Pull Requests diff --git a/src/agents/skills/gh-config-discovery.test.ts b/src/agents/skills/gh-config-discovery.test.ts new file mode 100644 index 000000000000..bfcf5050f1e3 --- /dev/null +++ b/src/agents/skills/gh-config-discovery.test.ts @@ -0,0 +1,223 @@ +import { describe, expect, it } from "vitest"; +import { + detectGhConfigDirMismatch, + formatGhConfigDirMismatchHint, + type GhConfigDirMismatch, + type GhConfigDiscoveryInput, +} from "./gh-config-discovery.js"; + +function makeInput(overrides: Partial): GhConfigDiscoveryInput { + return { + platform: "linux", + env: {}, + fileExists: () => false, + ...overrides, + }; +} + +function fileSet(...paths: readonly string[]): (absolutePath: string) => boolean { + const set = new Set(paths); + return (absolutePath) => set.has(absolutePath); +} + +describe("detectGhConfigDirMismatch", () => { + it("returns 'explicit-gh-config-dir-set' when GH_CONFIG_DIR is already set", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/agent/home", GH_CONFIG_DIR: "/etc/openclaw/gh" }, + }), + ); + expect(result).toEqual({ kind: "explicit-gh-config-dir-set", ghConfigDir: "/etc/openclaw/gh" }); + }); + + it("returns 'no-process-home' when HOME and XDG and APPDATA are missing", () => { + const result = detectGhConfigDirMismatch(makeInput({ env: {} })); + expect(result).toEqual({ kind: "no-process-home" }); + }); + + it("returns 'auth-discoverable' when the effective config dir already has hosts.yml", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/agent/home" }, + fileExists: fileSet("/agent/home/.config/gh/hosts.yml"), + }), + ); + expect(result).toEqual({ + kind: "auth-discoverable", + effectiveConfigDir: "/agent/home/.config/gh", + }); + }); + + it("flags a mismatch when /root/.config/gh has hosts.yml but the agent HOME does not", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/root/.openclaw/agents/main/agent/codex-home/home" }, + fileExists: fileSet("/root/.config/gh/hosts.yml"), + }), + ); + expect(result).toMatchObject({ + kind: "mismatch", + effectiveConfigDir: "/root/.openclaw/agents/main/agent/codex-home/home/.config/gh", + alternateConfigDir: "/root/.config/gh", + alternateHostsFile: "/root/.config/gh/hosts.yml", + alternateHomeHint: "/root", + suggestedEnvValue: "/root/.config/gh", + }); + }); + + it("uses SUDO_USER home as a candidate when set", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/var/lib/openclaw/agent", SUDO_USER: "alice" }, + fileExists: fileSet("/home/alice/.config/gh/hosts.yml"), + }), + ); + expect(result).toMatchObject({ + kind: "mismatch", + alternateConfigDir: "/home/alice/.config/gh", + alternateHomeHint: "/home/alice", + }); + }); + + it("uses USER home as a fallback candidate when SUDO_USER is missing", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/var/lib/openclaw/agent", USER: "ops" }, + fileExists: fileSet("/home/ops/.config/gh/hosts.yml"), + }), + ); + expect(result).toMatchObject({ + kind: "mismatch", + alternateConfigDir: "/home/ops/.config/gh", + alternateHomeHint: "/home/ops", + }); + }); + + it("ignores USER=root since /root is already part of the default candidate set", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/agent/home", USER: "root" }, + fileExists: fileSet("/root/.config/gh/hosts.yml"), + }), + ); + expect(result.kind).toBe("mismatch"); + }); + + it("returns 'no-known-auth' when no candidate has hosts.yml", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/agent/home" }, + fileExists: () => false, + }), + ); + expect(result).toEqual({ + kind: "no-known-auth", + effectiveConfigDir: "/agent/home/.config/gh", + }); + }); + + it("does not flag a mismatch when the agent HOME equals the operator HOME", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/root" }, + fileExists: fileSet("/root/.config/gh/hosts.yml"), + }), + ); + expect(result).toEqual({ + kind: "auth-discoverable", + effectiveConfigDir: "/root/.config/gh", + }); + }); + + it("respects XDG_CONFIG_HOME for the effective config dir on Linux", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/agent/home", XDG_CONFIG_HOME: "/agent/xdg" }, + fileExists: fileSet("/agent/xdg/gh/hosts.yml"), + }), + ); + expect(result).toEqual({ + kind: "auth-discoverable", + effectiveConfigDir: "/agent/xdg/gh", + }); + }); + + it("uses HOME/.config/gh on darwin (matches gh's documented macOS lookup)", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + platform: "darwin", + env: { HOME: "/Users/agent" }, + fileExists: fileSet("/Users/operator/.config/gh/hosts.yml"), + candidateOperatorHomes: ["/Users/operator"], + }), + ); + expect(result).toMatchObject({ + kind: "mismatch", + effectiveConfigDir: "/Users/agent/.config/gh", + alternateConfigDir: "/Users/operator/.config/gh", + alternateHomeHint: "/Users/operator", + }); + }); + + it("uses APPDATA/GitHub CLI on win32", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + platform: "win32", + env: { APPDATA: "C:\\Users\\agent\\AppData\\Roaming" }, + fileExists: fileSet("C:\\Users\\agent\\AppData\\Roaming\\GitHub CLI\\hosts.yml"), + }), + ); + expect(result).toMatchObject({ + kind: "auth-discoverable", + effectiveConfigDir: "C:\\Users\\agent\\AppData\\Roaming\\GitHub CLI", + }); + }); + + it("respects an explicit candidateOperatorHomes list", () => { + const result = detectGhConfigDirMismatch( + makeInput({ + env: { HOME: "/agent/home" }, + fileExists: fileSet("/srv/automation/.config/gh/hosts.yml"), + candidateOperatorHomes: ["/srv/automation"], + }), + ); + expect(result).toMatchObject({ + kind: "mismatch", + alternateConfigDir: "/srv/automation/.config/gh", + alternateHomeHint: "/srv/automation", + }); + }); +}); + +describe("formatGhConfigDirMismatchHint", () => { + it("formats the mismatch into operator-actionable lines", () => { + const mismatch: GhConfigDirMismatch = { + effectiveConfigDir: "/agent/home/.config/gh", + alternateConfigDir: "/root/.config/gh", + alternateHostsFile: "/root/.config/gh/hosts.yml", + alternateHomeHint: "/root", + suggestedEnvValue: "/root/.config/gh", + }; + expect(formatGhConfigDirMismatchHint(mismatch)).toEqual([ + "GitHub CLI auth was found at a different HOME than the one this OpenClaw process uses.", + " Process gh config dir: /agent/home/.config/gh", + " Authenticated config: /root/.config/gh (contains hosts.yml)", + " Authenticated HOME: /root", + " Fix: set GH_CONFIG_DIR=/root/.config/gh on the OpenClaw service environment, then restart the gateway.", + ]); + }); + + it("omits the home hint line when the alternate has no associated HOME", () => { + const mismatch: GhConfigDirMismatch = { + effectiveConfigDir: "/agent/home/.config/gh", + alternateConfigDir: "/srv/automation/.config/gh", + alternateHostsFile: "/srv/automation/.config/gh/hosts.yml", + suggestedEnvValue: "/srv/automation/.config/gh", + }; + const lines = formatGhConfigDirMismatchHint(mismatch); + expect(lines.some((line) => line.includes("Authenticated HOME"))).toBe(false); + expect(lines).toContain( + " Fix: set GH_CONFIG_DIR=/srv/automation/.config/gh on the OpenClaw service environment, then restart the gateway.", + ); + }); +}); diff --git a/src/agents/skills/gh-config-discovery.ts b/src/agents/skills/gh-config-discovery.ts new file mode 100644 index 000000000000..6c8404ca1826 --- /dev/null +++ b/src/agents/skills/gh-config-discovery.ts @@ -0,0 +1,187 @@ +import { posix as posixPath, win32 as win32Path } from "node:path"; + +function pathFor(platform: NodeJS.Platform) { + return platform === "win32" ? win32Path : posixPath; +} + +// Detects the case where `gh` is authenticated under one HOME but the current +// OpenClaw process is running with a different HOME (e.g. the per-agent +// codex-home, a systemd service home, or a sudo'd shell). Without GH_CONFIG_DIR +// the gh CLI looks at $XDG_CONFIG_HOME/gh or $HOME/.config/gh and reports +// "not logged in", even though the operator HOME has a valid hosts.yml. +// See https://github.com/openclaw/openclaw/issues/78063. + +export type GhConfigDiscoveryEnv = { + HOME?: string; + XDG_CONFIG_HOME?: string; + GH_CONFIG_DIR?: string; + APPDATA?: string; + SUDO_USER?: string; + USER?: string; + USERPROFILE?: string; +}; + +export type GhConfigDiscoveryInput = { + platform: NodeJS.Platform; + env: GhConfigDiscoveryEnv; + fileExists: (absolutePath: string) => boolean; + // Optional: well-known operator-home guesses to consider when looking for an + // alternate gh config dir. Defaults to a small Linux/macOS set; tests pass an + // explicit list to keep behavior deterministic. + candidateOperatorHomes?: readonly string[]; +}; + +export type GhConfigDirMismatch = { + // The directory `gh` would actually consult given the current process env. + effectiveConfigDir: string; + // The directory that contains the operator's real `hosts.yml`. + alternateConfigDir: string; + // Absolute path to the alternate hosts.yml that the current process won't see. + alternateHostsFile: string; + // The HOME-like path the alternate dir was derived from, if known. + alternateHomeHint?: string; + // Suggested env value the operator should set on the OpenClaw service to + // surface the alternate config to the agent shell. + suggestedEnvValue: string; +}; + +export type GhConfigDiscoveryResult = + | { kind: "no-gh-binary" } + | { kind: "explicit-gh-config-dir-set"; ghConfigDir: string } + | { kind: "no-process-home" } + | { kind: "auth-discoverable"; effectiveConfigDir: string } + | { kind: "no-known-auth"; effectiveConfigDir: string } + | ({ kind: "mismatch" } & GhConfigDirMismatch); + +const HOSTS_FILE = "hosts.yml"; + +// gh config-dir lookup order, matching the documented behavior of `gh +// help environment` for each platform. macOS uses Library/Application Support, +// Windows uses %AppData%\GitHub CLI, Linux/other uses XDG_CONFIG_HOME or +// $HOME/.config/gh. +function resolveEffectiveGhConfigDir(input: GhConfigDiscoveryInput): string | undefined { + const env = input.env; + if (env.GH_CONFIG_DIR && env.GH_CONFIG_DIR.trim()) { + return env.GH_CONFIG_DIR.trim(); + } + if (input.platform === "win32") { + const appData = env.APPDATA?.trim(); + if (appData) { + return pathFor(input.platform).join(appData, "GitHub CLI"); + } + const profile = env.USERPROFILE?.trim(); + if (profile) { + return pathFor(input.platform).join(profile, "AppData", "Roaming", "GitHub CLI"); + } + return undefined; + } + if (input.platform === "darwin") { + const home = env.HOME?.trim(); + if (!home) { + return undefined; + } + return pathFor(input.platform).join(home, ".config", "gh"); + } + // Linux and POSIX-like default + const xdg = env.XDG_CONFIG_HOME?.trim(); + if (xdg) { + return pathFor(input.platform).join(xdg, "gh"); + } + const home = env.HOME?.trim(); + if (!home) { + return undefined; + } + return pathFor(input.platform).join(home, ".config", "gh"); +} + +function defaultCandidateOperatorHomes(input: GhConfigDiscoveryInput): string[] { + const env = input.env; + const homes = new Set(); + // Common operator HOME on Linux servers running gateway as root. + if (input.platform !== "win32") { + homes.add("/root"); + } + // sudo invocation: the original shell user's home is exposed through SUDO_USER. + if (env.SUDO_USER?.trim()) { + const sudoUser = env.SUDO_USER.trim(); + homes.add(pathFor(input.platform).join("/home", sudoUser)); + if (input.platform === "darwin") { + homes.add(pathFor(input.platform).join("/Users", sudoUser)); + } + } + // USER fallback: works when HOME has been redirected but the login user is + // still on the env (e.g. systemd User= with PassEnvironment=USER). + if (env.USER?.trim()) { + const user = env.USER.trim(); + if (user !== "root") { + if (input.platform === "darwin") { + homes.add(pathFor(input.platform).join("/Users", user)); + } else if (input.platform !== "win32") { + homes.add(pathFor(input.platform).join("/home", user)); + } + } + } + // Drop the current process HOME from the candidate set; we want directories + // that are NOT what gh would already consult. + const processHome = env.HOME?.trim(); + if (processHome) { + homes.delete(processHome); + } + return [...homes]; +} + +function ghConfigDirForHome(home: string, platform: NodeJS.Platform): string { + // Linux and macOS both put gh's config under /.config/gh. Windows is + // not a realistic mismatch case for the bug this helper detects; we still + // return the POSIX-layout directory so the hint points at a sensible path. + return pathFor(platform).join(home, ".config", "gh"); +} + +export function detectGhConfigDirMismatch(input: GhConfigDiscoveryInput): GhConfigDiscoveryResult { + const env = input.env; + if (env.GH_CONFIG_DIR && env.GH_CONFIG_DIR.trim()) { + return { kind: "explicit-gh-config-dir-set", ghConfigDir: env.GH_CONFIG_DIR.trim() }; + } + const effective = resolveEffectiveGhConfigDir(input); + if (!effective) { + return { kind: "no-process-home" }; + } + const effectiveHosts = pathFor(input.platform).join(effective, HOSTS_FILE); + if (input.fileExists(effectiveHosts)) { + return { kind: "auth-discoverable", effectiveConfigDir: effective }; + } + const candidates = input.candidateOperatorHomes ?? defaultCandidateOperatorHomes(input); + for (const home of candidates) { + const candidateDir = ghConfigDirForHome(home, input.platform); + if (candidateDir === effective) { + continue; + } + const candidateHosts = pathFor(input.platform).join(candidateDir, HOSTS_FILE); + if (input.fileExists(candidateHosts)) { + return { + kind: "mismatch", + effectiveConfigDir: effective, + alternateConfigDir: candidateDir, + alternateHostsFile: candidateHosts, + alternateHomeHint: home, + suggestedEnvValue: candidateDir, + }; + } + } + return { kind: "no-known-auth", effectiveConfigDir: effective }; +} + +export function formatGhConfigDirMismatchHint(mismatch: GhConfigDirMismatch): string[] { + const lines: string[] = [ + "GitHub CLI auth was found at a different HOME than the one this OpenClaw process uses.", + ` Process gh config dir: ${mismatch.effectiveConfigDir}`, + ` Authenticated config: ${mismatch.alternateConfigDir} (contains ${HOSTS_FILE})`, + ]; + if (mismatch.alternateHomeHint) { + lines.push(` Authenticated HOME: ${mismatch.alternateHomeHint}`); + } + lines.push( + ` Fix: set GH_CONFIG_DIR=${mismatch.suggestedEnvValue} on the OpenClaw service environment, then restart the gateway.`, + ); + return lines; +} diff --git a/src/commands/doctor-skills.test.ts b/src/commands/doctor-skills.test.ts index d4426ff164b0..682d92eb4e33 100644 --- a/src/commands/doctor-skills.test.ts +++ b/src/commands/doctor-skills.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from "vitest"; import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js"; +import type { GhConfigDiscoveryInput } from "../agents/skills/gh-config-discovery.js"; import { createEmptyInstallChecks } from "../cli/requirements-test-fixtures.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { collectUnavailableAgentSkills, + describeGhConfigDirHintFromDiscovery, disableUnavailableSkillsInConfig, formatUnavailableSkillDoctorLines, } from "./doctor-skills.js"; @@ -87,6 +89,67 @@ describe("doctor skills", () => { expect(lines.join("\n")).toContain("openclaw doctor --fix"); }); + it("surfaces a GH_CONFIG_DIR hint when the github skill is eligible but auth lives at a different HOME", () => { + const githubSkill = createSkill({ + name: "github", + skillKey: "github", + eligible: true, + missing: { bins: [], anyBins: [], env: [], config: [], os: [] }, + }); + const discovery: GhConfigDiscoveryInput = { + platform: "linux", + env: { HOME: "/root/.openclaw/agents/main/agent/codex-home/home" }, + fileExists: (p) => p === "/root/.config/gh/hosts.yml", + }; + + const lines = describeGhConfigDirHintFromDiscovery([githubSkill], discovery); + + expect(lines.some((line) => line.includes("/root/.config/gh"))).toBe(true); + expect(lines.join("\n")).toContain("GH_CONFIG_DIR=/root/.config/gh"); + }); + + it("does not surface the GH_CONFIG_DIR hint when the github skill is missing the gh binary", () => { + const githubSkill = createSkill({ + name: "github", + skillKey: "github", + eligible: false, + missing: { bins: ["gh"], anyBins: [], env: [], config: [], os: [] }, + }); + const discovery: GhConfigDiscoveryInput = { + platform: "linux", + env: { HOME: "/agent/home" }, + fileExists: (p) => p === "/root/.config/gh/hosts.yml", + }; + + expect(describeGhConfigDirHintFromDiscovery([githubSkill], discovery)).toEqual([]); + }); + + it("does not surface the GH_CONFIG_DIR hint when GH_CONFIG_DIR is already set", () => { + const githubSkill = createSkill({ + name: "github", + skillKey: "github", + eligible: true, + missing: { bins: [], anyBins: [], env: [], config: [], os: [] }, + }); + const discovery: GhConfigDiscoveryInput = { + platform: "linux", + env: { HOME: "/agent/home", GH_CONFIG_DIR: "/etc/openclaw/gh" }, + fileExists: () => true, + }; + + expect(describeGhConfigDirHintFromDiscovery([githubSkill], discovery)).toEqual([]); + }); + + it("does not surface the GH_CONFIG_DIR hint when the github skill is not present in the report", () => { + const discovery: GhConfigDiscoveryInput = { + platform: "linux", + env: { HOME: "/agent/home" }, + fileExists: (p) => p === "/root/.config/gh/hosts.yml", + }; + + expect(describeGhConfigDirHintFromDiscovery([], discovery)).toEqual([]); + }); + it("disables unavailable skills through skills.entries without dropping existing config", () => { const config: OpenClawConfig = { skills: { diff --git a/src/commands/doctor-skills.ts b/src/commands/doctor-skills.ts index 35c21608ef7e..b365a7969eae 100644 --- a/src/commands/doctor-skills.ts +++ b/src/commands/doctor-skills.ts @@ -1,6 +1,13 @@ +import { existsSync } from "node:fs"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js"; import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; +import { + detectGhConfigDirMismatch, + formatGhConfigDirMismatchHint, + type GhConfigDiscoveryInput, + type GhConfigDiscoveryResult, +} from "../agents/skills/gh-config-discovery.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { note } from "../terminal/note.js"; @@ -43,6 +50,38 @@ function formatInstallHints(skill: SkillStatusEntry): string[] { return skill.install.slice(0, 2).map((entry) => ` install option: ${entry.label}`); } +function defaultGhConfigDiscoveryInput(): GhConfigDiscoveryInput { + return { + platform: process.platform, + env: process.env as GhConfigDiscoveryInput["env"], + fileExists: (absolutePath) => existsSync(absolutePath), + }; +} + +export function describeGhConfigDirHint(skills: SkillStatusEntry[]): string[] { + return describeGhConfigDirHintFromDiscovery(skills, defaultGhConfigDiscoveryInput()); +} + +export function describeGhConfigDirHintFromDiscovery( + skills: SkillStatusEntry[], + discoveryInput: GhConfigDiscoveryInput, +): string[] { + const githubSkill = skills.find((skill) => skill.name === "github"); + if (!githubSkill) { + return []; + } + // The github skill only requires the `gh` binary; if it is not installed we + // do not surface a config-dir hint (the bin install hint covers it). + if (githubSkill.missing.bins.includes("gh")) { + return []; + } + const result: GhConfigDiscoveryResult = detectGhConfigDirMismatch(discoveryInput); + if (result.kind !== "mismatch") { + return []; + } + return formatGhConfigDirMismatchHint(result); +} + export function formatUnavailableSkillDoctorLines(skills: SkillStatusEntry[]): string[] { const lines: string[] = [ "Some skills are allowed for this agent but are not usable in the current runtime environment.", @@ -91,6 +130,10 @@ export async function maybeRepairSkillReadiness(params: { config: params.cfg, agentId, }); + const githubHint = describeGhConfigDirHint(report.skills); + if (githubHint.length > 0) { + note(githubHint.join("\n"), "GitHub CLI"); + } const unavailable = collectUnavailableAgentSkills(report); if (unavailable.length === 0) { return params.cfg;