From 760c07f5f65744970e0337a543e4390e0bbfe2a4 Mon Sep 17 00:00:00 2001 From: olegshmuelov Date: Sun, 9 Aug 2026 15:59:27 +0300 Subject: [PATCH] fix(brains): run the brains startup hook on Claude Code session fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code fires SessionStart with source=fork on --fork-session, and the Claude map's matcher excluded it, so a forked session ran no brains startup hook at all — no inbox pass, no device report, no update nudge. core.md itself usually survived, because a fork copies the parent's records and the CLI drops a redundant re-injection, which is what kept this quiet. The Codex map already covers this: Codex declares four SessionStart sources and no fork, and a forked Codex thread arrives as startup or resume, both already matched. So Codex forks have always run the hook and this change makes the two clients converge. The contract test pins that asymmetry as a deliberate configuration fact rather than leaving it to read as drift. --- plugins/brains/.claude-plugin/plugin.json | 2 +- plugins/brains/.codex-plugin/plugin.json | 2 +- plugins/brains/hooks/claude-hooks.json | 2 +- tests/plugin-contract/run.ts | 11 ++++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/brains/.claude-plugin/plugin.json b/plugins/brains/.claude-plugin/plugin.json index 13a9465..194d8ad 100644 --- a/plugins/brains/.claude-plugin/plugin.json +++ b/plugins/brains/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "brains", "description": "Your memory layer: Gmail, Calendar, Drive, and prior Claude conversations as queryable pages, with reflexive recall, hook-driven turn-by-turn capture and inbox delivery, and boards/automations/workflows on top.", - "version": "2.8.1", + "version": "2.8.2", "author": { "name": "brains (ssvlabs)" }, diff --git a/plugins/brains/.codex-plugin/plugin.json b/plugins/brains/.codex-plugin/plugin.json index bf854c3..6398af4 100644 --- a/plugins/brains/.codex-plugin/plugin.json +++ b/plugins/brains/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "brains", - "version": "2.8.1", + "version": "2.8.2", "description": "Your personal memory layer for Codex: query Gmail, Calendar, Drive, and prior conversations, then build boards, automations, and workflows.", "author": { "name": "brains (ssvlabs)", diff --git a/plugins/brains/hooks/claude-hooks.json b/plugins/brains/hooks/claude-hooks.json index d761545..b5b2f8f 100644 --- a/plugins/brains/hooks/claude-hooks.json +++ b/plugins/brains/hooks/claude-hooks.json @@ -2,7 +2,7 @@ "hooks": { "SessionStart": [ { - "matcher": "startup|resume|clear|compact", + "matcher": "startup|resume|clear|compact|fork", "hooks": [ { "type": "command", diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index 529260d..1670e62 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -117,7 +117,7 @@ type HookGroup = { const CLAUDE_HOOK_EVENTS: Record = { SessionStart: [ { - matcher: "startup|resume|clear|compact", + matcher: "startup|resume|clear|compact|fork", hooks: [ { type: "command", @@ -986,6 +986,15 @@ for (const [label, source, expected] of [ // would reject adding them but would report it as a set mismatch; these say why. assert(!codexEvents.includes("PostToolUseFailure"), "Codex does not support PostToolUseFailure"); assert(!codexEvents.includes("SessionEnd"), "Codex does not support SessionEnd"); +// Not a claim about Codex at runtime — a CONFIGURATION pin recording why the two SessionStart +// matchers above differ, so the asymmetry reads as deliberate rather than as drift. Codex declares +// four SessionStart sources and no `fork` (codex-rs/hooks/src/events/session_start.rs); a forked +// Codex thread arrives as `startup` or `resume` depending on the fork path, and both are already +// matched. `fork` is Claude vocabulary only. +assert( + !(codexHooks.hooks.SessionStart[0].matcher ?? "").split("|").includes("fork"), + "Codex declares no fork SessionStart source — a forked Codex thread arrives as startup or resume, so `fork` here would pin a value Codex never emits", +); for (const command of [...hookScripts(claudeHooks), ...hookScripts(codexHooks)]) { const match = command.match(/hooks\/(brains-[a-z-]+\.sh)/);