Repository navigation
Fix CodeGraph SessionStart hook JSON output #57
Description
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.logshowsunexpected top-level keys: codegraphand validator exit1. - RED proof before the fix, disabled path:
outputs/red-codegraph-sessionstart-disabled.logshowsunexpected top-level keys: codegraphand validator exit1. - GREEN proof after the fix:
outputs/green-codegraph-sessionstart.logvalidates both enabled and disabled paths and printsGREEN: strict SessionStart hook output valid. - Source-dev distribution proof:
outputs/source-dev-codegraph-sessionstart-green.logvalidatescode-yeongyu/oh-my-openagentdevbuiltdist/cli.js; enabled has onlyhookSpecificOutput, 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.jsnode --check plugins/omo/components/codegraph/dist/cli.js
- Dependency-bound checks attempted:
bun test test/*.test.tscould not run becausebunis not installed in the verification environment.tsc --noEmitcould not run because the fresh checkout has no installedbun-typesandnodetype definitions.pnpm testcould not run because@oh-my-opencode/utilspoints to missing local path/tmp/lazycodex-codegraph-fix-JE0Tga/repo/utilsin 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
code-yeongyu commentedon Jun 18, 2026
[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:
origin/devcheckout,packages/omo-codex/plugin/components/codegraph/src/hook.tsnow writes onlyhookSpecificOutputfor the CodeGraphSessionStarthook. It no longer emits the unsupported top-levelcodegraphfield.stdout.join("") === ""), so the strict Codex parser no longer sees an unknown top-level key on either the enabled or disabled path.packages/omo-codex/plugin/components/codegraph/test/hook.test.tsnow 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.