Skip to content

Filter sidecar Haiku flows so session title generation stops leaking into the transcript - #25

Merged
Juliusolsson05 merged 1 commit into
mainfrom
fix/claude-title-haiku-sidecar-leak
Apr 27, 2026
Merged

Juliusolsson05 merged 1 commit into
mainfrom
fix/claude-title-haiku-sidecar-leak

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Apr 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Claude Code v2.1.x makes a small auxiliary /v1/messages call at session start to generate a 3-7 word session title via queryHaiku() (vendor utils/sessionTitle.ts). The proxy adapter currently treats it as a normal assistant turn — turn_started + text_delta + block_completed all fire on the SemanticChannel — and the renderer mints a ghost entry for the JSON-shaped output. Because Claude Code never writes auxiliary calls to the JSONL rollout, that ghost is never superseded, orphans after its 3 s TTL, and gets rendered as a phantom {"title": "..."} message at the bottom of the transcript.

The smoking gun, decoded straight from a captured proxy log:

POST /v1/messages?beta=true
event: message_start
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_013xNUQa..."}}

— a Haiku sidecar inside an Opus session, exactly matching generateSessionTitle() in the vendor source.

The actual fix lives in claude-code-headless (submodule SHA bumped from 7984e29 → 35bb19c, branch fix/sidecar-haiku-leak, PR Juliusolsson05/claude-code-headless#3). The adapter now demotes a flow to the existing 'secondary' attribution state at message_start when the flow's model matches /haiku/i AND a caller-supplied getSessionModel() returns a model that does not. The rule reads "filter Haiku flows on non-Haiku sessions" — catches every known auxiliary Haiku call (title gen, compaction summaries, hook agents, teleport title-and-branch) without false-positiving Haiku-as-primary users.

This PR's cc-shell side is the wiring: pass-through of the new getSessionModel and sidecarModelPattern options through ClaudeCodeHeadlessOptions.proxy, and in claudeSession.ts an opt-in that returns a non-Haiku constant by default with a CC_SHELL_PRIMARY_MODEL env-var escape hatch for users running Haiku as their primary model.

Dependency

This PR's submodule pointer references 35bb19c on the headless repo. That commit lives on fix/sidecar-haiku-leak and is being reviewed in Juliusolsson05/claude-code-headless#3. The headless PR should land first so the submodule SHA is reachable on main when this PR merges (otherwise the submodule pointer dangles on a feature branch).

What's NOT in this PR

  • Screen-header parsing of the actual primary model. Today getSessionModel() returns the constant 'claude-opus-4-7' because the model only matters for the /haiku/i pattern test, never as an authoritative model id. A TODO in claudeSession.ts marks where to drop in a real parser later.
  • Bundling the .gitignore change as a separate commit. The .worktrees/ ignore rule is the second commit in the diff (9fa530b); it's small infra for the development workflow that produced this fix. Happy to split if you'd rather it be a standalone PR.

Test plan

  • Headless package: npx tsx packages/claude-code-headless/src/testing/verify-sidecar-haiku.ts — 13/13 cases green
    • Haiku title-gen on Sonnet session → filtered (no turn_started, no text_delta, no block_completed, flow_ignored published)
    • Real Sonnet flow on Sonnet session → unaffected
    • Haiku flow on Haiku-as-primary session → unaffected (false-positive guard)
    • Bundle-replay (Haiku title flow → Opus real flow) → only the Opus flow surfaces
    • No getSessionModel callback → filter is inert (preserves pre-fix behaviour)
  • tsc --noEmit for claudeSession.ts, ClaudeProxyAdapter.ts, ClaudeCodeHeadless.ts, and the new test — clean
  • Manual: npm run dev against this branch on a fresh Opus/Sonnet session, confirm the {"title": "..."} ghost no longer appears at the bottom of the transcript
  • Manual: same on a Haiku-as-primary session with CC_SHELL_PRIMARY_MODEL=claude-haiku-4-5-20251001 set, confirm Haiku conversation turns still render

🤖 Generated with Claude Code

Bumps the claude-code-headless submodule to pick up the
sidecar-flow filter added in
claude-code-headless@a91e4a9, then opts the proxy adapter into
filtering by passing a `getSessionModel` callback.

The callback returns a constant non-Haiku model id by default
(`'claude-opus-4-7'`) because cc-shell does not yet parse the
user's primary model from Claude Code's screen header. The exact
string doesn't matter; the adapter only uses it for the
`/haiku/i` pattern test, never as an authoritative model id.
Users running Haiku as their primary model can override via the
`CC_SHELL_PRIMARY_MODEL` env var (set it to a Haiku model id to
disable filtering).

Without this wiring the headless-side filter is inert and the
production bug — Claude Code title-generation calls leaking as
phantom `{"title": "..."}` ghost entries at the bottom of the
transcript — would still reproduce.

Follow-up TODO carried in the comment: replace the constant
default with screen-header parsing so the env-var escape hatch
becomes redundant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant