Skip to content

fix(gemini-cli): emit SessionStart as structured JSON, not plaintext (#299)#314

Open
ousamabenyounes wants to merge 1 commit intomksglu:nextfrom
ousamabenyounes:fix/issue-299
Open

fix(gemini-cli): emit SessionStart as structured JSON, not plaintext (#299)#314
ousamabenyounes wants to merge 1 commit intomksglu:nextfrom
ousamabenyounes:fix/issue-299

Conversation

@ousamabenyounes
Copy link
Copy Markdown

@ousamabenyounes ousamabenyounes commented Apr 20, 2026

What

hooks/gemini-cli/sessionstart.mjs concatenated the full ~10 KB routing block into a plain-text string prefixed with "SessionStart:compact hook success: Success" and wrote it to stdout. Gemini CLI surfaces that text verbatim in the UI, which is exactly the startup noise reported in #299 ("the message is too verbose").

The Claude Code and VS Code Copilot SessionStart hooks already use structured JSON ({ hookSpecificOutput: { hookEventName, additionalContext } }) and are rendered as hook metadata rather than user-visible output. This PR brings the Gemini hook in line — no new config flag needed, hidden by default as the reporter suggested.

Fixes #299.

Why just this

An in-repo source contract for the VS Code Copilot hook already exists (tests/hooks/vscode-hooks.test.ts:279) that forbids \"SessionStart:compact hook success\" in that hook's source and requires JSON.stringify. The Gemini hook is the one remaining SessionStart hook that hadn't been migrated. No config / new env var / new feature flag was considered necessary — the issue is effectively a missed parity cleanup.

No code path other than the final stdout emission changes, so session-lifecycle behavior (startup / compact / resume / clear) is untouched.

Changes

hooks/gemini-cli/sessionstart.mjs

  • Replace the final two lines (const output = ...; process.stdout.write(output);) with the same console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: \"SessionStart\", additionalContext } })) form Claude Code / VS Code Copilot already use.

tests/hooks/gemini-hooks.test.ts — two regression cases added to the existing sessionstart.mjs describe block (per CONTRIBUTING's "add to existing domain file" rule):

  • sessionstart outputs structured JSON (hidden from user in Gemini CLI) — parses stdout as JSON, asserts hookSpecificOutput.hookEventName === \"SessionStart\" and that additionalContext is a string containing "context-mode". Also asserts the old plaintext markers (\"SessionStart:compact hook success\", \"SessionStart hook additional context:\") are not present.
  • sessionstart source uses JSON.stringify, not plaintext output (#299) — reads the hook source and pins the JSON path in place (symmetric with the existing vscode-copilot source check at tests/hooks/vscode-hooks.test.ts:279).

Test plan

  • npx vitest run tests/hooks/gemini-hooks.test.ts → 13/13 pass (2 new, 11 existing)
  • npm test → 1597 pass, 23 skipped, 0 failures
  • npm run typecheck → clean

Output quality (before / after)

Before — Gemini CLI user sees on session start:

ℹ SessionStart:compact hook success: Success
  SessionStart hook additional context: 
  <context_window_protection>
    <priority_instructions>
      Raw tool output floods your context window. You MUST use context-mode MCP tools...
    </priority_instructions>
    ... (~10 KB of XML routing instructions) ...

After:

(nothing visible — the routing block is still delivered, but as hook metadata)

The routing block is still injected into the agent's context on every session start — nothing changes semantically. Only the Gemini CLI UI rendering changes.

Generated by Claude Code
Vibe coded by ousamabenyounes

…ksglu#299)

The Gemini CLI SessionStart hook concatenated the full ~10 KB routing
block into a plain-text string prefixed with "SessionStart:compact hook
success". Gemini CLI surfaced that string verbatim as user-visible
startup noise, per mksglu#299 ("the message is too verbose").

The Claude Code and VS Code Copilot SessionStart hooks already use
structured JSON (`{ hookSpecificOutput: { hookEventName, additionalContext } }`)
which Gemini CLI treats as hook metadata and does not render to the
user. This PR brings the Gemini hook in line — same contract, no new
config flag needed, hidden by default.

- hooks/gemini-cli/sessionstart.mjs: JSON.stringify output instead of
  the plaintext concatenation.
- tests/hooks/gemini-hooks.test.ts: two regression tests — one parses
  stdout and asserts the expected shape, one greps the source to lock
  in the JSON path (mirrors the existing vscode-copilot source check).

Tests:
- npx vitest run tests/hooks/gemini-hooks.test.ts → 13/13 pass
- npm test → 1597 pass, 23 skipped, 0 fail
- npm run typecheck → clean

Co-Authored-By: Claude <noreply@anthropic.com>
@mksglu
Copy link
Copy Markdown
Owner

mksglu commented Apr 20, 2026

Hi @ousamabenyounes! Did you test on local env for that? Did you repro and fixed?

@ousamabenyounes
Copy link
Copy Markdown
Author

ousamabenyounes commented Apr 20, 2026

Tested with Gemini CLI 0.38.2 and reproduced the issue end-to-end in interactive TUI mode (which is where the visible bug lives; -p non-interactive suppresses additionalContext on all paths).

Before — current next branch (plaintext), live in Gemini CLI

Gemini CLI v0.38.2
  Signed in with Google
  Plan: Gemini Code Assist for individuals

ℹ You have 1 extension with an update available. Run "/extensions update superpowers".

ℹ SessionStart:compact hook success: Success
  SessionStart hook additional context:
  <context_window_protection>
    <priority_instructions>
      Raw tool output floods your context window. You MUST use context-mode MCP tools to keep raw data in the sandbox.
    </priority_instructions>
    <tool_selection_hierarchy>
      1. GATHER: mcp__context-mode__ctx_batch_execute(commands, queries)
         ...
      2. FOLLOW-UP: mcp__context-mode__ctx_search(queries: ["q1", "q2", ...])
         ...
      3. PROCESSING: mcp__context-mode__ctx_execute(language, code) | ctx_execute_file(path, language, code)
         ...
    </tool_selection_hierarchy>
    <forbidden_actions> ... </forbidden_actions>
    <file_writing_policy> ... </file_writing_policy>
    <output_constraints> ... </output_constraints>
    <ctx_commands> ... </ctx_commands>
  </context_window_protection>

That's ~3.5 KB of hook internals rendered as user-visible text on every gemini startup — exactly what the reporter flagged in #299.

After — this PR, live in Gemini CLI

Gemini CLI v0.38.2
  Signed in with Google
  Plan: Gemini Code Assist for individuals

ℹ You have 1 extension with an update available. Run "/extensions update superpowers".

> Type your message or @path/to/file

Clean startup. No SessionStart:compact hook success line, no XML block. The routing context is still delivered (additionalContext stays 3489 chars, I verified via JSON.parse on the hook stdout) — Gemini just treats it as hook metadata instead of rendering it.

Methodology

Tested by swapping only the hooks/gemini-cli/sessionstart.mjs file between next HEAD and this PR's HEAD, same settings.json wiring, same Gemini CLI session. No other variables.

Direct raw stdout comparison (same hook input)

# OLD (next branch)
$ echo '{"source":"startup","session_id":"demo"}' | node hooks/gemini-cli/sessionstart.mjs
SessionStart:compact hook success: Success
SessionStart hook additional context:

<context_window_protection>
  <priority_instructions> ...

# NEW (this PR)
$ echo '{"source":"startup","session_id":"demo"}' | node hooks/gemini-cli/sessionstart.mjs
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"...3489 chars..."}}

So — reproduced on next, fix confirmed on this branch, tests locked in via tests/hooks/gemini-hooks.test.ts (the source-grep one mirrors the existing vscode-copilot lock at tests/hooks/vscode-hooks.test.ts:279). Ready whenever you are.

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.

2 participants