Skip to content

Run SessionStart initialization only once per Claude session #112

@GuyMoses

Description

@GuyMoses

Problem

Claude Code fires the SessionStart hook multiple times within a single session — not just on startup, but also on resume, clear, and compact (distinguished by the event's source field). The plugin's SessionStart handling has no guard against re-fires, so it runs its startup work every time.

In cmd/on-event/main.go:

  • main.go:552-560 — overwrites the saved trace context (SaveTraceContext) on every SessionStart.
  • main.go:614-628 — re-runs otlp.CheckConnectivity(cfg) (a network round-trip) and re-prints the user-visible status message on every SessionStart:
if hookEvent == "SessionStart" {
    if cfg.OTLPUrl == "" {
        printHookResponse("dash0: telemetry is not active — …", "")
    } else if err := otlp.CheckConnectivity(cfg); err != nil {
        printHookResponse(fmt.Sprintf("dash0: connectivity check failed — %v", err), "")
    } else {
        printHookResponse(fmt.Sprintf("dash0: connected (v%s)", version.Version), "")
    }
}

The hook payload is not inspected for source, so a resume/compact/clear is indistinguishable from a fresh startup.

Symptoms

  • The user sees dash0: connected (vX) (or the not-active / connectivity-failed message) repeatedly within one session — once on startup and again after every compaction, resume, or /clear.
  • A redundant connectivity network call fires on each of those events.
  • The saved trace context is clobbered mid-session, which can disrupt trace-context continuity for an in-progress turn when a compact/resume lands between UserPromptSubmit and Stop.

Request

Make SessionStart initialization run once per session:

  • Guard the connectivity check + status message (and the trace-context init) so subsequent SessionStart fires for the same session are no-ops. Options:
    • Gate on the event's source field (source == "startup" only), and/or
    • Use a per-session marker in sessionDir (e.g. a started sentinel file) written on first SessionStart and checked on subsequent ones — robust even if source semantics change.
  • On a re-fire, do not overwrite existing trace context (or only initialize it when absent).

Note: the per-session sessionDir already exists (main.go:546), so a sentinel file there is a natural fit and is cleaned up at SessionEnd with the rest of the directory.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions