Skip to content

Capture new Claude Code hook fields (effort, duration_ms, is_interrupt) - #200

Open
Abhijeet Prasad (AbhiPrasad) wants to merge 2 commits into
mainfrom
check-claude-code-hooks-9-6b6e3d65
Open

Abhijeet Prasad (AbhiPrasad) wants to merge 2 commits into
mainfrom
check-claude-code-hooks-9-6b6e3d65

Conversation

@AbhiPrasad

@AbhiPrasad Abhijeet Prasad (AbhiPrasad) commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

What was missing

Claude Code hooks now send more input fields; the same set showed up in claude-agent-sdk 0.2.166 (braintrustdata/braintrust-sdk-python#973). The Claude Code translator reads payload fields directly instead of going through an allowlist, so last_assistant_message, error, and prompt_id were already being used. Three fields were still dropped:

Hook field Hooks Before
effort ({"level": "high"}) all ignored
duration_ms PostToolUse / PostToolUseFailure used only to work out start_ms when there was no PreToolUse, never recorded
is_interrupt PostToolUseFailure ignored

The fix

Effort → turn metadata. effort is decoded in HookContext and tracked in observe_session_details together with permission_mode. Claude Code doesn't send effort on UserPromptSubmit; it shows up on later hooks (Stop, PreToolUse, PostToolUse). So reading it only when a turn opens isn't enough. Each turn tracks the level it has recorded, and when a different level arrives it is merged into the active turn:

UserPromptSubmit                   ──►  Turn 1 opens (effort unknown)
PreToolUse {effort:{level:"high"}} ──►  merge Turn 1  metadata.effort = "high"
Stop                               ──►  Turn 1 closes
/effort max
UserPromptSubmit                   ──►  Turn 2 opens  metadata.effort = "high" (carried)
Stop {effort:{level:"max"}}        ──►  merge Turn 2  metadata.effort = "max"
fn record_turn_effort(&mut self, ops: &mut Vec<SpanOp>) {
    let Some(turn) = self.turn.as_mut() else { return };
    if self.effort.is_none() || turn.effort == self.effort {
        return;
    }
    turn.effort = self.effort.clone();
    ops.push(SpanOp::Merge(SpanRow {
        span_id: turn.id.clone(),
        metadata: Some(json!({ "effort": turn.effort })),
        // ...
    }));
}

Tool timing / interrupts → tool metadata. finish_tool reads duration_ms once. That one value sets both native_duration_ms (the key the Cursor translator already uses) and the fallback start time, so the span timing and the metadata can't disagree:

let duration = event.payload.get("duration_ms").and_then(Value::as_i64);
if let Some(duration) = duration {
    metadata["native_duration_ms"] = json!(duration);
}
if let Some(interrupted) = event.payload.get("is_interrupt").and_then(Value::as_bool) {
    metadata["is_interrupt"] = json!(interrupted);
}
// ...
start_ms: Some(event.ts_ms.saturating_sub(duration.unwrap_or(0))),

Span timing doesn't change when PreToolUse exists: the span still runs from PreToolUse to PostToolUse.

prompt_id comment. The old comment said native hooks never send prompt_id. They now send it after the first user input, though older Claude Code versions leave it out. The matching code already handled both cases, so only the comment changed.

Test cleanup

The Claude translator tests went from 33 to 29 by merging overlapping cases: the three root-output tests, the two SubagentStart-validation tests, and the success and failure tool-outcome tests. Hand-built Envelope literals were replaced with the existing claude_event helper.

🤖 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:27.653106Z e6bf913 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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 552e777d85

ℹ️ About Codex in GitHub

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

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment on lines +445 to +446
if let Some(effort) = &hook.effort {
self.effort = Some(effort.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the active turn when effort first appears

Claude does not consistently include effort on UserPromptSubmit: for example, subagent-compact/events.ndjson line 2 omits it and the corresponding Stop on line 4 reports high. Because open_turn snapshots self.effort only when the turn opens, this assignment merely caches the late value for the next turn and never adds it to the turn that produced the hook. Consequently, the first turn loses known effort metadata, and after an effort change a later turn can retain the previous level until another turn opens; merge newly observed effort into the currently active turn as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, fixed in 8e34e04. The recorded fixtures confirm that UserPromptSubmit never carries effort; it only arrives on later hooks. Each turn now tracks the level it has recorded, and a newly observed level is merged into the active turn (record_turn_effort). The real-fixture test now asserts that every turn gets xhigh.

Record effort.level on turn spans, and duration_ms / is_interrupt on tool
spans. Effort is decoded through HookContext alongside permission_mode,
and duration_ms is read once per tool completion so span timing and
metadata agree. Consolidate overlapping Claude translator tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude Code omits effort on UserPromptSubmit and reports it on later hooks
(Stop, PreToolUse, PostToolUse). Snapshotting effort only when a turn opened
left the first turn without it and let later turns carry a stale level.
Track the level each turn recorded and merge a newly observed one into the
active turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the check-claude-code-hooks-9-6b6e3d65 branch 2 times, most recently from af2f4a7 to e6bf913 Compare October 19, 2026 23:31

This branch has not been deployed

No deployments
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.

2 participants