Skip to content

ci: add sidecar start/stop lifecycle for Claude Code telemetry#107

Open
jqdsouza wants to merge 3 commits into
mainfrom
claude/ci-telemetry-sidecar-EhfQg
Open

ci: add sidecar start/stop lifecycle for Claude Code telemetry#107
jqdsouza wants to merge 3 commits into
mainfrom
claude/ci-telemetry-sidecar-EhfQg

Conversation

@jqdsouza

@jqdsouza jqdsouza commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a sidecar lifecycle (beacon ci start / beacon ci stop) for Claude Code CI telemetry. ci exec only captures telemetry when it can wrap the Claude invocation; the sidecar covers the common case where Claude runs inside another action or a later step you can't wrap.

- name: Start Beacon telemetry
  run: beacon ci start --forward splunk --forward-endpoint "$SPLUNK_HEC_URL"
  env:
    BEACON_CI_SPLUNK_HEC_TOKEN: ${{ secrets.SPLUNK_HEC_TOKEN }}

- uses: some-org/claude-code-action@v1   # emits OTLP to the sidecar
  with: { prompt: "Review this PR" }

- name: Stop Beacon telemetry
  if: always()
  run: beacon ci stop

How it works

  • ci start provisions and launches the collector detached (background, reparented to init on exit), records a small tokenless session state file ($RUNNER_TEMP/beacon/session.json), and exports the Claude OTLP env vars — appending them to $GITHUB_ENV in GitHub Actions so every later step in the job emits to the sidecar.
  • ci stop reads the state file, terminates the collector by PID (SIGTERM, escalating to kill after a grace period), validates the runtime log, and honors --require-telemetry / --keep-artifacts exactly like ci exec.

Notable details

  • Single source of truth for the env vars. New ClaudeTelemetryVars is shared by in-process injection (ci exec) and the $GITHUB_ENV export (ci start); ClaudeEnv is refactored to use it.
  • Token never persisted. The forwarding token is read from the collector's environment at start time (the merged-in ${env:...} mechanism) and is never written to the state file; a round-trip test asserts the state contains no token.
  • No zombies. StartDetached reaps the child via a goroutine if it exits while the short-lived ci start process is alive; in normal use ci start exits immediately and the OS reparents/reaps after ci stop signals by PID.

Tests

New sidecar_test.go (start→stop lifecycle with a fake collector, readiness-failure returns error + resets PID, state round-trip with no-token assertion) and a ClaudeTelemetryVars retention test. internal/ci passes under -race; cmd green; go vet and gofmt clean.

Note: pre-existing endpoint/harness and lifecycle failures in a full local run are container artifacts (macOS embedded binary on a Linux container) and are unrelated to this change.

Relationship to other PRs

Independent of the composite action (#106). The two compose: a future JS action with a post: hook could wrap start/stop, but the documented start … always()-stop steps already deliver the capability today. Includes a reference workflow in examples/.

https://claude.ai/code/session_01XeN6mjkot19NzydHEazaWq


Generated by Claude Code


Note

Medium Risk
Introduces background process management and persists session metadata on disk; SIEM tokens are excluded from state but collector config on disk may still hold env-referenced secrets as in exec.

Overview
Adds a sidecar CI lifecycle (beacon ci start / beacon ci stop) so Claude Code telemetry can be collected when later steps or third-party actions run Claude without wrapping them in beacon ci exec.

ci start provisions the ephemeral collector, runs it detached with PID recorded in a tokenless session.json, appends Claude OTLP env vars to $GITHUB_ENV on GitHub Actions (and prints them), and supports the same forwarding/collector flags as exec. ci stop loads that state, stops the collector by PID, validates the runtime log (including --require-telemetry / --keep-artifacts), and can clean up artifacts.

Refactors Claude OTLP configuration into shared ClaudeTelemetryVars (used by ClaudeEnv and ci start). New sidecar.go implements detached start/stop, state I/O, and process liveness checks, with tests and README plus an example GitHub Actions workflow.

Reviewed by Cursor Bugbot for commit 02f5bb3. Bugbot is set up for automated code reviews on this repo. Configure here.

`ci exec` only captures telemetry when it wraps the Claude invocation. The
sidecar lifecycle covers the case where Claude runs inside another action or
a later step that cannot be wrapped:

- `beacon ci start` provisions and launches the collector in the background,
  records a tokenless session state file, and exports the Claude OTLP
  environment variables (appending them to $GITHUB_ENV in GitHub Actions) so
  every later step in the job emits to the sidecar.
- `beacon ci stop` reads the state file, terminates the collector by PID
  (escalating to kill after a grace period), validates the runtime log, and
  honors --require-telemetry / --keep-artifacts like `ci exec`.

The Claude telemetry variables are now produced by a single
ClaudeTelemetryVars source of truth shared by in-process injection (ci exec)
and the $GITHUB_ENV export (ci start). The forwarding token is read from the
collector's environment at start and never written to the state file.

Includes a reference sidecar workflow under examples/ and README docs.

https://claude.ai/code/session_01XeN6mjkot19NzydHEazaWq
Comment thread cli/beacon/cmd/ci.go Fixed
Comment thread cli/beacon/cmd/ci.go Fixed
Comment thread cli/beacon/cmd/ci.go
Comment thread cli/beacon/cmd/ci.go
…o start

Bug 1: ci stop now derives the state file path from --log-path when
neither --state-file nor --base-dir is specified, mirroring how ci start
derives BaseDir from the log path via resolveBaseDir.

Bug 2: ci start now hooks SIGINT/SIGTERM so that if the process is
interrupted after StartDetached succeeds, the detached collector is
stopped instead of being orphaned with ports held.

Co-authored-by: Justin D'Souza <jqdsouza@users.noreply.github.com>
Comment thread cli/beacon/cmd/ci.go Fixed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stop lacks base-dir flag
    • Added ciStopCmd to the --base-dir flag registration loop so that beacon ci stop --base-dir correctly resolves the session state path written by ci start --base-dir.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 453fad6. Configure here.

Comment thread cli/beacon/cmd/ci.go
The --base-dir flag was only registered for ci exec and ci start, but
runCIStop checks ciOpts.baseDir to resolve the session state path. Without
the flag on ci stop, ciOpts.baseDir was always empty, causing stop to fall
back to DefaultStatePath() and fail to find the session state written by
a start that used a custom base directory. This left the sidecar collector
running.

Add ciStopCmd to the --base-dir flag registration loop and mark it hidden
(consistent with exec and start).

Co-authored-by: Justin D'Souza <jqdsouza@users.noreply.github.com>
Comment thread cli/beacon/cmd/ci.go
if err != nil {
return err
}
defer logFile.Close()
Comment thread cli/beacon/cmd/ci.go
if err != nil {
return err
}
defer f.Close()
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.

3 participants