fix(install-hooks): harden Grok hook support

This commit is contained in:
AkitaOnRails
2026-06-14 11:20:41 -03:00
parent aa46f5ef5b
commit 9ccfdfee02
27 changed files with 404 additions and 54 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `install-hooks --agent grok` (plus `setup-agent` / `uninstall` coverage) for
the xAI **Grok Build CLI**. Grok's `~/.grok/hooks/ai-memory.json` shares Claude
Code's JSON shape and seven-event vocabulary, so it reuses the claude-code hook
scripts and emits the native `ai-memory hook --event … --agent grok` command.
Code's JSON shape and seven-event vocabulary, with a Grok-specific hook bundle
and native `ai-memory hook --event … --agent grok` commands.
ai-memory entries merge into a dedicated `ai-memory.json`, leaving any
third-party `~/.grok/hooks/*.json` untouched. NOTE: Grok ignores hook stdout on
`SessionStart`, so capture works but handoff injection does not. Grok's
+8 -7
View File
@@ -31,7 +31,7 @@
| Claude Desktop | MCP-only | Uses `mcp-remote`; no lifecycle hooks. |
| OpenClaw | Supported | MCP config + native plugin lifecycle hooks. |
| Antigravity CLI | Supported | MCP config (`serverUrl`) + lifecycle hooks (`agy` alias). |
| Grok Build CLI | Hooks | Lifecycle hooks via `install-hooks --agent grok` (`~/.grok/hooks/ai-memory.json`, native `--agent grok`, reuses claude-code scripts). Capture works; no handoff injection — Grok ignores `SessionStart` stdout, so recover handoffs via MCP `memory_handoff_accept`. |
| Grok Build CLI | Hooks | Lifecycle hooks via `install-hooks --agent grok` (`~/.grok/hooks/ai-memory.json`, Grok-specific hook bundle, native `--agent grok`). Capture works; no handoff injection — Grok ignores `SessionStart` stdout, so recover handoffs via MCP `memory_handoff_accept`. |
| VS Code Copilot | MCP-only | `.vscode/mcp.json` for Copilot agent mode; no lifecycle hooks (Copilot does not expose them yet). |
| LLM/auth providers | Supported | Anthropic, OpenAI, OpenAI OAuth/Codex, GitHub Copilot, Gemini, OpenAI-compatible endpoints, and generic OIDC device auth for native hooks. |
| Embedding providers | Supported | OpenAI, Voyage, and Google Gemini. |
@@ -101,9 +101,10 @@ priors are at the [bottom](#influences-and-prior-art).
## Use cases
- **"Quit at 4 PM, pick up at 9 AM in a different agent."** The
classic. SessionStart hook in the next agent (any of the
supported CLIs) prepends a typed handoff with the open questions,
next steps, and a session summary.
classic. SessionStart hook in the next supported hook client prepends a
typed handoff with open questions, next steps, and a session summary. Grok
captures lifecycle events but ignores SessionStart stdout, so ask it to call
`memory_handoff_accept` when resuming from a handoff.
- **"What did we decide about X six weeks ago?"** Type
`memory_query X` from the agent (or `ai-memory search X` from a
terminal) - FTS5 over the wiki. Pages are LLM-consolidated, so
@@ -178,7 +179,7 @@ packaged unit. Full user-service, system-service, auth, and provider setup is in
### Docker
You need: Docker + an agent CLI (Claude Code, Codex, OpenCode, OMP, Cursor,
Antigravity CLI, or anything else that speaks MCP).
Antigravity CLI, Grok Build CLI, or anything else that speaks MCP).
The published Docker image includes `linux/amd64` and `linux/arm64` variants,
so Apple Silicon Macs and ARM64 Linux hosts can pull `akitaonrails/ai-memory`
@@ -272,8 +273,8 @@ use `--mcp-url` if you installed MCP with a custom endpoint, and
and staged hook scripts. Redeploy remote servers separately.
For Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI,
OpenClaw, VS Code Copilot, curl-based hook installs, source builds, CLI env vars,
and the full subcommand reference, see [`docs/install.md`](docs/install.md).
Grok Build CLI, OpenClaw, VS Code Copilot, curl-based hook installs, source builds,
CLI env vars, and the full subcommand reference, see [`docs/install.md`](docs/install.md).
## Security
+2 -2
View File
@@ -716,8 +716,8 @@ pub enum AgentChoice {
AntigravityCli,
/// xAI Grok Build CLI — JSON-config hooks in
/// `~/.grok/hooks/ai-memory.json`. Native `ai-memory hook --event`
/// integration like Claude Code (reuses the claude-code hook
/// scripts). NOTE: Grok ignores hook stdout on `SessionStart`, so
/// integration using Grok-specific hook scripts. NOTE: Grok ignores
/// hook stdout on `SessionStart`, so
/// capture works but handoff injection does not — recover the prior
/// session's handoff via the MCP `memory_handoff_accept` tool.
Grok,
@@ -509,14 +509,13 @@ fn apply_to_claude_code_settings(
Ok(())
}
/// Mutate `~/.grok/hooks/ai-memory.json` so Grok Build CLI fires the
/// ai-memory lifecycle hooks. Grok's hook config is structurally
/// identical to Claude Code's (`{"hooks":{"SessionStart":[{"hooks":
/// [{"type":"command","command":"..."}]}]}}`) and uses the same seven
/// CamelCase event names, so we reuse Claude Code's staged scripts and
/// only swap the emitted `--agent grok` tag. We merge into a dedicated
/// `ai-memory.json` (Grok discovers every `~/.grok/hooks/*.json`), so a
/// pre-existing third-party hook file is left untouched.
/// Mutate `~/.grok/hooks/ai-memory.json` so Grok Build CLI fires the ai-memory
/// lifecycle hooks. Grok's hook config is structurally identical to Claude
/// Code's nested hook JSON and uses the same seven CamelCase event names, but
/// its script bundle carries `agent=grok` and skips destructive SessionStart
/// handoff fetches. We merge into a dedicated `ai-memory.json` (Grok discovers
/// every `~/.grok/hooks/*.json`), so a pre-existing third-party hook file is
/// left untouched.
fn apply_to_grok_settings(
hooks_dir: &Path,
server_url: &str,
@@ -1718,9 +1717,7 @@ fn resolve_hooks_dir(explicit: Option<&Path>, agent: AgentChoice) -> Result<Path
AgentChoice::Cursor => "cursor",
AgentChoice::GeminiCli => "gemini-cli",
AgentChoice::AntigravityCli => "antigravity-cli",
// Grok reuses Claude Code's hook scripts (identical JSON shape +
// event vocabulary); only the emitted `--agent grok` tag differs.
AgentChoice::Grok => "claude-code",
AgentChoice::Grok => "grok",
AgentChoice::OpenCode | AgentChoice::Omp | AgentChoice::Openclaw => {
anyhow::bail!("{agent:?} uses a generated integration, not a hook script directory")
}
@@ -2100,6 +2097,16 @@ mod tests {
);
}
#[test]
fn resolve_hooks_dir_uses_grok_bundle_for_grok() {
let tmp = TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("grok")).unwrap();
fs::create_dir_all(tmp.path().join("claude-code")).unwrap();
let resolved = resolve_hooks_dir(Some(tmp.path()), AgentChoice::Grok).unwrap();
assert_eq!(resolved, tmp.path().join("grok"));
}
#[test]
fn opencode_mcp_inference_supplies_hook_origin_and_token() {
let inferred = infer_json_mcp_config(
@@ -26,7 +26,7 @@ use serde_json::json;
///
/// Adding a hook event means updating this list AND adding the
/// matching `.sh` and `.ps1` files under
/// `hooks/{claude-code,codex,cursor,gemini-cli,opencode}/`. The
/// `hooks/{claude-code,codex,cursor,gemini-cli,grok,opencode}/`. The
/// install-hooks parity test fails if the bundle drifts.
pub(crate) const CLAUDE_CODE_EVENTS: [(&str, &str); 7] = [
("SessionStart", "session-start.sh"),
@@ -131,10 +131,28 @@ pub(crate) fn build_claude_code_payload_with_data_dir(
)
}
/// Grok Build CLI hook payload. Grok's `~/.grok/hooks/*.json` shares
/// Claude Code's JSON shape and seven-event vocabulary
/// (`CLAUDE_CODE_EVENTS`, `HookShape::Nested`), so we reuse both and
/// only swap the `--agent grok` tag in the emitted native command.
/// Grok Build CLI hook payload for docker/setup-agent script snippets.
/// Grok shares Claude Code's JSON shape and event vocabulary, but uses
/// its own script bundle so script fallback keeps `agent=grok` and never
/// destructively fetches handoffs on SessionStart.
#[must_use]
pub(crate) fn build_grok_payload(
emit_root: &Path,
server_url: &str,
auth_token: Option<&str>,
) -> serde_json::Value {
build_hook_payload_for_platform(
&CLAUDE_CODE_EVENTS,
emit_root,
server_url,
auth_token,
HookShape::Nested,
HookCommandContext::new(HookCommandPlatform::for_bash_script_runner(), "grok", None),
)
}
/// Grok Build CLI hook payload for apply/render paths. Native commands are the
/// default; explicit script fallback still points at the Grok script bundle.
pub(crate) fn build_grok_payload_with_data_dir(
emit_root: &Path,
server_url: &str,
@@ -767,6 +785,47 @@ mod tests {
}
}
#[test]
fn grok_native_payload_uses_grok_agent() {
let root = PathBuf::from("/host/hooks/grok");
let v = build_hook_payload_for_platform(
&CLAUDE_CODE_EVENTS,
&root,
"http://localhost:49374",
None,
HookShape::Nested,
HookCommandContext::new(HookCommandPlatform::PosixNative, "grok", None),
);
let command = v
.pointer("/hooks/SessionStart/0/hooks/0/command")
.and_then(|s| s.as_str())
.unwrap();
assert!(command.contains("--agent grok"), "{command}");
assert!(!command.contains("claude-code"), "{command}");
}
#[test]
fn grok_script_payload_uses_grok_bundle() {
let root = PathBuf::from("/host/hooks/grok");
let v = build_hook_payload_for_platform(
&CLAUDE_CODE_EVENTS,
&root,
"http://localhost:49374",
None,
HookShape::Nested,
HookCommandContext::new(HookCommandPlatform::Posix, "grok", None),
);
let command = v
.pointer("/hooks/SessionStart/0/hooks/0/command")
.and_then(|s| s.as_str())
.unwrap();
assert!(
command.contains("/host/hooks/grok/session-start.sh"),
"{command}"
);
assert!(!command.contains("claude-code"), "{command}");
}
#[test]
fn claude_code_payload_embeds_auth_token_when_provided() {
let root = PathBuf::from("/host/hooks/claude-code");
@@ -36,7 +36,8 @@ use anyhow::{Context, Result, bail};
use crate::cli::{AgentChoice, SetupAgentArgs};
use crate::commands::render_shared::{
CLAUDE_CODE_EVENTS, build_claude_code_payload, hook_script_for_current_platform,
CLAUDE_CODE_EVENTS, build_claude_code_payload, build_grok_payload,
hook_script_for_current_platform,
};
use crate::config::{Config, DEFAULT_SERVER_URL};
@@ -70,8 +71,7 @@ pub fn run(config: &Config, args: SetupAgentArgs) -> Result<()> {
AgentChoice::Cursor => "cursor",
AgentChoice::GeminiCli => "gemini-cli",
AgentChoice::AntigravityCli => "antigravity-cli",
// Grok reuses Claude Code's hook scripts (same shape + events).
AgentChoice::Grok => "claude-code",
AgentChoice::Grok => "grok",
AgentChoice::OpenCode => unreachable!("opencode handled above"),
AgentChoice::Omp => unreachable!("omp handled above"),
AgentChoice::Openclaw => unreachable!("openclaw handled above"),
@@ -128,7 +128,8 @@ pub fn run(config: &Config, args: SetupAgentArgs) -> Result<()> {
.join(agent_sub);
match args.agent {
AgentChoice::ClaudeCode | AgentChoice::Grok => emit_claude_code(&emit_root, &args)?,
AgentChoice::ClaudeCode => emit_claude_code(&emit_root, &args)?,
AgentChoice::Grok => emit_grok(&emit_root, &args)?,
AgentChoice::Codex
| AgentChoice::Cursor
| AgentChoice::GeminiCli
@@ -205,6 +206,26 @@ fn emit_claude_code(emit_root: &Path, args: &SetupAgentArgs) -> Result<()> {
Ok(())
}
fn emit_grok(emit_root: &Path, args: &SetupAgentArgs) -> Result<()> {
let payload = build_grok_payload(emit_root, &args.server_url, args.auth_token.as_deref());
let serialized =
serde_json::to_string_pretty(&payload).context("serializing Grok hook config")?;
println!("# Grok Build CLI — write to ~/.grok/hooks/ai-memory.json");
println!("# Hook scripts (must be reachable from the host that runs Grok):");
println!("# {}", emit_root.display());
println!("# AI-memory server: {}", args.server_url);
if args.auth_token.is_some() {
println!("# Auth: AI_MEMORY_AUTH_TOKEN embedded in each hook command below.");
println!("# Treat ~/.grok/hooks/ai-memory.json as sensitive (chmod 600).");
}
println!("# NOTE: Grok ignores SessionStart stdout, so this config captures");
println!("# lifecycle events but does not inject handoffs automatically.");
println!("# Recover handoffs via the MCP memory_handoff_accept tool.");
println!();
println!("{serialized}");
Ok(())
}
fn emit_other(emit_root: &Path, label: &str, args: &SetupAgentArgs) {
// These clients have hook surfaces, but their print-mode config
// snippets are intentionally conservative: apply-mode owns the
+5 -3
View File
@@ -246,11 +246,12 @@ impl AgentKind {
/// **destructive** (the server marks the handoff accepted) and the result
/// would be discarded — silently losing the handoff. For such agents the
/// handoff stays available on demand via the MCP `memory_handoff_accept`
/// tool. Only the native-hook agents (claude-code, grok) reach this code;
/// the others inject via their own staged-script / plugin formats.
/// tool. Unknown future agents return `false` until we know their
/// SessionStart stdout semantics; accepting a handoff is single-use and
/// should fail safe.
#[must_use]
pub fn session_start_injects_handoff(self) -> bool {
!matches!(self, Self::Grok)
!matches!(self, Self::Grok | Self::Other)
}
}
@@ -296,6 +297,7 @@ mod tests {
assert!(!AgentKind::Grok.session_start_injects_handoff());
assert!(AgentKind::ClaudeCode.session_start_injects_handoff());
assert!(AgentKind::Codex.session_start_injects_handoff());
assert!(!AgentKind::Other.session_start_injects_handoff());
}
#[test]
+123
View File
@@ -2334,6 +2334,129 @@ mod tests {
assert_eq!(parent_rows, 1);
}
#[test]
fn v20_adds_grok_and_preserves_sessions_invariants_on_upgraded_db() {
use ai_memory_core::{AgentKind, NewObservation, NewSession, ObservationKind, SessionId};
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("test.sqlite");
let ws;
let proj;
let existing_sid = SessionId::new();
{
let mut conn = Connection::open(&db_path).unwrap();
conn.pragma_update(None, "foreign_keys", "OFF").unwrap();
crate::migrations::run_to(&mut conn, 19).unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
ws = get_or_create_workspace(&mut conn, "default").unwrap();
proj = get_or_create_project(&mut conn, &ws, "scratch", None).unwrap();
begin_session(
&mut conn,
&NewSession {
id: existing_sid,
workspace_id: ws,
project_id: proj,
agent_kind: AgentKind::ClaudeCode,
cwd: None,
},
)
.unwrap();
insert_observation(
&mut conn,
&NewObservation {
session_id: existing_sid,
workspace_id: ws,
project_id: proj,
kind: ObservationKind::UserPrompt,
extension: None,
source_event: None,
title: "before v20".into(),
body: "existing observation survives table rebuild".into(),
importance: 5,
},
)
.unwrap();
}
let mut conn = Connection::open(&db_path).unwrap();
conn.pragma_update(None, "foreign_keys", "OFF").unwrap();
crate::migrations::run_to(&mut conn, 20).unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
begin_session(
&mut conn,
&NewSession {
id: SessionId::new(),
workspace_id: ws,
project_id: proj,
agent_kind: AgentKind::Grok,
cwd: None,
},
)
.unwrap();
let obs_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM observations WHERE session_id = ?1",
params![existing_sid.as_bytes()],
|r| r.get(0),
)
.unwrap();
assert_eq!(obs_count, 1, "V20 must preserve existing observations");
let index_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master \
WHERE type = 'index' \
AND name IN ('idx_sessions_recent', 'idx_sessions_project', 'idx_sessions_started_at')",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(index_count, 3, "V20 must recreate sessions indexes");
let trigger_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master \
WHERE type = 'trigger' AND name = 'sessions_ws_proj_pairing_ai'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
trigger_count, 1,
"V20 must recreate the V18 pairing trigger"
);
let other_ws = get_or_create_workspace(&mut conn, "other").unwrap();
let other_proj =
get_or_create_project(&mut conn, &other_ws, "other-project", None).unwrap();
let err = begin_session(
&mut conn,
&NewSession {
id: SessionId::new(),
workspace_id: ws,
project_id: other_proj,
agent_kind: AgentKind::Grok,
cwd: None,
},
)
.unwrap_err();
assert!(
err.to_string()
.contains("sessions.workspace_id does not match"),
"pairing trigger must reject split-brain sessions after V20: {err}"
);
let fk_violations: i64 = conn
.query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |r| {
r.get(0)
})
.unwrap();
assert_eq!(fk_violations, 0, "V20 must leave foreign keys clean");
}
/// V19 is idempotent: re-running on a repaired DB is a no-op.
/// Also asserts the initial run on a clean DB (no orphans, no
/// empty fragments) is a no-op.
+3 -3
View File
@@ -7,8 +7,8 @@
## Purpose
ai-memory is a single Rust binary that gives AI coding agents (Claude
Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, OpenClaw,
OpenCode, OMP, and MCP-capable clients) long-term memory shared across CLIs.
Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI,
OpenClaw, OpenCode, OMP, and MCP-capable clients) long-term memory shared across CLIs.
Quit one mid-task; open another in the same directory; continue. No
manual `write_note` ceremony, no copy-pasting summaries between
sessions.
@@ -26,7 +26,7 @@ markdown stays the source of truth.
┌──────────────────────┐
│ Claude Code / Codex │
│ Cursor / Gemini CLI │
│ Antigravity CLI │
│ Antigravity / Grok │
│ OpenClaw / OpenCode │
│ OMP │
└──────────┬───────────┘
+15 -8
View File
@@ -9,7 +9,7 @@ path (docker + Claude Code). This page covers everything else:
- [Arch Linux native packages (AUR)](#arch-linux-native-packages-aur)
(systemd system service or user service)
- [Configuring other agent CLIs](#configuring-other-agent-clis)
(Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, OpenClaw, VS Code Copilot)
(Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, Grok Build CLI, OpenClaw, VS Code Copilot)
- [Installing hooks without docker](#installing-hooks-without-docker)
(curl-based installer)
- [Running ai-memory without docker](#running-ai-memory-without-docker)
@@ -427,11 +427,11 @@ Each agent CLI needs two things:
becomes manual.
Claude Desktop is MCP-only today. Claude Code, Codex, OpenCode, OMP,
Cursor, Gemini CLI, Antigravity CLI, and OpenClaw have lifecycle capture paths through
Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, and OpenClaw have lifecycle capture paths through
`install-hooks`.
> **Two-step hook install pattern.** Claude Code, Codex, Cursor,
> Gemini CLI, and Antigravity CLI use shell/PowerShell hook scripts: (1) `docker cp` the
> Gemini CLI, Antigravity CLI, and Grok Build CLI use shell/PowerShell hook scripts: (1) `docker cp` the
> bundled scripts to your home dir, (2) `docker run --rm install-hooks`
> to render the config snippet.
> On native Windows, Claude Code is the exception to the PowerShell default:
@@ -528,7 +528,7 @@ files owned by the user running the command. Prefer it as the
default; reach for `setup-agent` only when your docker setup is
known not to remap UIDs.
### Cursor, Gemini CLI, Claude Desktop, OpenClaw, Antigravity CLI, VS Code Copilot
### Cursor, Gemini CLI, Claude Desktop, OpenClaw, Antigravity CLI, Grok Build CLI, VS Code Copilot
See [**`docs/mcp-install.md`**](mcp-install.md) for the per-client MCP
config file path and snippet, or one-shot it via:
@@ -562,6 +562,10 @@ docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent antigravity-cli --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent grok --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client openclaw --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
@@ -576,12 +580,15 @@ docker run --rm akitaonrails/ai-memory:latest \
```
Cursor, Gemini CLI, Antigravity CLI, and OpenClaw support both `install-mcp` and
`install-hooks`. Claude Desktop and VS Code Copilot are MCP-only here,
`install-hooks`. Grok Build CLI is hook-only in ai-memory's installer today:
`install-hooks --agent grok` captures lifecycle events, but Grok ignores
`SessionStart` stdout, so handoffs must be accepted through MCP with
`memory_handoff_accept` when resuming. Claude Desktop and VS Code Copilot are MCP-only here,
so you'll need to nudge the model to call `memory_query` /
`memory_handoff_accept` itself.
For clients with `install-hooks` support, the capture path handles
handoff injection at session start or the client's closest equivalent
(Antigravity CLI uses `PreInvocation`).
handoff injection at session start or the client's closest equivalent, except
for Grok's no-stdout SessionStart behavior (Antigravity CLI uses `PreInvocation`).
---
@@ -605,7 +612,7 @@ docker run --rm akitaonrails/ai-memory:latest \
```
The curl script installer supports
`--agent claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi`
`--agent claude-code|codex|cursor|gemini-cli|antigravity-cli|grok|opencode|openclaw|omp|pi`
and `--to <dir>`; `--help` prints the full flag list. OpenCode,
OpenClaw, and OMP do not need script extraction because `install-hooks`
generates TypeScript plugin/extension files for them instead.
+6 -5
View File
@@ -19,13 +19,14 @@
This page documents how to register ai-memory as an MCP server with
agent CLIs beyond the README quick start.
Claude Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, OpenClaw, OpenCode, and
Claude Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, OpenClaw, OpenCode, and
OMP have automatic capture integrations (shell/PowerShell hooks for
Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI, TypeScript plugin/extension
Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI / Grok Build CLI, TypeScript plugin/extension
files for OpenClaw / OpenCode / OMP) and are covered in the
[main README](../README.md#quick-start). On native Windows, Claude Code uses
Git Bash `.sh` hooks rather than the PowerShell default used by other
script-hook agents.
script-hook agents. Grok captures lifecycle events, but it ignores
SessionStart stdout, so ai-memory does not auto-inject handoffs for Grok.
Claude Desktop and VS Code Copilot are **MCP-only** here: they expose
long-term memory to their LLMs via ai-memory's MCP tools
@@ -521,8 +522,8 @@ that *starts* the next one - to play nicely with ai-memory:
| Side | What's needed | Covered by |
|---|---|---|
| **Ending side** | The agent must create a handoff, either through a true session-end hook or by calling `memory_handoff_begin`. | Built-in for Claude Code, Cursor, Gemini CLI, OpenClaw, and OMP. Codex, OpenCode, and Antigravity CLI have no true session-end event in the current integration, so ask them to call `memory_handoff_begin` before quitting when you need a handoff. |
| **Starting side** | Either (a) the session-start/plugin path injects the handoff via `/handoff`, OR (b) the model proactively calls `memory_handoff_accept` on first turn. | (a) is built-in for Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI / OpenClaw / OpenCode / OMP. (b) works for any MCP-capable client if you nudge the model - see [the routing snippet](usage.md#install-the-routing-snippet). |
| **Ending side** | The agent must create a handoff, either through a true session-end hook or by calling `memory_handoff_begin`. | Built-in for Claude Code, Cursor, Gemini CLI, Grok Build CLI, OpenClaw, and OMP. Codex, OpenCode, and Antigravity CLI have no true session-end event in the current integration, so ask them to call `memory_handoff_begin` before quitting when you need a handoff. |
| **Starting side** | Either (a) the session-start/plugin path injects the handoff via `/handoff`, OR (b) the model proactively calls `memory_handoff_accept` on first turn. | (a) is built-in for Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI / OpenClaw / OpenCode / OMP. Grok is explicitly excluded because it ignores SessionStart stdout; use (b). (b) works for any MCP-capable client if you nudge the model - see [the routing snippet](usage.md#install-the-routing-snippet). |
So a typical mixed workflow looks like:
+1 -1
View File
@@ -253,7 +253,7 @@ Windows agent builds.
Claude Code invokes hooks as a direct binary call (no shell) by default;
`AI_MEMORY_HOOK_PLATFORM=windows-bash` restores the Git Bash `bash -c`
path. WSL2 Claude Code uses normal WSL `.sh` paths.
- Codex, OpenCode, Cursor, Gemini CLI, and OpenClaw may each choose different
- Codex, OpenCode, Cursor, Gemini CLI, Grok Build CLI, and OpenClaw may each choose different
Windows config locations or shell execution behavior. ai-memory uses
the current best-known defaults, but they need validation on real
installations.
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "post-tool-use" -Agent "grok"
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Grok Build CLI post-tool-use hook.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=post-tool-use&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "pre-compact" -Agent "grok"
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Grok Build CLI pre-compact hook.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=pre-compact&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "pre-tool-use" -Agent "grok"
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Grok Build CLI pre-tool-use hook.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=pre-tool-use&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "session-end" -Agent "grok"
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Grok Build CLI session-end hook.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=session-end&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "session-start" -Agent "grok"
exit 0
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# Grok Build CLI SessionStart hook.
# Grok ignores SessionStart stdout, so this hook captures the event only.
# Do NOT fetch /handoff here: accepting a handoff is destructive and Grok
# would discard the returned context.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=session-start&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "stop" -Agent "grok"
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Grok Build CLI stop hook.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=stop&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "user-prompt" -Agent "grok"
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Grok Build CLI user-prompt hook.
_lib_dir="$(dirname "$0")"
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
. "$_lib_dir/_lib.sh"
SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
PAYLOAD=$(cat)
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
QS=$(ai_memory_marker_qs "$CWD")
printf '%s' "$PAYLOAD" \
| ai_memory_post_hook "$SERVER/hook?event=user-prompt&agent=grok${QS}" >/dev/null 2>&1 || true
printf '{}\n'
exit 0
+3 -3
View File
@@ -10,7 +10,7 @@
# | bash -s -- --agent claude-code
#
# Options:
# --agent <claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi>
# --agent <claude-code|codex|cursor|gemini-cli|antigravity-cli|grok|opencode|openclaw|omp|pi>
# which agent (default: claude-code;
# generated-plugin agents print hints)
# --to <dir> install root (default: $HOME/.ai-memory/hooks)
@@ -46,9 +46,9 @@ while [[ $# -gt 0 ]]; do
done
case "$AGENT" in
claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi|oh-my-pi) ;;
claude-code|codex|cursor|gemini-cli|antigravity-cli|grok|opencode|openclaw|omp|pi|oh-my-pi) ;;
*)
echo "unsupported agent: $AGENT (expected claude-code | codex | cursor | gemini-cli | antigravity-cli | opencode | openclaw | omp | pi | oh-my-pi)" >&2
echo "unsupported agent: $AGENT (expected claude-code | codex | cursor | gemini-cli | antigravity-cli | grok | opencode | openclaw | omp | pi | oh-my-pi)" >&2
exit 64 ;;
esac