> = vec![Box::new(adapter())];
+
+ hk_core::manager::toggle_extension_with_adapters(&store, &adapters, &row.id, true).unwrap();
+ assert_eq!(std::fs::read_to_string(&project_cfg).unwrap(), original);
+ let user = std::fs::read_to_string(dir.path().join(".grok/config.toml")).unwrap();
+ assert!(user.contains("enabled"));
+ assert!(user.contains("team-tool"));
+ let enabled = hk_core::scanner::scan_project_extensions(&adapter(), "repo", &project)
+ .into_iter()
+ .find(|e| e.name == "team-tool")
+ .unwrap();
+ assert!(enabled.enabled);
+}
diff --git a/media/pr-grok-build/agent-detail.png b/media/pr-grok-build/agent-detail.png
new file mode 100644
index 0000000..077e386
Binary files /dev/null and b/media/pr-grok-build/agent-detail.png differ
diff --git a/media/pr-grok-build/extensions-filter.png b/media/pr-grok-build/extensions-filter.png
new file mode 100644
index 0000000..36f9955
Binary files /dev/null and b/media/pr-grok-build/extensions-filter.png differ
diff --git a/media/pr-grok-build/overview.png b/media/pr-grok-build/overview.png
new file mode 100644
index 0000000..92e3668
Binary files /dev/null and b/media/pr-grok-build/overview.png differ
diff --git a/src/components/extensions/extension-detail.tsx b/src/components/extensions/extension-detail.tsx
index 0a9e2ac..bbb2850 100644
--- a/src/components/extensions/extension-detail.tsx
+++ b/src/components/extensions/extension-detail.tsx
@@ -343,6 +343,22 @@ export function ExtensionDetail() {
)}
+ {/* Grok folder-trust gate — project-level hooks, MCP/LSP servers,
+ * plugins, and permission rules load only after the folder is
+ * trusted inside Grok (source-verified: folder_trust.rs decide()).
+ * Skills are NOT trust-gated, so this stays off skill rows. Keyed
+ * on the instances so it also shows under the "All" scope filter. */}
+ {["hook", "mcp", "plugin"].includes(group.kind) &&
+ group.agents.includes("grok") &&
+ group.instances.some(
+ (i) => i.agents.includes("grok") && i.scope.type === "project",
+ ) && (
+
+
+ {t("detail.grokFolderTrustNote")}
+
+ )}
+
{/* Silent skill-drop warning — the `skill-invocation-key-case` audit
* rule means DeepSeek Harness discards this ENTIRE skill (functional
* breakage, not just a score deduction), so it warrants an inline
diff --git a/src/components/extensions/extension-filters.tsx b/src/components/extensions/extension-filters.tsx
index 9a6974d..8a1b991 100644
--- a/src/components/extensions/extension-filters.tsx
+++ b/src/components/extensions/extension-filters.tsx
@@ -47,6 +47,7 @@ const AGENT_FILTER_COLORS: Record = {
kiro: "bg-agent-kiro/10 text-agent-kiro border-agent-kiro/30",
omp: "bg-agent-omp/10 text-agent-omp border-agent-omp/30",
dsh: "bg-agent-dsh/10 text-agent-dsh border-agent-dsh/30",
+ grok: "bg-agent-grok/10 text-agent-grok border-agent-grok/30",
};
export function ExtensionFilters() {
diff --git a/src/components/onboarding/onboarding.tsx b/src/components/onboarding/onboarding.tsx
index c61b726..3aa3c7d 100644
--- a/src/components/onboarding/onboarding.tsx
+++ b/src/components/onboarding/onboarding.tsx
@@ -267,6 +267,7 @@ const FLOAT_DELAYS: Record<(typeof AGENT_ORDER)[number], number> = {
kiro: 1.4,
omp: 1.0,
dsh: 1.7,
+ grok: 2.1,
};
const SCATTER_POSITIONS: Record<
(typeof AGENT_ORDER)[number],
@@ -284,6 +285,7 @@ const SCATTER_POSITIONS: Record<
kiro: { x: 40, y: -126, r: -10 },
omp: { x: -200, y: 60, r: 22 },
dsh: { x: 90, y: 135, r: 7 },
+ grok: { x: -30, y: 145, r: -12 },
};
function HandAnnotation({
diff --git a/src/components/shared/agent-card.tsx b/src/components/shared/agent-card.tsx
index 1b6d082..c6a12f6 100644
--- a/src/components/shared/agent-card.tsx
+++ b/src/components/shared/agent-card.tsx
@@ -14,6 +14,7 @@ const CLICK_DURATIONS: Partial> = {
kiro: 1100,
omp: 900,
dsh: 1200,
+ grok: 900,
};
export function AgentCard({ agent }: AgentCardProps) {
diff --git a/src/components/shared/agent-mascot/agent-mascot.tsx b/src/components/shared/agent-mascot/agent-mascot.tsx
index 8685431..f36ab4d 100644
--- a/src/components/shared/agent-mascot/agent-mascot.tsx
+++ b/src/components/shared/agent-mascot/agent-mascot.tsx
@@ -7,6 +7,7 @@ import { CursorMascot } from "./cursor-mascot";
import { DevinMascot } from "./devin-mascot";
import { DshMascot } from "./dsh-mascot";
import { FallbackMascot } from "./fallback-mascot";
+import { GrokMascot } from "./grok-mascot";
import { GeminiMascot } from "./gemini-mascot";
import { HermesMascot } from "./hermes-mascot";
import { KiroMascot } from "./kiro-mascot";
@@ -82,6 +83,11 @@ const MASCOT_MAP: Record<
className: "mascot-dsh",
scale: 0.95,
},
+ grok: {
+ component: GrokMascot,
+ className: "mascot-grok",
+ scale: 0.95,
+ },
};
export function AgentMascot({
diff --git a/src/components/shared/agent-mascot/grok-mascot.tsx b/src/components/shared/agent-mascot/grok-mascot.tsx
new file mode 100644
index 0000000..c308225
--- /dev/null
+++ b/src/components/shared/agent-mascot/grok-mascot.tsx
@@ -0,0 +1,30 @@
+// Grok Build mascot — LobeHub Icons `grok` mark
+// https://unpkg.com/@lobehub/icons-static-svg@latest/icons/grok.svg
+// Brand color on icons.lobehub.com is #000; we paint with --mascot-icon-color
+// so the silhouette stays readable on light and dark cards.
+// Motion lives on the .mascot-grok wrapper in mascot.css.
+
+interface MascotSvgProps {
+ size: number;
+}
+
+const GROK_MARK_D =
+ "M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815";
+
+export function GrokMascot({ size }: MascotSvgProps) {
+ return (
+
+ );
+}
diff --git a/src/components/shared/agent-mascot/mascot.css b/src/components/shared/agent-mascot/mascot.css
index 14861c8..7fb8550 100644
--- a/src/components/shared/agent-mascot/mascot.css
+++ b/src/components/shared/agent-mascot/mascot.css
@@ -1460,6 +1460,28 @@
.mascot-dsh.is-clicked .dsh-drop-2 { --fling-x: 0px; }
.mascot-dsh.is-clicked .dsh-drop-3 { --fling-x: 3.5px; animation-delay: 0.3s; }
+/* Grok: LobeHub mark. Quiet hover pulse; click is a brief scale flash. */
+.mascot-grok .grok-mark {
+ transform-box: fill-box;
+ transform-origin: center;
+}
+.mascot-grok.is-animated .grok-mark {
+ animation: grok-pulse 2.4s ease-in-out infinite;
+}
+.mascot-grok.is-clicked .grok-svg {
+ animation: grok-flash 0.9s ease-out;
+}
+
+@keyframes grok-pulse {
+ 0%, 100% { transform: scale(1); opacity: 0.92; }
+ 50% { transform: scale(1.06); opacity: 1; }
+}
+@keyframes grok-flash {
+ 0% { transform: scale(1); opacity: 0.9; }
+ 35% { transform: scale(1.16); opacity: 1; }
+ 100% { transform: scale(1); opacity: 0.92; }
+}
+
/* Sine-like bob: per-segment timing functions so velocity peaks crossing
the midline and only reaches zero at the extremes — a single easing for
the whole animation would stall at every keyframe boundary instead. */
@@ -1807,6 +1829,8 @@
.mascot-omp.is-animated *,
.mascot-dsh.is-animated,
.mascot-dsh.is-animated *,
+ .mascot-grok.is-animated,
+ .mascot-grok.is-animated *,
.mascot-fallback.is-animated *,
.mascot-claude.is-clicked,
.mascot-claude.is-clicked *,
@@ -1830,6 +1854,8 @@
.mascot-omp.is-clicked *,
.mascot-dsh.is-clicked,
.mascot-dsh.is-clicked *,
+ .mascot-grok.is-clicked,
+ .mascot-grok.is-clicked *,
.mascot-fallback.is-clicked * {
animation: none;
transition: none;
diff --git a/src/index.css b/src/index.css
index d1784e6..3b8d4be 100644
--- a/src/index.css
+++ b/src/index.css
@@ -100,6 +100,7 @@
--agent-kiro: oklch(0.58 0.15 305);
--agent-omp: oklch(0.58 0.13 160);
--agent-dsh: oklch(0.55 0.15 270);
+ --agent-grok: oklch(0.38 0.04 75);
--toast-success-bg: oklch(0.94 0.06 155);
--toast-success-border: oklch(0.72 0.16 155);
--toast-success-text: oklch(0.4 0.1 155);
@@ -206,6 +207,7 @@
--agent-kiro: oklch(0.78 0.13 305);
--agent-omp: oklch(0.78 0.11 160);
--agent-dsh: oklch(0.75 0.13 270);
+ --agent-grok: oklch(0.86 0.04 75);
--toast-success-bg: oklch(0.23 0.04 155);
--toast-success-border: oklch(0.55 0.14 155);
--toast-success-text: oklch(0.78 0.12 155);
@@ -306,6 +308,7 @@
--agent-kiro: oklch(0.58 0.15 305);
--agent-omp: oklch(0.58 0.13 160);
--agent-dsh: oklch(0.55 0.15 270);
+ --agent-grok: oklch(0.38 0.04 75);
--toast-success-bg: oklch(0.94 0.06 155);
--toast-success-border: oklch(0.65 0.15 155);
--toast-success-text: oklch(0.4 0.1 155);
@@ -400,6 +403,7 @@
--agent-kiro: oklch(0.78 0.13 305);
--agent-omp: oklch(0.78 0.11 160);
--agent-dsh: oklch(0.75 0.13 270);
+ --agent-grok: oklch(0.86 0.04 75);
--toast-success-bg: oklch(0.26 0.04 155);
--toast-success-border: oklch(0.55 0.14 155);
--toast-success-text: oklch(0.78 0.12 155);
@@ -506,6 +510,7 @@
--agent-kiro: oklch(0.58 0.15 305);
--agent-omp: oklch(0.58 0.13 160);
--agent-dsh: oklch(0.55 0.15 270);
+ --agent-grok: oklch(0.38 0.04 75);
--toast-success-bg: oklch(0.94 0.06 155);
--toast-success-border: oklch(0.72 0.16 155);
--toast-success-text: oklch(0.4 0.1 155);
@@ -606,6 +611,7 @@
--agent-kiro: oklch(0.78 0.13 305);
--agent-omp: oklch(0.78 0.11 160);
--agent-dsh: oklch(0.75 0.13 270);
+ --agent-grok: oklch(0.86 0.04 75);
--toast-success-bg: oklch(0.23 0.04 155);
--toast-success-border: oklch(0.55 0.14 155);
--toast-success-text: oklch(0.78 0.12 155);
@@ -684,6 +690,7 @@ html.dark[data-theme="tiesen"][data-web="true"] {
--color-agent-kiro: var(--agent-kiro);
--color-agent-omp: var(--agent-omp);
--color-agent-dsh: var(--agent-dsh);
+ --color-agent-grok: var(--agent-grok);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
diff --git a/src/lib/__tests__/agent-capabilities.test.ts b/src/lib/__tests__/agent-capabilities.test.ts
index b004da3..8782aeb 100644
--- a/src/lib/__tests__/agent-capabilities.test.ts
+++ b/src/lib/__tests__/agent-capabilities.test.ts
@@ -42,6 +42,12 @@ const WINDSURF = agent("windsurf", {
hooks_supported: true,
global_hook_install: true,
});
+const GROK = agent("grok", {
+ project_install: { skill: true, mcp: true, hook: true, cli: true },
+ hooks_supported: true,
+ global_hook_install: true,
+ mcp_remote: { http: true, sse: true },
+});
describe("canInstallAtScope", () => {
it("returns true for any agent/kind at global and all scopes", () => {
@@ -58,6 +64,9 @@ describe("canInstallAtScope", () => {
expect(canInstallAtScope(WINDSURF, "skill", PROJECT)).toBe(true);
// Windsurf MCP is global-only upstream.
expect(canInstallAtScope(WINDSURF, "mcp", PROJECT)).toBe(false);
+ expect(canInstallAtScope(GROK, "skill", PROJECT)).toBe(true);
+ expect(canInstallAtScope(GROK, "mcp", PROJECT)).toBe(true);
+ expect(canInstallAtScope(GROK, "hook", PROJECT)).toBe(true);
});
it("returns false at project scope for Hermes (global-only, hermes-agent#4667)", () => {
@@ -98,6 +107,8 @@ describe("canReceiveMcpTransport", () => {
// Codex speaks Streamable HTTP only.
expect(canReceiveMcpTransport(codex, "http")).toBe(true);
expect(canReceiveMcpTransport(codex, "sse")).toBe(false);
+ expect(canReceiveMcpTransport(GROK, "http")).toBe(true);
+ expect(canReceiveMcpTransport(GROK, "sse")).toBe(true);
});
it("gates remote transports off when capabilities are absent (old backend / unknown agent)", () => {
diff --git a/src/lib/__tests__/types.test.ts b/src/lib/__tests__/types.test.ts
index fcb1ef7..7b746d1 100644
--- a/src/lib/__tests__/types.test.ts
+++ b/src/lib/__tests__/types.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import type { Extension, GroupedExtension } from "../types";
import {
+ AGENT_ORDER,
agentDisplayName,
extensionGroupKey,
formatRelativeTime,
@@ -332,6 +333,13 @@ describe("sortAgentNames", () => {
});
});
+describe("AGENT_ORDER", () => {
+ it("lists 13 agents with grok last", () => {
+ expect(AGENT_ORDER).toHaveLength(13);
+ expect(AGENT_ORDER[AGENT_ORDER.length - 1]).toBe("grok");
+ });
+});
+
describe("agentDisplayName", () => {
it("returns display name for known agents", () => {
expect(agentDisplayName("claude")).toBe("Claude Code");
@@ -342,6 +350,7 @@ describe("agentDisplayName", () => {
expect(agentDisplayName("kiro")).toBe("Kiro");
expect(agentDisplayName("omp")).toBe("Oh My Pi");
expect(agentDisplayName("dsh")).toBe("DeepSeek");
+ expect(agentDisplayName("grok")).toBe("Grok Build");
});
it("capitalizes first letter for unknown agents", () => {
diff --git a/src/lib/i18n/locales/en/extensions.json b/src/lib/i18n/locales/en/extensions.json
index fbfbdaf..feae85c 100644
--- a/src/lib/i18n/locales/en/extensions.json
+++ b/src/lib/i18n/locales/en/extensions.json
@@ -73,6 +73,7 @@
"partOfSkillPrefix": "This skill is part of ",
"codexHooksWarning": "Codex hooks must be trusted via `codex /hooks` before they run.",
"dshMcpPluginNote": "In DeepSeek Harness this server appears in Settings → Plugins as an 'mcp-client' plugin card (search this server's name there to locate it).",
+ "grokFolderTrustNote": "To activate: open a terminal in this project folder, run `grok`, and answer yes when it asks whether you trust the folder.",
"skillInvocationKeyWarning": "This skill's frontmatter uses a camelCase invocation key — DeepSeek Harness silently drops the whole skill. Rename to the kebab-case form (see Audit for details); re-run the audit after fixing and this warning disappears.",
"enabled": "Enabled",
"disabled": "Disabled",
diff --git a/src/lib/i18n/locales/zh-TW/extensions.json b/src/lib/i18n/locales/zh-TW/extensions.json
index b6a93d3..87fc36b 100644
--- a/src/lib/i18n/locales/zh-TW/extensions.json
+++ b/src/lib/i18n/locales/zh-TW/extensions.json
@@ -73,6 +73,7 @@
"partOfSkillPrefix": "這個 skill 屬於 ",
"codexHooksWarning": "Codex hook 必須先透過 `codex /hooks` 信任後才能執行。",
"dshMcpPluginNote": "在 DeepSeek Harness 中,此 server 顯示在 Settings → Plugins 裡,是名為 mcp-client 的 plugin 卡片(在其中搜尋此 server 名稱可定位)。",
+ "grokFolderTrustNote": "啟用方法:在該專案目錄下開啟終端機執行 `grok`,當它詢問是否信任該資料夾時選擇「是」。",
"skillInvocationKeyWarning": "此 skill 的 frontmatter 使用了 camelCase 呼叫鍵——DeepSeek Harness 會靜默捨棄整個 skill。請改為 kebab-case 寫法(詳見稽核);修正後重新執行稽核,此提示會消失。",
"enabled": "已啟用",
"disabled": "已停用",
diff --git a/src/lib/i18n/locales/zh/extensions.json b/src/lib/i18n/locales/zh/extensions.json
index aa2432a..93b5d22 100644
--- a/src/lib/i18n/locales/zh/extensions.json
+++ b/src/lib/i18n/locales/zh/extensions.json
@@ -73,6 +73,7 @@
"partOfSkillPrefix": "此 skill 属于 ",
"codexHooksWarning": "Codex hook 必须先通过 `codex /hooks` 信任后才能运行。",
"dshMcpPluginNote": "在 DeepSeek Harness 中,此 server 显示在 Settings → Plugins 里,是名为 mcp-client 的插件卡片(在其中搜索此 server 名称可定位)。",
+ "grokFolderTrustNote": "启用方法:在该项目目录下打开终端运行 `grok`,当它询问是否信任该文件夹时选择「是」。",
"skillInvocationKeyWarning": "此 skill 的 frontmatter 使用了 camelCase 调用键——DeepSeek Harness 会静默丢弃整个 skill。请改为 kebab-case 写法(详见审计);修复后重新运行审计,此提示会消失。",
"enabled": "已启用",
"disabled": "已禁用",
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 302ad72..f75635d 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -429,6 +429,7 @@ export const AGENT_ORDER = [
"kiro",
"omp",
"dsh",
+ "grok",
] as const;
/** Sort an array of agents (or agent-like objects with a `name` field) by a given order. */
@@ -456,6 +457,7 @@ const AGENT_DISPLAY_NAMES: Record = {
kiro: "Kiro",
omp: "Oh My Pi",
dsh: "DeepSeek",
+ grok: "Grok Build",
};
/** Get the display name for an agent (e.g. "claude" → "Claude Code"). */