OTel: git_commit_id still silently dropped for `git -C <path> commit` and quiet/redirected commits (2.1.278) — recurrence of #77237

Status Open
Reported on v2.1.278
Maintainer reply None cached
Activity 3 comments · opened Sep 21, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using a recent version of Claude Code (2.1.278)

What's Wrong?

Re-filing #77237, which was auto-closed by the stale-issue bot on 2026-09-21 ("inactive for too long") with no code fix — verified below that the underlying behavior is unchanged in 2.1.278 (the original report was against 2.1.207/2.1.209).

With OTel enabled (OTEL_LOG_TOOL_DETAILS=1), git_commit_id on the claude_code.tool_result event is still extracted by scraping the Bash command string and the command's stdout, rather than by resolving the actual HEAD SHA. Two gates, both still present in 2.1.278:

Gate 1 — command string must match /\bgit\s+commit\b/. git -C <path> commit … fails this (the -C <path> separates the two words), even though the commit succeeds and prints a normal summary line to stdout.

Gate 2 — a [<ref> <sha>] summary line must be present in the captured stdout. git commit -q, --quiet, and >/dev/null redirection all suppress or drop that line, so extraction fails even though the command string passes gate 1.

Verification against 2.1.278 (bundled CLI)

strings on the installed binary (~/.local/share/claude/versions/2.1.278) shows the extraction call site is essentially unchanged from the original report:

We.command.match(/\bgit\s+commit\b/) && hr.data && typeof hr.data==="object" && "stdout" in hr.data
  ? (ss = await L6t(String(hr.data.stdout), s.session.project.cwd), ... Fn.git_commit_id = ss.commitId ...)
  : /* git_commit_id left unset */

L6t/D6t (the stdout-scrape + resolve helpers):

var q9r = /^\[(?:([\w./-]+)|detached HEAD)(?: \(root-commit\))? ([0-9a-f]{4,})\]/m;
function D6t(e){ let n = O6t(e).match(q9r); if (n?.[2]===void 0) return; return {sha:n[2], ...} }
async function L6t(e,n){ let r = D6t(e); if (!r) return; let s = await QPn(n); return {commitId: s?.startsWith(r.sha) ? s : r.sha, ...} }

Two things changed since the original report, neither of which fixes the bug:

  1. The stdout-scrape regex gained a ^…$/m anchor and detached HEAD support — cosmetic, doesn't change whether the summary line has to survive in the captured stdout.
  2. There's now a real git-internal HEAD-SHA resolver, QPn (walks .git refs directly — no shelling out, no stdout dependency), used inside L6t — but only to expand/verify the SHA D6t already scraped. If D6t returns undefined (gate 1 or gate 2 fails), L6t returns immediately and QPn is never called. The fix is already sitting right next to the bug: QPn(cwd) can resolve the current HEAD SHA independent of stdout, but it's wired as a scrape-dependent enrichment step instead of a scrape-independent fallback.

Live repro (git behavior, confirms the command-string gate is the live failure point)

$ git -C /tmp/repro commit -m "path form"
[master d6301e7] path form
 1 file changed, 1 insertion(+)
 create mode 100644 b.txt

$ echo 'git -C /tmp/repro commit -m x' | grep -qE '\bgit\s+commit\b' && echo yes || echo no
no

Git prints the normal [<ref> <sha>] summary line — the commit is fully attributable — but the command string never reaches gate 2 because gate 1 already rejected it.

$ git commit -q -m "quiet commit"
$ # stdout is empty — confirmed via `git commit -q -m ... 2>&1` capturing zero bytes

What Should Happen?

Per the monitoring-usage docs, git_commit_id should be present for any successful git commit, independent of command spelling or stdout formatting. Since the resolver needed to do this correctly (QPn/L6t's internal HEAD lookup) already exists in the codebase, the fix is to call it unconditionally whenever a Bash tool call whose command contains git and commit (in either order/adjacency) succeeds, rather than gating it behind a successful stdout scrape.

Steps to Reproduce

  1. Enable telemetry with tool details (CLAUDE_CODE_ENABLE_TELEMETRY=1, OTEL_LOGS_EXPORTER=otlp, OTEL_LOG_TOOL_DETAILS=1) against an OTLP collector you can inspect.
  2. In a git repo, have Claude run as separate Bash commands:
  • git commit --allow-empty -m ok → git_commit_id present
  • git -C . commit --allow-empty -m nope → git_commit_id absent (commit succeeds, SHA is knowable, telemetry silently drops it)
  • git commit -q --allow-empty -m nope → git_commit_id absent
  1. Inspect the exported claude_code.tool_result records.

Impact

Same as #77237: git_commit_id is the only structured link between a Claude-run commit and its SHA in OTel. Commit-attribution pipelines built on it undercount silently and with no error surfaced, specifically for the git -C <path> form (worktrees, multi-repo, path-explicit commits) and quiet/redirected commits.

This repo (ai-internal-enablement) enforces a written workaround for this exact gap in both org-level and project-level CLAUDE.md instructions (never use git -C <path> commit, never quiet/pipe a commit's stdout) — evidence this is a live operational cost for at least one downstream telemetry consumer, not a one-off report.

Claude Model

N/A — behavior is independent of the model.

Is this a regression?

No — confirmed present continuously from 2.1.207 (original report) through 2.1.278 (current, verified above).

Last Working Version

N/A — never worked as documented.

Claude Code Version

2.1.278

Platform

CLI (terminal)

Operating System

macOS (Darwin)

View original on GitHub ↗

3 Comments

fsc-eriker · 16 days ago

Adding a noise comment to prevent this from being closed.

benissimo · 16 days ago

@fsc-eriker thanks, from what I see of the sweep script, currently the only real guarantee is if the issue receives 10 or more thumbs up - without that, tickets eventually get closed even if unaddressed and containing full repro steps.

fsc-eriker · 2 days ago

Adding a noise comment to prevent this from being closed.