Skip to content

feat: Grok Build (grok) agent support — detection, extensions, config scan - #125

Merged
RealZST merged 12 commits into
RealZST:mainfrom
linhai0872:feat/grok-build-agent-support
Aug 28, 2026
Merged

feat: Grok Build (grok) agent support — detection, extensions, config scan#125
RealZST merged 12 commits into
RealZST:mainfrom
linhai0872:feat/grok-build-agent-support

Conversation

@linhai0872

@linhai0872 linhai0872 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Adds Grok Build (grok) as the 13th supported agent. Closes #124.

Source-backed against xai-org/grok-build @ c2ad97f. Home is $GROK_HOME verbatim when non-empty, otherwise ~/.grok. Detection never creates the directory.

What's included

Detection & layout

  • User home: $GROK_HOME / ~/.grok
  • Project marker: .grok
  • Writes stay on native Grok paths (.grok/skills, .grok/config.toml, .grok/hooks/, .grok/plugins/)
  • .agents/skills is a shared read-only alias only — Codex/Gemini keep ownership
  • .claude / .cursor contents are not attributed to Grok
  • auth.json and mcp_credentials.json are never scanned

Skills / MCP / Hooks / Plugins

  • Skills: $GROK_HOME/skills and project .grok/skills
  • MCP: TOML [mcp_servers.<name>] with headers (not Codex http_headers); type = "sse" is SSE, otherwise URL is HTTP
  • Native MCP toggle: personal disable writes user disabled_mcp_servers (+ user entry enabled when present) and does not rewrite the shared project file. Enable clears the user list and unsticks a winning project enabled = false
  • Hooks: Claude-like JSON; only command handlers are scanned/managed. HTTP handlers still occupy Grok's hook index so spec names stay aligned with upstream (global/session-start:pre_tool_use[0].hooks[1])
  • Hook toggle: $GROK_HOME/disabled-hooks using the real spec.name
  • Hook prefix: only $GROK_HOME/hooks is global/; a project under a custom $GROK_HOME stays project/
  • SubagentEnd uses upstream's subagent_stop display token and shares one group-index sequence
  • Plugins: $GROK_HOME/plugins, installed-plugins, and project .grok/plugins; stable id {scope}/{hex8}/{name}; toggle via [plugins].enabled / [plugins].disabled (user + project lists merged, disabled wins); default disabled; delete removes the directory and both lists. Project plugin toggle writes the user lists only

Project scope & configs

  • Project MCP/hooks/skills/plugins/rules/settings/subagents/memory/commands
  • Settings: config.toml, pager.toml only
  • Memory: one layer under $GROK_HOME/memory plus project .grok/memory/*.md; nested memory/index/ is not claimed

Frontend & docs

  • AGENT_ORDER / display name Grok Build, filter color, onboarding scatter, reduced-motion mascot
  • Mascot is the LobeHub Icons grok.svg mark (currentColor + --mascot-icon-color)
  • README / README.zh-CN: 12 → 13, capability matrix, CLI example

Known limitation

HTTP hooks are outside HarnessKit's current HookEntry command model. This PR only scans and toggles command hooks. HTTP handlers are skipped as entries but still counted in Grok's hook index so disable names stay correct.

Verification

  • npm test: 307 passed
  • npm run build: tsc + vite succeeded
  • cargo test --workspace: passed (after frontend dist/ build, matching CI)
  • cargo test -p hk-core --lib grok (25) and cargo test -p hk-core --test toggle_integration grok (5) passed
  • Isolated $GROK_HOME smoke (grok 1.0.5 + hk):
    • hk status1 detected (grok)
    • hk list --agent grok → skill / stdio+HTTP MCP / plugin / command hook
    • hk disable / hk enable echo → user disabled_mcp_servers round-trip; theme and unrelated MCP headers kept
    • plugin toggle writes stable id user/<hex8>/demo-plugin
    • grok mcp list and grok inspect read the same home (stdio echo, HTTP linear, command hook, demo skill). Auth files were not copied or printed.

Screenshots

Overview

Agent detail

Extensions filter

Native Skills, MCP, command hooks, plugins, and project scope, with
personal MCP disable that never rewrites shared project files.

Closes RealZST#124
Replace the homemade spark with the official grok.svg path, scan project
.grok/plugins, keep SubagentEnd spec names unique, and only treat
$GROK_HOME/hooks as global.
materialinhai and others added 9 commits August 26, 2026 10:40
Match xai-grok-hooks build_specs exactly, verified against
xai-org/grok-build@c2ad97f:

- [i] is keyed per event ENUM VARIANT, not per display token.
  SubagentStop and SubagentEnd are separate variants whose spec names
  collide on the shared subagent_stop display token (upstream
  discovery.rs asserts exactly this); the previous merged sequence
  emitted subagent_stop[1], a name Grok never generates, so disable
  was a silent no-op.
- Dedup specs on (canonical event, raw command, matcher), first wins,
  with SubagentEnd folding into SubagentStop — a hook hedged under
  both spellings runs once upstream and is now one row here.
- Handler errors are split the way upstream splits them. A missing
  `type` (or a wrong JSON type on any field) is a serde error on
  RawHandler's plain `String` / `Option<String>` fields, and
  GroupErrorPolicy::Fail then yields zero hooks for the whole file. An
  unknown `type` deserializes fine and errors inside build_specs' loop
  (HookError::UnsupportedHandlerType), as does a `command` handler with
  no `command` (HookError::InvalidConfig) — the file's other hooks still
  register, and the rejected handler still consumes its `[j]` index, so
  its siblings keep the spec names Grok generates. Treating every bad
  handler as a whole-file failure would have hidden hooks Grok runs.

Known limitation, deliberately not modeled: when one file spells a
single event under two alias keys (Cursor's beforeShellExecution and
beforeReadFile both map to PreToolUse), upstream merges the groups in
std-HashMap order, so our index may address the sibling entry. The
shape is rare, upstream itself is unstable there, and HK reads the
real state back from disabled-hooks on every scan.
…ugin ids

Upstream hashes the dunce-canonical plugin root (plugins/discovery.rs)
and bans std::fs::canonicalize repo-wide via clippy disallowed-methods,
because std returns \\?\ verbatim paths on Windows — those hash to a
different hex8 than the id Grok writes to its [plugins] lists, so every
toggle would write an id Grok does not recognize. dunce delegates
directly to std on Unix, so ids on macOS/Linux are unchanged. The crate
is already in the lockfile (via tauri) at the same version upstream
pins; the canonicalize-failure fallback stays, as upstream only hits it
in a delete race and skipping would hide the plugin entirely.
…ugin identity, settings, agent-memory

Four corrections, each source-verified against xai-org/grok-build@c2ad97f:

- MCP transport: SSE is type = "sse" (ASCII case-insensitive) OR a
  byte-exact "/sse" url suffix, and the suffix wins even over an
  explicit type = "http" (config-types mcp.rs to_acp_mcp_server).
- Plugins: convention components also include commands/ and .lsp.json;
  dirnames sanitize like upstream name_from_dirname (lowercase, other
  chars to '-', trim, >64 rejects); a manifest that fails to parse or
  carries an invalid name rejects the directory outright — Grok never
  falls back to the dirname there. The three old helpers collapse into
  one grok_plugin_identity entry point.
- Settings: add sandbox.toml and lsp.json (real user-tier config).
  managed_config.toml and requirements.toml stay excluded on purpose:
  both are server-synced artifacts, atomically overwritten per fetch
  and deleted on logout — listing them invites edits that vanish.
- Memory: <project>/.grok/memory does not exist upstream; project
  memory is per-subagent .grok/agent-memory{,-local}/<agent>/MEMORY.md.
  The old concrete pattern also made the Kit memory sync write into a
  directory Grok never reads; all-glob patterns mean no write target,
  which is correct — HK cannot invent a subagent name. Global side
  keeps memory/MEMORY.md and adds agent-memory/*/MEMORY.md; the flat
  memory/*.md glob is gone (Grok never reads those at startup).

Also in this pass, since it is the same question of what HK models about
Grok's files: `scan_agent_configs` now keeps one row per (real path,
lowercased filename) within a category. Grok lists both AGENTS.md and
Agents.md — one file under two spellings on a case-insensitive
filesystem — which surfaced as a duplicate row. Pairing the canonical
path with the spelling confines the suppression to that artifact:
CLAUDE.md symlinked to AGENTS.md is two names the user deliberately has
(the recommended AGENTS.md migration) and keeps both rows, matching how
the rest of HK surfaces symlinks rather than merging them away.
Grok finds SKILL.md up to six segments below each skills root
(walk_for_skill_md, depth cap 5), including skills nested inside other
skills — team-folder layouts like skills/team/infra/SKILL.md are real
skills that the flat scanner missed entirely.

New AgentAdapter::expand_skill_roots hook (default: identity, so the
other twelve adapters are unchanged) returns the root plus every nested
parent directory that directly holds skill subdirs; the scanner's skill
scan and lookup paths (scan, find-by-id, locations, content) expand
through it so nested skills are not just listed but also toggleable and
deletable. Install targets still resolve through the canonical first
root only.

Recursion alone would be wrong, though: scan_skill_dir also treats any
loose *.md at the top of a scanned root as a standalone skill, and
expanding roots aims that rule at directories chosen precisely because
they hold other things. AgentAdapter::standalone_md_skills (default
true, so the other twelve adapters keep today's behaviour) turns it off
for Grok, which is what upstream does anyway — walk_for_skill_md filters
read_dir to directories before it looks at a filename, and the only name
it accepts is SKILL.md inside one, verified against grok 1.0.5. A bare
notes.md, a grouping folder's README.md and a skill's own reference.md
are therefore payload, not skills; listing one would offer a row Grok
cannot load and whose Delete removes an ordinary file. The one place a
loose .md is a first-class Grok entity is commands/, a different root
that HK already lists as a config file. The rule is applied on the
lookup paths too — find_skill_by_id (which delete_extension remove_file()s
the result of) and find_skill_content classify entries with their own
copy of it, so gating only the scan would have left the same file
reachable through a different door.

scan_skill_dir also gains a deterministic sort — read_dir order is
filesystem-dependent, so same-name collisions now resolve stably.

Both roots, not one: `~/.agents/skills` joins `~/.grok/skills` in
skill_dirs. Upstream's CompatConfig::skill_config_dirs hard-codes
`.grok` and `.agents` and gates only `.claude`/`.cursor` behind their
compat cells, and the global pass adds `~/.agents` unconditionally
beside grok_home — so HK was modelling one of the two roots Grok always
reads, and a skill installed there showed as belonging to
Codex/Cursor/OpenCode/dsh but not to Grok, which runs it. The recursive
walk applies to both, as upstream's does. `.claude/skills` and
`.cursor/skills` stay with their own adapters, following the rule omp
already states for the same shared dirs; the project side already
declared `.agents/skills` as a read dir, only the global list was short.
…es on deploy

Upstream's StreamableHttp.url accepts urlTemplate / url_template as
deserialize-only aliases (config-types mcp.rs). HK read only `url`, so
an alias-keyed remote server scanned as stdio with an empty command —
and redeploying that reading through install-to-agent wrote
`command = ""` beside the template, which upstream drops as a blank
transport: the remote server was destroyed. The read side now accepts
all three spellings (two at once is a duplicate-field error upstream,
so there is no precedence to model) and classifies command-first,
matching the untagged enum's Stdio-first order.

The deploy path now removes urlTemplate/url_template in both branches:
writing `url` beside a surviving alias would be that same duplicate-
field error and Grok would drop the whole entry.
Two halves of the same question — is this plugin actually on? — each
source-verified against xai-org/grok-build@c2ad97f.

Project tier (config/mod.rs resolve_effective_plugins_config): upstream
merges a project config's [plugins].disabled (always) and .paths (only
once the folder is trusted), never its .enabled. A repo cannot
self-enable its own plugins — deliberate, so a malicious repo can't
bypass the project-plugin auto-disable. HK merged the project enabled
list too, showing a plugin as enabled that Grok keeps disabled. Now
only the user tier's enabled list counts (id-or-name matching is
upstream's own rule and stays; the default-disabled rule itself lives
in plugins/registry.rs).

Deleting a project plugin now also cleans the project .grok/config.toml
lists: the project disabled list merges ungated, so a stale entry would
silently disable a future same-named plugin. Name-based removal stays —
grok plugin enable/disable writes bare names, so they are live entries.

Claude compat (merge_claude_enabled_plugins): unless /import-claude has
set [claude_compat] imported in config.toml, Grok live-merges
~/.claude/settings.json enabledPlugins into its plugin lists BEFORE the
scope auto-disable, so a Claude-enabled name switches a plugin on —
user and project scope alike. HK computed enablement from Grok-native
lists only, showing such plugins as disabled while Grok runs them.

Semantics mirrored exactly: name@marketplace suffix stripped, false
wins per name within the file, and a name already in either native list
is never moved to the opposite one — native config wins, so a natively-
enabled name survives a Claude false. User-tier settings.json only:
upstream deliberately refuses project .claude/settings.json (a repo
must not self-enable hooks) and simply never reads settings.local.json
for this merge. The lists resolve once per scanned plugin directory,
not per plugin (read_plugins walks two global dirs, so a full global
scan resolves them twice).

Known boundaries, unchanged: HK does not scan ~/.claude/plugins dirs
for grok (shared-directory ownership stays with the Claude adapter),
and plugins reached via [plugins].paths outside ~/.grok/plugins
(ConfigPath scope, auto-enabled upstream) are not discovered either.
Grok loads project-level hooks, MCP/LSP servers, plugins, and
permission rules only after the folder is trusted inside Grok (first-
open prompt or /hooks-trust; grant keyed on the repository root,
inherited by subdirectories). HK shows those rows as active regardless,
so an install that silently does nothing looks like a success. Surface
the gate as an inline note (en/zh/zh-TW), following the kiro/dsh note
pattern. Skills are deliberately excluded — they are not trust-gated
upstream.
Grok's saved Rhai workflows ($GROK_HOME/workflows, .grok/workflows —
siblings of commands/ in upstream's watcher), personas, and roles
(SubagentPersona/SubagentRole TOML, one per file) are user-editable
config Grok loads; HK's agent dashboard listed only commands/*.md and
agents/*.md. The config-file pipeline is format-agnostic (stat + raw
30-line preview; Codex subagents are already TOML there), so these are
pure listing additions. bundled/{workflows,personas,roles} stays
excluded as vendor content rather than user config: it is the
GCS-synced builtin bundle, and while a hand edit there does survive
(the bundle is checksum-tracked and never overwrites a modified file),
a user or project file still cannot shadow a compiled-in name. Workflow RUN state lives under the session dir
and is untouched by these non-recursive globs.
@RealZST
RealZST merged commit 42c749e into RealZST:main Aug 28, 2026
3 checks passed
@RealZST

RealZST commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Thanks for adding Grok! I checked against the upstream mirror (xai-org/grok-build@c2ad97f), tested on a real grok 1.0.5 install, and pushed eight commits on top of yours:

  • fa58456: Fix hook spec names. Upstream keys [i] per event enum variant, and SubagentStop / SubagentEnd are two variants sharing the display token subagent_stop (event.rs), so we wrote subagent_stop[1] — a name Grok never generates, and disabling the hook did nothing. Also splits handler errors like upstream: a missing type drops the whole file, an unknown one only drops that handler.

  • 1bb5017: Read urlTemplate / url_template. They're deserialize-only aliases of url (mcp.rs), and HK read only url, so an alias-keyed remote server scanned as stdio — redeploying it wrote command = "", which Grok drops, and the server was gone.

  • 70558ba: Fix plugin enable state. A project config never contributes [plugins].enabled (config/mod.rs), and Grok live-merges Claude's enabledPlugins unless /import-claude has run (agent/config.rs). HK showed plugins as on that Grok keeps off, and off that Grok runs.

  • 24e7bcd: Hash plugin roots with dunce. Upstream bans std::fs::canonicalize because it returns \\?\ paths on Windows (clippy.toml), which hash to a different id than the one Grok writes — so every plugin toggle on Windows did nothing. Unix is unchanged.

  • bff33bf: Find nested skills. Grok walks each skills root recursively for directories containing SKILL.md (08-skills.md), so skills/team/infra/ was invisible. Same doc: a loose *.md is never a skill, and ~/.agents/skills is a second root Grok always reads.

  • 85e8ec7: Four surface fixes — a /sse url suffix means SSE even with type = "http" (mcp.rs); plugin dirs also count commands/ and .lsp.json; added sandbox.toml and lsp.json to settings, while managed_config.toml stays out since it's server-synced and overwritten per fetch; project memory is .grok/agent-memory/<agent>/MEMORY.md, not .grok/memory.

  • 1a4a4d2: Add a folder-trust note. Project hooks, MCP/LSP servers and plugins are silently skipped until the folder is trusted in Grok (10-hooks.md), so a project-scope install looked like it worked when it didn't.

  • d66ccf7: List saved workflows, personas and roles in the agent dashboard, next to commands/ and agents/. bundled/ stays out — that's the GCS-synced builtin bundle, not user config.

Thanks again! Merged

RealZST added a commit that referenced this pull request Aug 28, 2026
* chore: drop the Grok PR screenshots from media/

The three PNGs under media/pr-grok-build/ only ever illustrated PR #125.
Nothing in the repo references them, and that PR's body points at
raw.githubusercontent.com on the contributor's fork rather than at this
repository, so removing them here leaves the thread's images intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015weU8hXkgz73A96UcHL7oS

* chore(mascot): remove the dead cursor shadow CSS

.shadow-el has no element behind it — cursor-mascot.tsx renders only
.rocker and .icon-svg. The four rules and the two keyframe blocks that
drove it were left behind when the shadow was dropped from the markup, so
they have been styling nothing since.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015weU8hXkgz73A96UcHL7oS

* feat(mascot): rework grok's hover and click motion

Hover no longer scales the whole mark. The glyph reads as a star, so it
holds still while a planet crosses it on a drawn elliptical trajectory
whose dashes flow along the path. The planet is a grey four-point flare
inside a halo, scaled and faded along the orbit so the near point is
large and bright and the far one small and dim — grey rather than the
mark's own colour so it reads as a second, lesser body rather than a chip
off the star.

Click reads the same glyph the other way, as the spinning top it also
looks like. It crouches, hops, and is stood upright 45deg into the ascent
— the angle the line through its two spikes already sits at — then turns
about the vertical axis as it lands and runs down to a stop before
unwinding back to brand angle. The three tracks sit on three nested
elements because a child's transform composes before its parent's, and
that is the only arrangement which spins an upright mark rather than
tilting the axis it spins about.

CLICK_DURATIONS.grok goes from 900ms to 2100ms. The old value pulled
is-clicked while the mark was still held at 45deg, cutting the unwind.

Everything the orbit adds is transparent at rest, so every surface that
draws the mascot without hover — the extension tables, the marketplace,
the kit drawer — is unchanged. Only agent-card.tsx passes `animated`,
and it already leaves grok on overflow-visible for the orbit to overhang.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015weU8hXkgz73A96UcHL7oS

* chore: clear the outstanding biome findings

`npm run lint` has been reporting on three files that nothing in this
branch touched: an unsorted import in agent-mascot.tsx, a stale format in
agent-card.tsx, and a non-null assertion in the memory-grouping test. CI
runs test and build but not lint, which is why they went unnoticed.

The first two are biome's own fixes. The third replaces the assertion
with an explicit throw, so a missing group fails with a message naming
the group rather than with a null dereference three lines later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015weU8hXkgz73A96UcHL7oS

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@RealZST RealZST mentioned this pull request Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(agents): add Grok Build (grok) agent support

3 participants