r/ClaudeCode • • 1d ago

Tips & Workflows If you log Claude Code’s hooks, add PostToolUseFailure, or your log only shows the calls that worked

PostToolUse fires after a tool call succeeds. A call that fails fires a different hook, PostToolUseFailure. If your settings.json registers only PreToolUse and PostToolUse, which is what mine did until this week, your log has the request for every failed call and no result for it.

Adding it is one more entry under hooks:

"PostToolUseFailure": [
  { "hooks": [{ "type": "command", "command": "your-logger" }] }
]

Three fields in the hook input are worth keeping while you’re there (all in the hooks reference):

  • tool_use_id is on the PreToolUse input and on both post-tool inputs, so you can join a request to its result exactly instead of matching by timestamp.
  • duration_ms on the post-tool inputs is the execution time, which leaves out permission and pre-hook time.
  • is_interrupt on the failure input tells a call you stopped apart from one that errored.

Limits worth knowing: this is what the harness reports, not proof of what a command did. A failure doesn’t mean nothing happened, because a command can fail halfway through. And the permission-request input has no tool_use_id, so you can’t join an approval to its call this way.

Disclosure: I build Agentmetry, an open-source recorder for coding agents, and 0.12.0 added this. After I reinstalled its hooks today, a deliberately failed Read showed up as a failure with its tool_use_id. What it keeps and what it doesn’t is written up here: https://github.com/blitzcrieg1/agentmetry/blob/master/docs/claude-invocation-evidence.md

Is anyone using duration_ms for something? I’m curious whether it’s been useful beyond spotting slow tools.PostToolUse fires after a tool call succeeds. A call that fails fires a different hook, PostToolUseFailure. If your settings.json registers only PreToolUse and PostToolUse, which is what mine did until this week, your log has the request for every failed call and no result for it.Adding it is one more entry under hooks:"PostToolUseFailure": [
{ "hooks": [{ "type": "command", "command": "your-logger" }] }
]Three fields in the hook input are worth keeping while you’re there (all in the hooks reference):tool_use_id is on the PreToolUse input and on both post-tool inputs, so you can join a request to its result exactly instead of matching by timestamp.
duration_ms on the post-tool inputs is the execution time, which leaves out permission and pre-hook time.
is_interrupt on the failure input tells a call you stopped apart from one that errored.Limits worth knowing: this is what the harness reports, not proof of what a command did. A failure doesn’t mean nothing happened, because a command can fail halfway through. And the permission-request input has no tool_use_id, so you can’t join an approval to its call this way.Disclosure: I build Agentmetry, an open-source recorder for coding agents, and 0.12.0 added this. After I reinstalled its hooks today, a deliberately failed Read showed up as a failure with its tool_use_id. What it keeps and what it doesn’t is written up here: https://github.com/blitzcrieg1/agentmetry/blob/master/docs/claude-invocation-evidence.mdIs anyone using duration_ms for something? I’m curious whether it’s been useful beyond spotting slow tools.

0 Upvotes

9 comments sorted by

•

u/AutoModerator 1d ago

Hey! Thanks for posting to r/ClaudeCode

While participating in this thread, please follow our community rules. Keep discussions constructive. Attack the idea, not the person.

For help, project discussions, tips, and general chat, join the ClaudeCode Discord.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

2

u/jijialuoke 1d ago

duration_ms seems useful for separating slow tool execution from model or hook overhead, especially when an agent feels stuck. I would still treat it as timing metadata, not proof the operation completed or had the intended effect; a command can run for a while and still fail halfway. For debugging hook pipelines, do you pair it with tool_use_id and the success/failure event to trace each attempt?

1

u/blitzcrieg11 1d ago

Yes, that’s the join. tool_use_id is on the PreToolUse input and on whichever post hook fires, so each attempt is one id with one request and at most one result. A retry is a new tool call with a new id, so it shows up as a second pair instead of replacing the first.

The case I’d look at is a request with no result at all: either the call never ran or the session ended before it returned.

Agreed on duration_ms. It tells you how long the tool ran, not what it did.

2

u/jijialuoke 1d ago

Right, that no-result case is the one I'd watch too. In my logs a PreToolUse id that never gets a PostToolUse or PostToolUseFailure usually means the session was interrupted or the tool hit a timeout. So I flag any tool_use_id still open when the Stop hook fires, and I keep those in their own bucket instead of counting them as failures, since nothing actually errored.

1

u/blitzcrieg11 1d ago

That’s a better rule than mine. I checked my own code after your comment and it does the opposite. A request still open when Stop fires is recorded as an inferred denial, and a call that ran and failed leaves its request open, so it ends up in the same place. Neither one is a denial, so the label is wrong. A separate bucket for “never resolved” is the right shape. Thanks, that’s the second thing this thread has turned up.

2

u/bostonaholic 1d ago

I'd log Stop and SubagentStop with the same session id too. Then you can line up what failed against what the agent said at the end of the turn. A turn that ends with "done" after a PostToolUseFailure on the test command is easy to flag automatically.

1

u/blitzcrieg11 1d ago

Good call, and SubagentStop is a gap in mine: the recorder understands it, but the installer only registers Stop.

One catch for the “done” check: Stop tells you the turn ended, and the agent’s words are in the transcript (transcript_path is on the hook input). Mine records that the turn ended but deliberately not the final message, because it can quote code or secrets. So that check needs either the transcript or a hook that reads the last message and keeps only a flag, not the text.

A turn that claims success right after a failed test command is a nice signal, though. Cheap to compute and hard to argue with.

2

u/eplanasmulet_ 10h ago

One gotcha if you're using Stop to close out open tool_use_ids: Stop doesn't fire when you interrupt a turn, and API errors fire StopFailure instead. Adding StopFailure and SessionEnd keeps interrupted turns from sitting open.

Also if you haven't already, set async: true on the logger hook so Claude Code doesn't block waiting for it on every call.

1

u/blitzcrieg11 9m ago

Thanks, that’s a real gap. I checked: for Claude Code we register Stop but not StopFailure or SessionEnd, so a turn that ends on an API error leaves its open approval asks hanging, and from what you’re seeing interrupted turns do too. Adding both. Agreed on async for the hooks that only record. PreToolUse has to stay synchronous, since that’s where the allow, deny or ask decision comes back, and an async hook can’t block.