Skip to content

Fix CodeGraph SessionStart hook JSON output #57

Closed

Description

@Hellfrosted

Problem Situation

LazyCodex's OMO CodeGraph SessionStart hook can fail when Codex parses the hook JSON output. The hook currently writes a top-level codegraph field in addition to hookSpecificOutput, but Codex's SessionStart hook output schema rejects unknown top-level fields.

This also explains why locally disabling CodeGraph is not a complete workaround: the disabled path still emitted codegraph: { "action": "skipped-disabled" }, so it could hit the same strict parser.

Reproduction Logs

Fresh LazyCodex checkout: /tmp/lazycodex-codegraph-fix-JE0Tga/repo

Failing proof against the current built hook output:

{
  "exitCode": 0,
  "spawned": 1,
  "parsed": {
    "hookSpecificOutput": {
      "hookEventName": "SessionStart",
      "additionalContext": "LazyCodex CodeGraph bootstrap scheduled in background"
    },
    "codegraph": {
      "action": "spawned"
    }
  },
  "unexpected": [
    "codegraph"
  ]
}
RED: invalid SessionStart output, unexpected top-level keys: codegraph
validator_exit=1

Failing proof for the disabled path against the current generated LazyCodex mirror output:

{
  "cliExit": 0,
  "stdout": "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\"},\"codegraph\":{\"action\":\"skipped-disabled\"}}",
  "stderr": "",
  "parsed": {
    "hookSpecificOutput": {
      "hookEventName": "SessionStart"
    },
    "codegraph": {
      "action": "skipped-disabled"
    }
  },
  "unexpected": [
    "codegraph"
  ],
  "validatorExit": 1
}
RED: disabled SessionStart output invalid, unexpected top-level keys: codegraph
validator_exit=1

Root Cause

Codex's SessionStart hook output accepts only the universal hook fields plus hookSpecificOutput; the event-specific output supports hookEventName and optional additionalContext. Unknown fields are denied by the wire schema.

The LazyCodex CodeGraph hook writes an extra top-level codegraph object from writeHookJson():

const output = action === "spawned"
  ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } }
  : { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };

Additional source context: code-yeongyu/oh-my-openagent dev at bae5484334d425b8137ea5380c2d7d21a9f420e3 already has a stricter CodeGraph source/dist shape under packages/omo-codex/plugin/components/codegraph: spawned emits only hookSpecificOutput, and disabled emits no stdout. The generated LazyCodex mirror/cache still contains the stale top-level codegraph field, so this issue is specifically about getting the distributed LazyCodex artifact regenerated or patched.

Verified Fix

Remove the unsupported codegraph field from the hook JSON written to stdout and update the regression tests to assert the strict Codex-compatible shape. The internal SessionStartAction return value remains unchanged, so LazyCodex can still distinguish spawned, skipped-disabled, and other internal outcomes without leaking those fields into the hook protocol.

diff --git a/plugins/omo/components/codegraph/dist/cli.js b/plugins/omo/components/codegraph/dist/cli.js
index 8c40c78..92e78ee 100755
--- a/plugins/omo/components/codegraph/dist/cli.js
+++ b/plugins/omo/components/codegraph/dist/cli.js
@@ -2002,7 +2002,7 @@ async function executeCodegraphSessionStartHook(options = {}) {
   return { action: "spawned", exitCode: 0 };
 }
 function writeHookJson(stdout, action) {
-  const output = action === "spawned" ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } } : { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };
+  const output = action === "spawned" ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE } } : { hookSpecificOutput: { hookEventName: "SessionStart" } };
   stdout.write(`${JSON.stringify(output)}
 `);
 }
diff --git a/plugins/omo/components/codegraph/src/hook.ts b/plugins/omo/components/codegraph/src/hook.ts
index 1c62395..8f0aa5c 100644
--- a/plugins/omo/components/codegraph/src/hook.ts
+++ b/plugins/omo/components/codegraph/src/hook.ts
@@ -62,8 +62,8 @@ export async function executeCodegraphSessionStartHook(options: SessionStartHook
 
 function writeHookJson(stdout: HookStdout, action: SessionStartAction): void {
 	const output = action === "spawned"
-		? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } }
-		: { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };
+		? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE } }
+		: { hookSpecificOutput: { hookEventName: "SessionStart" } };
 	stdout.write(`${JSON.stringify(output)}\n`);
 }
 
diff --git a/plugins/omo/components/codegraph/test/hook.test.ts b/plugins/omo/components/codegraph/test/hook.test.ts
index 02dfa44..3e82465 100644
--- a/plugins/omo/components/codegraph/test/hook.test.ts
+++ b/plugins/omo/components/codegraph/test/hook.test.ts
@@ -42,7 +42,6 @@ describe("CodeGraph SessionStart hook", () => {
 					hookEventName: "SessionStart",
 					additionalContext: "LazyCodex CodeGraph bootstrap scheduled in background",
 				},
-				codegraph: { action: "spawned" },
 			});
 		} finally {
 			rmSync(homeDir, { recursive: true, force: true });
@@ -68,7 +67,6 @@ describe("CodeGraph SessionStart hook", () => {
 		expect(spawned).toEqual([]);
 		expect(JSON.parse(stdout.join(""))).toEqual({
 			hookSpecificOutput: { hookEventName: "SessionStart" },
-			codegraph: { action: "skipped-disabled" },
 		});
 	});
 
@@ -97,7 +95,9 @@ describe("CodeGraph SessionStart hook", () => {
 			// then
 			expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
 			expect(spawned).toEqual([]);
-			expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
+			expect(JSON.parse(stdout.join(""))).toEqual({
+				hookSpecificOutput: { hookEventName: "SessionStart" },
+			});
 		} finally {
 			rmSync(homeDir, { recursive: true, force: true });
 			rmSync(workspace, { recursive: true, force: true });
@@ -128,7 +128,9 @@ describe("CodeGraph SessionStart hook", () => {
 			// then
 			expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
 			expect(spawned).toEqual([]);
-			expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
+			expect(JSON.parse(stdout.join(""))).toEqual({
+				hookSpecificOutput: { hookEventName: "SessionStart" },
+			});
 		} finally {
 			rmSync(homeDir, { recursive: true, force: true });
 			rmSync(workspace, { recursive: true, force: true });
@@ -191,7 +193,12 @@ describe("CodeGraph SessionStart hook", () => {
 					},
 				},
 			]);
-			expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "spawned" });
+			expect(JSON.parse(stdout.join(""))).toEqual({
+				hookSpecificOutput: {
+					hookEventName: "SessionStart",
+					additionalContext: "LazyCodex CodeGraph bootstrap scheduled in background",
+				},
+			});
 		} finally {
 			rmSync(workspace, { recursive: true, force: true });
 		}
@@ -531,7 +538,9 @@ describe("CodeGraph SessionStart hook", () => {
 		// then
 		expect(result.exitCode).toBe(0);
 		expect(spawned).toEqual([]);
-		expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
+		expect(JSON.parse(stdout.join(""))).toEqual({
+			hookSpecificOutput: { hookEventName: "SessionStart" },
+		});
 	});
 
 	it("#given plugin hook config #when inspected #then CodeGraph is registered after bootstrap SessionStart", () => {

Verification

  • RED proof before the fix: outputs/red-codegraph-sessionstart.log shows unexpected top-level keys: codegraph and validator exit 1.
  • RED proof before the fix, disabled path: outputs/red-codegraph-sessionstart-disabled.log shows unexpected top-level keys: codegraph and validator exit 1.
  • GREEN proof after the fix: outputs/green-codegraph-sessionstart.log validates both enabled and disabled paths and prints GREEN: strict SessionStart hook output valid.
  • Source-dev distribution proof: outputs/source-dev-codegraph-sessionstart-green.log validates code-yeongyu/oh-my-openagent dev built dist/cli.js; enabled has only hookSpecificOutput, disabled has no stdout, and both have no unsupported keys.
  • Static checks after the fix:
    • git diff --check -- plugins/omo/components/codegraph/src/hook.ts plugins/omo/components/codegraph/test/hook.test.ts plugins/omo/components/codegraph/dist/cli.js
    • node --check plugins/omo/components/codegraph/dist/cli.js
  • Dependency-bound checks attempted:
    • bun test test/*.test.ts could not run because bun is not installed in the verification environment.
    • tsc --noEmit could not run because the fresh checkout has no installed bun-types and node type definitions.
    • pnpm test could not run because @oh-my-opencode/utils points to missing local path /tmp/lazycodex-codegraph-fix-JE0Tga/repo/utils in this generated snapshot.

Commit in temporary verification branch: a3581a8 fix(omo): emit strict codegraph session-start output


This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated

Activity

  1. code-yeongyu commented on Jun 18, 2026

    @code-yeongyu
    Owner

    [sisyphus-dev] Thanks for the very clear report and the failing proof. This is resolved now in the main oh-my-openagent source that feeds LazyCodex.

    Evidence:

    • Merged PR: fix(codex): make codegraph startup hook output valid oh-my-openagent#5379
    • Fix commit: code-yeongyu/oh-my-openagent@47af76f
    • In the current origin/dev checkout, packages/omo-codex/plugin/components/codegraph/src/hook.ts now writes only hookSpecificOutput for the CodeGraph SessionStart hook. It no longer emits the unsupported top-level codegraph field.
    • The disabled paths now emit no hook JSON at all (stdout.join("") === ""), so the strict Codex parser no longer sees an unknown top-level key on either the enabled or disabled path.
    • The regression tests in packages/omo-codex/plugin/components/codegraph/test/hook.test.ts now assert the strict shape.

    I’m closing this as fixed. Thank you again for including the exact parser failure and the target shape; it made the verification straightforward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions