Summary
In ACP mode, stdout can be polluted by non-JSON debug lines when project hooks are enabled and the ACP session closes. The ACP client then logs Failed to parse JSON message: for hook planner/runner debug lines even though the hook itself succeeds.
Examples of leaked lines:
Deduplicated hook: ...
Created execution plan for SessionEnd: ...
Expanding hook command: ...
Hook execution for SessionEnd: ...
Why this matters
ACP stdout should remain strict NDJSON. Even when the hook behavior is correct, the client sees protocol corruption/noise and reports parser warnings.
More importantly, this blocks practical use of Gemini CLI as a dependable ACP server for external clients, orchestrators, and IDE integrations. The broader goal here is not just to remove noise in a test run, but to make Gemini CLI ACP mode reliable enough to be used as a real integration surface.
Repro
- Configure project-level hooks with a
SessionEnd matcher for exit.
- Start Gemini CLI in ACP mode from the bundle, for example
bundle/gemini.js --acp.
- Connect an ACP client over
acp.ndJsonStream(...).
- Run a prompt that triggers at least one tool call and then close stdin / close the ACP connection.
- Observe client-side parse warnings from
@agentclientprotocol/sdk/dist/stream.js with Failed to parse JSON message:.
I reproduced this while validating ACP hook behavior with a SessionEnd(exit) hook and an AfterTool(write_file) hook.
Expected
Only NDJSON protocol messages should be emitted on stdout in ACP mode. Debug / feedback text should stay on stderr.
That guarantee is important if Gemini CLI ACP mode is intended to be used as a real server boundary rather than only as an internal/dev surface.
Actual
During shutdown, hook planning/execution debug lines are emitted to stdout, so the ACP client tries to parse them as NDJSON and reports parse failures.
Diagnosis
This looks like a cleanup ordering bug between the global console patch and ACP shutdown:
packages/cli/src/gemini.tsx installs a global ConsolePatcher and registers consolePatcher.cleanup with registerCleanup(...).
packages/cli/src/acp/acpClient.ts also installs an ACP-specific ConsolePatcher.
- ACP shutdown awaits
connection.closed.finally(runExitCleanup).
- During
runExitCleanup(), the global cleanup restores the console before SessionEnd hook debug logging is fully done.
- The later
debugLogger.debug(...) calls from hook planning / execution then fall back to stdout.
Candidate fix
Keep a console patch active for the entire ACP shutdown lifecycle.
One local fix that stopped the parser warnings was:
- treat ACP as non-interactive for the global patch setup, and
- do not register the global
ConsolePatcher.cleanup in ACP mode, so the console remains redirected until ACP shutdown is fully complete.
The ACP-local patch can still be restored at the very end.
Validation
After applying that fix locally, these passed:
npm run bundle
npm run test:integration:sandbox:none -- ./acp-success-memory-hooks.test.ts
npm run test:integration:sandbox:none -- ./acp-telemetry.test.ts
The stderr warnings are still visible, but ACP stdout stays clean and the client no longer logs NDJSON parse failures.
Summary
In ACP mode, stdout can be polluted by non-JSON debug lines when project hooks are enabled and the ACP session closes. The ACP client then logs
Failed to parse JSON message:for hook planner/runner debug lines even though the hook itself succeeds.Examples of leaked lines:
Deduplicated hook: ...Created execution plan for SessionEnd: ...Expanding hook command: ...Hook execution for SessionEnd: ...Why this matters
ACP stdout should remain strict NDJSON. Even when the hook behavior is correct, the client sees protocol corruption/noise and reports parser warnings.
More importantly, this blocks practical use of Gemini CLI as a dependable ACP server for external clients, orchestrators, and IDE integrations. The broader goal here is not just to remove noise in a test run, but to make Gemini CLI ACP mode reliable enough to be used as a real integration surface.
Repro
SessionEndmatcher forexit.bundle/gemini.js --acp.acp.ndJsonStream(...).@agentclientprotocol/sdk/dist/stream.jswithFailed to parse JSON message:.I reproduced this while validating ACP hook behavior with a
SessionEnd(exit)hook and anAfterTool(write_file)hook.Expected
Only NDJSON protocol messages should be emitted on stdout in ACP mode. Debug / feedback text should stay on stderr.
That guarantee is important if Gemini CLI ACP mode is intended to be used as a real server boundary rather than only as an internal/dev surface.
Actual
During shutdown, hook planning/execution debug lines are emitted to stdout, so the ACP client tries to parse them as NDJSON and reports parse failures.
Diagnosis
This looks like a cleanup ordering bug between the global console patch and ACP shutdown:
packages/cli/src/gemini.tsxinstalls a globalConsolePatcherand registersconsolePatcher.cleanupwithregisterCleanup(...).packages/cli/src/acp/acpClient.tsalso installs an ACP-specificConsolePatcher.connection.closed.finally(runExitCleanup).runExitCleanup(), the global cleanup restores the console beforeSessionEndhook debug logging is fully done.debugLogger.debug(...)calls from hook planning / execution then fall back to stdout.Candidate fix
Keep a console patch active for the entire ACP shutdown lifecycle.
One local fix that stopped the parser warnings was:
ConsolePatcher.cleanupin ACP mode, so the console remains redirected until ACP shutdown is fully complete.The ACP-local patch can still be restored at the very end.
Validation
After applying that fix locally, these passed:
npm run bundlenpm run test:integration:sandbox:none -- ./acp-success-memory-hooks.test.tsnpm run test:integration:sandbox:none -- ./acp-telemetry.test.tsThe stderr warnings are still visible, but ACP stdout stays clean and the client no longer logs NDJSON parse failures.