Skip to content

Use executed tool input on Claude tool spans - #199

Merged
Stephen Belanger (Qard) merged 1 commit into
mainfrom
fix/claude-tool-span-executed-input
Oct 20, 2026
Merged

Stephen Belanger (Qard) merged 1 commit into
mainfrom
fix/claude-tool-span-executed-input

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Member

Fixes #156.

The bug

Claude Code PreToolUse hooks can rewrite a proposed tool call by returning updatedInput. The terminal hook (PostToolUse, PostToolUseFailure, or PermissionDenied) reports the input the call actually ran with, or was denied with, in tool_input.

The Claude translator creates the tool span from PreToolUse.tool_input. When the terminal hook arrived, finish_tool merged in output, approval, error and metadata, but never input or name:

PreToolUse   tool_input: {"command": "rm -rf build"}  ──► Insert  name:  "Terminal: rm -rf build"
                                                               input: {"command": "rm -rf build"}
     │  hook returns updatedInput
     ▼
PostToolUse  tool_input: {"command": "ls build"}      ──► Merge   output, approval, metadata
                                                               (input and name untouched)

Resulting span: input says `rm -rf build`, output is from `ls build`

The span's metadata was rebuilt from the terminal input, so the span could also disagree with itself: its input/name showed the proposed call while its input-derived metadata reflected the one that ran.

The fix

PendingTool now remembers the proposed input. When the terminal hook reports a different tool_input, the merge replaces the span's input and recomputes its name:

let rewritten = raw_tool_input(&event.payload).is_some() && input != pending.input;
ops.push(SpanOp::Merge(SpanRow {
    name: if rewritten { tool_span_name(&tool_name, &input) } else { String::new() },
    input: rewritten.then_some(input),
    // ...
}));
  • Unchanged input is not resent. Most calls are never rewritten, and Write/Edit inputs can carry whole files, so the merge only includes input when it actually changed. An empty name leaves the existing span name as is in the sink.
  • A missing tool_input keeps the proposed input. raw_tool_input() tells "the hook reported no input" apart from an empty one, so the {} fallback in tool_input() can't overwrite the original.
  • Denied calls follow the same rule. The permission check evaluates the rewritten call, so PermissionDenied.tool_input is what was actually denied.
  • Replays stay no-ops. A duplicate terminal hook for an already-finished call is still ignored, so it cannot bring back the proposed input.

raw_tool_input() is now the only place that looks up the tool_input/input keys; tool_input() is built on top of it.

The test helper reduce() now applies input and name from merges (it silently dropped them before, which is how this went unnoticed). The tool-span tests also share one tool_span() lookup helper, and two overlapping hook-outcome tests are merged into one.

Known limitation

Braintrust deep-merges object fields on merge. If a hook removes a key from the input, the old key stays on the span; changed values are replaced correctly. Fixing that needs _merge_paths support in the sink, which the SDK's SpanLog builder doesn't expose yet.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T23:33:20.298216Z 2527fd7 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Claude Code PreToolUse hooks can rewrite a call's arguments via
updatedInput. The tool span was created from PreToolUse.tool_input and
the terminal hook's merge never replaced it, so traces could show the
proposed arguments next to the output of a different executed call.

The terminal merge now carries the terminal hook's tool_input and a
recomputed span name when it differs from the proposed input.

Fixes #156

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the fix/claude-tool-span-executed-input branch from 7ce01eb to 2527fd7 Compare October 19, 2026 23:30
@Qard
Stephen Belanger (Qard) merged commit 97c094b into main Oct 20, 2026
22 checks passed
@Qard
Stephen Belanger (Qard) deleted the fix/claude-tool-span-executed-input branch October 20, 2026 09:04
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.

Claude tool spans can show pre-rewrite arguments instead of executed input

2 participants